add quarantine videos feature (#1637)
* add quarantine videos feature * increase Notification settings test timeout to 20000ms. was completing 7000 locally but timing out after 10000 on travis * fix quarantine video test issues -propagate misspelling -remove skip from server/tests/client.ts * WIP use blacklist for moderator video approval instead of video.quarantine boolean * finish auto-blacklist feature
This commit is contained in:
parent
12fed49eba
commit
7ccddd7b52
|
@ -11,7 +11,12 @@ import { JobsComponent } from './jobs/job.component'
|
|||
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
|
||||
import { JobService } from './jobs/shared/job.service'
|
||||
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
|
||||
import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
|
||||
import {
|
||||
ModerationCommentModalComponent,
|
||||
VideoAbuseListComponent,
|
||||
VideoBlacklistListComponent,
|
||||
VideoAutoBlacklistListComponent
|
||||
} from './moderation'
|
||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
||||
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
|
||||
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
|
||||
|
@ -42,6 +47,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
|
|||
ModerationComponent,
|
||||
VideoBlacklistListComponent,
|
||||
VideoAbuseListComponent,
|
||||
VideoAutoBlacklistListComponent,
|
||||
ModerationCommentModalComponent,
|
||||
InstanceServerBlocklistComponent,
|
||||
InstanceAccountBlocklistComponent,
|
||||
|
|
|
@ -161,6 +161,23 @@
|
|||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div i18n class="inner-form-title">Auto-blacklist</div>
|
||||
|
||||
<ng-container formGroupName="autoBlacklist">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="ofUsers">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="New videos of users automatically blacklisted enabled"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div i18n class="inner-form-title">Administrator</div>
|
||||
|
||||
<div class="form-group" formGroupName="admin">
|
||||
|
|
|
@ -117,6 +117,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
|
||||
allowAdditionalExtensions: null,
|
||||
resolutions: {}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from './video-abuse-list'
|
||||
export * from './video-auto-blacklist-list'
|
||||
export * from './video-blacklist-list'
|
||||
export * from './moderation.component'
|
||||
export * from './moderation.routes'
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
<div class="admin-sub-nav">
|
||||
<a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a>
|
||||
|
||||
<a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a>
|
||||
<a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">{{ autoBlacklistVideosEnabled ? 'Manually blacklisted videos' : 'Blacklisted videos' }}</a>
|
||||
|
||||
<a *ngIf="autoBlacklistVideosEnabled && hasVideoBlacklistRight()" i18n routerLink="video-auto-blacklist/list" routerLinkActive="active">Auto-blacklisted videos</a>
|
||||
|
||||
<a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
|
||||
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
import { Component } from '@angular/core'
|
||||
import { UserRight } from '../../../../../shared'
|
||||
import { AuthService } from '@app/core/auth/auth.service'
|
||||
import { AuthService, ServerService } from '@app/core'
|
||||
|
||||
@Component({
|
||||
templateUrl: './moderation.component.html',
|
||||
styleUrls: [ './moderation.component.scss' ]
|
||||
})
|
||||
export class ModerationComponent {
|
||||
constructor (private auth: AuthService) {}
|
||||
autoBlacklistVideosEnabled: boolean
|
||||
|
||||
constructor (
|
||||
private auth: AuthService,
|
||||
private serverService: ServerService
|
||||
) {
|
||||
this.autoBlacklistVideosEnabled = this.serverService.getConfig().autoBlacklist.videos.ofUsers.enabled
|
||||
}
|
||||
|
||||
hasVideoAbusesRight () {
|
||||
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
|
||||
|
|
|
@ -3,6 +3,7 @@ import { UserRight } from '../../../../../shared'
|
|||
import { UserRightGuard } from '@app/core'
|
||||
import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
|
||||
import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
|
||||
import { VideoAutoBlacklistListComponent } from '@app/+admin/moderation/video-auto-blacklist-list'
|
||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
|
||||
|
||||
|
@ -26,6 +27,11 @@ export const ModerationRoutes: Routes = [
|
|||
redirectTo: 'video-blacklist/list',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'video-auto-blacklist',
|
||||
redirectTo: 'video-auto-blacklist/list',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'video-abuses/list',
|
||||
component: VideoAbuseListComponent,
|
||||
|
@ -37,6 +43,17 @@ export const ModerationRoutes: Routes = [
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'video-auto-blacklist/list',
|
||||
component: VideoAutoBlacklistListComponent,
|
||||
canActivate: [ UserRightGuard ],
|
||||
data: {
|
||||
userRight: UserRight.MANAGE_VIDEO_BLACKLIST,
|
||||
meta: {
|
||||
title: 'Auto-blacklisted videos'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'video-blacklist/list',
|
||||
component: VideoBlacklistListComponent,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './video-auto-blacklist-list.component'
|
|
@ -0,0 +1,49 @@
|
|||
<div i18n *ngIf="pagination.totalItems === 0">No results.</div>
|
||||
<div
|
||||
myInfiniteScroller
|
||||
[pageHeight]="pageHeight"
|
||||
(nearOfTop)="onNearOfTop()"
|
||||
(nearOfBottom)="onNearOfBottom()"
|
||||
(pageChanged)="onPageChanged($event)"
|
||||
class="videos" #videosElement
|
||||
>
|
||||
<div *ngFor="let videos of videoPages; let i = index" class="videos-page">
|
||||
<div class="video" *ngFor="let video of videos; let j = index">
|
||||
<div class="checkbox-container">
|
||||
<my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
|
||||
</div>
|
||||
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
||||
|
||||
<div class="video-info">
|
||||
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
||||
<div>{{ video.account.displayName }}</div>
|
||||
<div>{{ video.publishedAt | myFromNow }}</div>
|
||||
<div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
|
||||
<div><span i18n>Sensitve: </span><span> {{ video.nsfw }}</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Display only once -->
|
||||
<div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
|
||||
<div class="action-selection-mode-child">
|
||||
<span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
|
||||
Cancel
|
||||
</span>
|
||||
|
||||
<span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()">
|
||||
<my-global-icon iconName="tick"></my-global-icon>
|
||||
<ng-container i18n>Unblacklist</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-buttons" *ngIf="isInSelectionMode() === false">
|
||||
<my-button
|
||||
i18n-label
|
||||
label="Unblacklist"
|
||||
icon="tick"
|
||||
(click)="removeVideoFromBlacklist(video)"
|
||||
></my-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,94 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.action-selection-mode {
|
||||
width: 194px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.action-selection-mode-child {
|
||||
position: fixed;
|
||||
|
||||
.action-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.action-button-cancel-selection {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.action-button-unblacklist-selection {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
@include button-with-icon(21px);
|
||||
|
||||
my-global-icon {
|
||||
@include apply-svg-color(#fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
@include row-blocks;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 47px;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
my-video-thumbnail {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
flex-grow: 1;
|
||||
|
||||
.video-info-name {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
color: var(--mainForegroundColor);
|
||||
display: block;
|
||||
width: fit-content;
|
||||
font-size: 16px;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.video-buttons {
|
||||
min-width: 190px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
.video {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
|
||||
.video-info-name {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
my-video-thumbnail {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.video-buttons {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { Location } from '@angular/common'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Router, ActivatedRoute } from '@angular/router'
|
||||
import { AbstractVideoList } from '@app/shared/video/abstract-video-list'
|
||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||
import { Notifier, AuthService } from '@app/core'
|
||||
import { Video } from '@shared/models'
|
||||
import { VideoBlacklistService } from '@app/shared'
|
||||
import { immutableAssign } from '@app/shared/misc/utils'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-auto-blacklist-list',
|
||||
templateUrl: './video-auto-blacklist-list.component.html',
|
||||
styleUrls: [ './video-auto-blacklist-list.component.scss' ]
|
||||
})
|
||||
export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||
titlePage: string
|
||||
currentRoute = '/admin/moderation/video-auto-blacklist/list'
|
||||
checkedVideos: { [ id: number ]: boolean } = {}
|
||||
pagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 5,
|
||||
totalItems: null
|
||||
}
|
||||
|
||||
protected baseVideoWidth = -1
|
||||
protected baseVideoHeight = 155
|
||||
|
||||
constructor (
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
protected i18n: I18n,
|
||||
protected notifier: Notifier,
|
||||
protected location: Location,
|
||||
protected authService: AuthService,
|
||||
protected screenService: ScreenService,
|
||||
private videoBlacklistService: VideoBlacklistService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.titlePage = this.i18n('Auto-blacklisted videos')
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
super.ngOnInit()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
|
||||
abortSelectionMode () {
|
||||
this.checkedVideos = {}
|
||||
}
|
||||
|
||||
isInSelectionMode () {
|
||||
return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
|
||||
}
|
||||
|
||||
getVideosObservable (page: number) {
|
||||
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||
|
||||
return this.videoBlacklistService.getAutoBlacklistedAsVideoList(newPagination)
|
||||
}
|
||||
|
||||
generateSyndicationList () {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
removeVideoFromBlacklist (entry: Video) {
|
||||
this.videoBlacklistService.removeVideoFromBlacklist(entry.id).subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('Video {{name}} removed from blacklist.', { name: entry.name }))
|
||||
this.reloadVideos()
|
||||
},
|
||||
|
||||
error => this.notifier.error(error.message)
|
||||
)
|
||||
}
|
||||
|
||||
removeSelectedVideosFromBlacklist () {
|
||||
const toReleaseVideosIds = Object.keys(this.checkedVideos)
|
||||
.filter(k => this.checkedVideos[ k ] === true)
|
||||
.map(k => parseInt(k, 10))
|
||||
|
||||
this.videoBlacklistService.removeVideoFromBlacklist(toReleaseVideosIds).subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('{{num}} videos removed from blacklist.', { num: toReleaseVideosIds.length }))
|
||||
|
||||
this.abortSelectionMode()
|
||||
this.reloadVideos()
|
||||
},
|
||||
|
||||
error => this.notifier.error(error.message)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { Notifier } from '@app/core'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { ConfirmService } from '../../../core'
|
||||
import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
|
||||
import { VideoBlacklist } from '../../../../../../shared'
|
||||
import { VideoBlacklist, VideoBlacklistType } from '../../../../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
|
||||
import { Video } from '../../../shared/video/video.model'
|
||||
|
@ -20,11 +20,13 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
rowsPerPage = 10
|
||||
sort: SortMeta = { field: 'createdAt', order: 1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
listBlacklistTypeFilter: VideoBlacklistType = undefined
|
||||
|
||||
videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
|
||||
|
||||
constructor (
|
||||
private notifier: Notifier,
|
||||
private serverService: ServerService,
|
||||
private confirmService: ConfirmService,
|
||||
private videoBlacklistService: VideoBlacklistService,
|
||||
private markdownRenderer: MarkdownService,
|
||||
|
@ -32,6 +34,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
) {
|
||||
super()
|
||||
|
||||
// don't filter if auto-blacklist not enabled as this will be only list
|
||||
if (this.serverService.getConfig().autoBlacklist.videos.ofUsers.enabled) {
|
||||
this.listBlacklistTypeFilter = VideoBlacklistType.MANUAL
|
||||
}
|
||||
|
||||
this.videoBlacklistActions = [
|
||||
{
|
||||
label: this.i18n('Unblacklist'),
|
||||
|
@ -77,7 +84,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
protected loadData () {
|
||||
this.videoBlacklistService.listBlacklist(this.pagination, this.sort)
|
||||
this.videoBlacklistService.listBlacklist(this.pagination, this.sort, this.listBlacklistTypeFilter)
|
||||
.subscribe(
|
||||
async resultList => {
|
||||
this.totalRecords = resultList.total
|
||||
|
|
|
@ -31,10 +31,12 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
|
|||
private serverService: ServerService,
|
||||
private notifier: Notifier
|
||||
) {
|
||||
|
||||
this.labelNotifications = {
|
||||
newVideoFromSubscription: this.i18n('New video from your subscriptions'),
|
||||
newCommentOnMyVideo: this.i18n('New comment on your video'),
|
||||
videoAbuseAsModerator: this.i18n('New video abuse'),
|
||||
videoAutoBlacklistAsModerator: this.i18n('Video auto-blacklisted waiting review'),
|
||||
blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'),
|
||||
myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
|
||||
myVideoImportFinished: this.i18n('Video import finished'),
|
||||
|
@ -46,6 +48,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
|
|||
|
||||
this.rightNotifications = {
|
||||
videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
|
||||
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
|
||||
newUserRegistration: UserRight.MANAGE_USERS
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,6 +98,13 @@ export class ServerService {
|
|||
videos: {
|
||||
intervalDays: 0
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private videoCategories: Array<VideoConstant<number>> = []
|
||||
|
|
|
@ -54,6 +54,7 @@ export class UserNotification implements UserNotificationServer {
|
|||
videoUrl?: string
|
||||
commentUrl?: any[]
|
||||
videoAbuseUrl?: string
|
||||
videoAutoBlacklistUrl?: string
|
||||
accountUrl?: string
|
||||
videoImportIdentifier?: string
|
||||
videoImportUrl?: string
|
||||
|
@ -107,6 +108,11 @@ export class UserNotification implements UserNotificationServer {
|
|||
this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
|
||||
break
|
||||
|
||||
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
|
||||
this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
|
||||
this.videoUrl = this.buildVideoUrl(this.video)
|
||||
break
|
||||
|
||||
case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
|
||||
this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
|
||||
break
|
||||
|
|
|
@ -36,6 +36,14 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS">
|
||||
<my-global-icon iconName="no"></my-global-icon>
|
||||
|
||||
<div class="message">
|
||||
The recently added video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been <a (click)="markAsRead(notification)" [routerLink]="notification.videoAutoBlacklistUrl">auto-blacklisted</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
|
||||
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { catchError, map } from 'rxjs/operators'
|
||||
import { catchError, map, concatMap, toArray } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { Observable } from 'rxjs'
|
||||
import { VideoBlacklist, ResultList } from '../../../../../shared'
|
||||
import { from as observableFrom, Observable } from 'rxjs'
|
||||
import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared'
|
||||
import { Video } from '../video/video.model'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { RestExtractor, RestPagination, RestService } from '../rest'
|
||||
import { ComponentPagination } from '../rest/component-pagination.model'
|
||||
|
||||
@Injectable()
|
||||
export class VideoBlacklistService {
|
||||
|
@ -17,10 +19,14 @@ export class VideoBlacklistService {
|
|||
private restExtractor: RestExtractor
|
||||
) {}
|
||||
|
||||
listBlacklist (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoBlacklist>> {
|
||||
listBlacklist (pagination: RestPagination, sort: SortMeta, type?: VideoBlacklistType): Observable<ResultList<VideoBlacklist>> {
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
if (type) {
|
||||
params = params.set('type', type.toString())
|
||||
}
|
||||
|
||||
return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
|
@ -28,12 +34,37 @@ export class VideoBlacklistService {
|
|||
)
|
||||
}
|
||||
|
||||
removeVideoFromBlacklist (videoId: number) {
|
||||
return this.authHttp.delete(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist')
|
||||
.pipe(
|
||||
map(this.restExtractor.extractDataBool),
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
getAutoBlacklistedAsVideoList (videoPagination: ComponentPagination): Observable<{ videos: Video[], totalVideos: number}> {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
|
||||
// prioritize first created since waiting longest
|
||||
const AUTO_BLACKLIST_SORT = 'createdAt'
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, AUTO_BLACKLIST_SORT)
|
||||
|
||||
params = params.set('type', VideoBlacklistType.AUTO_BEFORE_PUBLISHED.toString())
|
||||
|
||||
return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
|
||||
.pipe(
|
||||
map(res => {
|
||||
const videos = res.data.map(videoBlacklist => new Video(videoBlacklist.video))
|
||||
const totalVideos = res.total
|
||||
return { videos, totalVideos }
|
||||
}),
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
}
|
||||
|
||||
removeVideoFromBlacklist (videoIdArgs: number | number[]) {
|
||||
const videoIds = Array.isArray(videoIdArgs) ? videoIdArgs : [ videoIdArgs ]
|
||||
|
||||
return observableFrom(videoIds)
|
||||
.pipe(
|
||||
concatMap(id => this.authHttp.delete(VideoBlacklistService.BASE_VIDEOS_URL + id + '/blacklist')),
|
||||
toArray(),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
blacklistVideo (videoId: number, reason: string, unfederate: boolean) {
|
||||
|
|
|
@ -162,6 +162,12 @@ import:
|
|||
torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
|
||||
enabled: false
|
||||
|
||||
auto_blacklist:
|
||||
# New videos automatically blacklisted so moderators can review before publishing
|
||||
videos:
|
||||
of_users:
|
||||
enabled: false
|
||||
|
||||
instance:
|
||||
name: 'PeerTube'
|
||||
short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'
|
||||
|
|
|
@ -176,6 +176,12 @@ import:
|
|||
torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
|
||||
enabled: false
|
||||
|
||||
auto_blacklist:
|
||||
# New videos automatically blacklisted so moderators can review before publishing
|
||||
videos:
|
||||
of_users:
|
||||
enabled: false
|
||||
|
||||
# Instance settings
|
||||
instance:
|
||||
name: 'PeerTube'
|
||||
|
|
|
@ -94,6 +94,13 @@ async function getConfig (req: express.Request, res: express.Response) {
|
|||
}
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
|
||||
}
|
||||
}
|
||||
},
|
||||
avatar: {
|
||||
file: {
|
||||
size: {
|
||||
|
@ -265,6 +272,13 @@ function customConfig (): CustomConfig {
|
|||
enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
|
||||
}
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
|
|||
newVideoFromSubscription: body.newVideoFromSubscription,
|
||||
newCommentOnMyVideo: body.newCommentOnMyVideo,
|
||||
videoAbuseAsModerator: body.videoAbuseAsModerator,
|
||||
videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
|
||||
blacklistOnMyVideo: body.blacklistOnMyVideo,
|
||||
myVideoPublished: body.myVideoPublished,
|
||||
myVideoImportFinished: body.myVideoImportFinished,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as express from 'express'
|
||||
import { UserRight, VideoBlacklist, VideoBlacklistCreate } from '../../../../shared'
|
||||
import { VideoBlacklist, UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { getFormattedObjects } from '../../../helpers/utils'
|
||||
import {
|
||||
|
@ -12,7 +12,8 @@ import {
|
|||
setDefaultPagination,
|
||||
videosBlacklistAddValidator,
|
||||
videosBlacklistRemoveValidator,
|
||||
videosBlacklistUpdateValidator
|
||||
videosBlacklistUpdateValidator,
|
||||
videosBlacklistFiltersValidator
|
||||
} from '../../../middlewares'
|
||||
import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
|
||||
import { sequelizeTypescript } from '../../../initializers'
|
||||
|
@ -36,6 +37,7 @@ blacklistRouter.get('/blacklist',
|
|||
blacklistSortValidator,
|
||||
setBlacklistSort,
|
||||
setDefaultPagination,
|
||||
videosBlacklistFiltersValidator,
|
||||
asyncMiddleware(listBlacklist)
|
||||
)
|
||||
|
||||
|
@ -68,7 +70,8 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
|
|||
const toCreate = {
|
||||
videoId: videoInstance.id,
|
||||
unfederated: body.unfederate === true,
|
||||
reason: body.reason
|
||||
reason: body.reason,
|
||||
type: VideoBlacklistType.MANUAL
|
||||
}
|
||||
|
||||
const blacklist = await VideoBlacklistModel.create(toCreate)
|
||||
|
@ -98,7 +101,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
|
|||
}
|
||||
|
||||
async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort)
|
||||
const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.type)
|
||||
|
||||
return res.json(getFormattedObjects<VideoBlacklist, VideoBlacklistModel>(resultList.data, resultList.total))
|
||||
}
|
||||
|
@ -107,18 +110,30 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
|
|||
const videoBlacklist = res.locals.videoBlacklist
|
||||
const video = res.locals.video
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
|
||||
const unfederated = videoBlacklist.unfederated
|
||||
const videoBlacklistType = videoBlacklist.type
|
||||
|
||||
await videoBlacklist.destroy({ transaction: t })
|
||||
|
||||
// Re federate the video
|
||||
if (unfederated === true) {
|
||||
await federateVideoIfNeeded(video, true, t)
|
||||
}
|
||||
|
||||
return videoBlacklistType
|
||||
})
|
||||
|
||||
Notifier.Instance.notifyOnVideoUnblacklist(video)
|
||||
|
||||
if (videoBlacklistType === VideoBlacklistType.AUTO_BEFORE_PUBLISHED) {
|
||||
Notifier.Instance.notifyOnVideoPublishedAfterRemovedFromAutoBlacklist(video)
|
||||
|
||||
// Delete on object so new video notifications will send
|
||||
delete video.VideoBlacklist
|
||||
Notifier.Instance.notifyOnNewVideo(video)
|
||||
}
|
||||
|
||||
logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
|
||||
|
||||
return res.type('json').status(204).end()
|
||||
|
|
|
@ -18,10 +18,12 @@ import { join } from 'path'
|
|||
import { isArray } from '../../../helpers/custom-validators/misc'
|
||||
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
||||
import { VideoChannelModel } from '../../../models/video/video-channel'
|
||||
import { UserModel } from '../../../models/account/user'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import * as parseTorrent from 'parse-torrent'
|
||||
import { getSecureTorrentName } from '../../../helpers/utils'
|
||||
import { readFile, move } from 'fs-extra'
|
||||
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
|
||||
|
||||
const auditLogger = auditLoggerFactory('video-imports')
|
||||
const videoImportsRouter = express.Router()
|
||||
|
@ -85,7 +87,7 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
|
|||
videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string
|
||||
}
|
||||
|
||||
const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
|
||||
const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }, user)
|
||||
|
||||
await processThumbnail(req, video)
|
||||
await processPreview(req, video)
|
||||
|
@ -128,7 +130,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
|||
}).end()
|
||||
}
|
||||
|
||||
const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
|
||||
const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo, user)
|
||||
|
||||
const downloadThumbnail = !await processThumbnail(req, video)
|
||||
const downloadPreview = !await processPreview(req, video)
|
||||
|
@ -156,7 +158,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
|||
return res.json(videoImport.toFormattedJSON()).end()
|
||||
}
|
||||
|
||||
function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo) {
|
||||
function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo, user: UserModel) {
|
||||
const videoData = {
|
||||
name: body.name || importData.name || 'Unknown name',
|
||||
remote: false,
|
||||
|
@ -218,6 +220,8 @@ function insertIntoDB (
|
|||
const videoCreated = await video.save(sequelizeOptions)
|
||||
videoCreated.VideoChannel = videoChannel
|
||||
|
||||
await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
|
||||
|
||||
// Set tags to the video
|
||||
if (tags) {
|
||||
const tagInstances = await TagModel.findOrCreateTags(tags, t)
|
||||
|
|
|
@ -6,6 +6,7 @@ import { processImage } from '../../../helpers/image-utils'
|
|||
import { logger } from '../../../helpers/logger'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
|
||||
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
|
||||
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
|
||||
import {
|
||||
CONFIG,
|
||||
MIMETYPES,
|
||||
|
@ -193,6 +194,7 @@ async function addVideo (req: express.Request, res: express.Response) {
|
|||
channelId: res.locals.videoChannel.id,
|
||||
originallyPublishedAt: videoInfo.originallyPublishedAt
|
||||
}
|
||||
|
||||
const video = new VideoModel(videoData)
|
||||
video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
|
||||
|
||||
|
@ -237,7 +239,7 @@ async function addVideo (req: express.Request, res: express.Response) {
|
|||
// Create the torrent file
|
||||
await video.createTorrentAndSetInfoHash(videoFile)
|
||||
|
||||
const videoCreated = await sequelizeTypescript.transaction(async t => {
|
||||
const { videoCreated, videoWasAutoBlacklisted } = await sequelizeTypescript.transaction(async t => {
|
||||
const sequelizeOptions = { transaction: t }
|
||||
|
||||
const videoCreated = await video.save(sequelizeOptions)
|
||||
|
@ -266,15 +268,23 @@ async function addVideo (req: express.Request, res: express.Response) {
|
|||
}, { transaction: t })
|
||||
}
|
||||
|
||||
await federateVideoIfNeeded(video, true, t)
|
||||
const videoWasAutoBlacklisted = await autoBlacklistVideoIfNeeded(video, res.locals.oauth.token.User, t)
|
||||
|
||||
if (!videoWasAutoBlacklisted) {
|
||||
await federateVideoIfNeeded(video, true, t)
|
||||
}
|
||||
|
||||
auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
|
||||
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
|
||||
|
||||
return videoCreated
|
||||
return { videoCreated, videoWasAutoBlacklisted }
|
||||
})
|
||||
|
||||
Notifier.Instance.notifyOnNewVideo(videoCreated)
|
||||
if (videoWasAutoBlacklisted) {
|
||||
Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
|
||||
} else {
|
||||
Notifier.Instance.notifyOnNewVideo(videoCreated)
|
||||
}
|
||||
|
||||
if (video.state === VideoState.TO_TRANSCODE) {
|
||||
// Put uuid because we don't have id auto incremented for now
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Response } from 'express'
|
||||
import * as validator from 'validator'
|
||||
import { exists } from './misc'
|
||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||
import { VideoBlacklistModel } from '../../models/video/video-blacklist'
|
||||
import { VideoBlacklistType } from '../../../shared/models/videos'
|
||||
|
||||
const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
|
||||
|
||||
|
@ -24,9 +26,14 @@ async function doesVideoBlacklistExist (videoId: number, res: Response) {
|
|||
return true
|
||||
}
|
||||
|
||||
function isVideoBlacklistTypeValid (value: any) {
|
||||
return exists(value) && validator.isInt('' + value) && VideoBlacklistType[value] !== undefined
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isVideoBlacklistReasonValid,
|
||||
isVideoBlacklistTypeValid,
|
||||
doesVideoBlacklistExist
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { CONFIG } from '../initializers'
|
||||
import { VideoModel } from '../models/video/video'
|
||||
import { UserRight } from '../../shared'
|
||||
import { UserModel } from '../models/account/user'
|
||||
|
||||
type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ function checkMissedConfig () {
|
|||
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
|
||||
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
|
||||
'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions',
|
||||
'import.videos.http.enabled', 'import.videos.torrent.enabled',
|
||||
'import.videos.http.enabled', 'import.videos.torrent.enabled', 'auto_blacklist.videos.of_users.enabled',
|
||||
'trending.videos.interval_days',
|
||||
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
|
||||
'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
|
||||
|
|
|
@ -18,7 +18,7 @@ let config: IConfig = require('config')
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 345
|
||||
const LAST_MIGRATION_VERSION = 350
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -288,6 +288,13 @@ const CONFIG = {
|
|||
}
|
||||
}
|
||||
},
|
||||
AUTO_BLACKLIST: {
|
||||
VIDEOS: {
|
||||
OF_USERS: {
|
||||
get ENABLED () { return config.get<boolean>('auto_blacklist.videos.of_users.enabled') }
|
||||
}
|
||||
}
|
||||
},
|
||||
CACHE: {
|
||||
PREVIEWS: {
|
||||
get SIZE () { return config.get<number>('cache.previews.size') }
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import * as Sequelize from 'sequelize'
|
||||
import { VideoBlacklistType } from '../../../shared/models/videos'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction,
|
||||
queryInterface: Sequelize.QueryInterface,
|
||||
sequelize: Sequelize.Sequelize,
|
||||
db: any
|
||||
}): Promise<void> {
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
}
|
||||
|
||||
await utils.queryInterface.addColumn('videoBlacklist', 'type', data)
|
||||
}
|
||||
|
||||
{
|
||||
const query = 'UPDATE "videoBlacklist" SET "type" = ' + VideoBlacklistType.MANUAL
|
||||
await utils.sequelize.query(query)
|
||||
}
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: null
|
||||
}
|
||||
await utils.queryInterface.changeColumn('videoBlacklist', 'type', data)
|
||||
}
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true
|
||||
}
|
||||
await utils.queryInterface.addColumn('userNotificationSetting', 'videoAutoBlacklistAsModerator', data)
|
||||
}
|
||||
|
||||
{
|
||||
const query = 'UPDATE "userNotificationSetting" SET "videoAutoBlacklistAsModerator" = 3'
|
||||
await utils.sequelize.query(query)
|
||||
}
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: false
|
||||
}
|
||||
await utils.queryInterface.changeColumn('userNotificationSetting', 'videoAutoBlacklistAsModerator', data)
|
||||
}
|
||||
}
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
|
@ -45,7 +45,7 @@ import { VideoShareModel } from '../../models/video/video-share'
|
|||
import { VideoCommentModel } from '../../models/video/video-comment'
|
||||
|
||||
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
|
||||
// If the video is not private and published, we federate it
|
||||
// If the video is not private and is published, we federate it
|
||||
if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
|
||||
// Fetch more attributes that we will need to serialize in AP object
|
||||
if (isArray(video.VideoCaptions) === false) {
|
||||
|
|
|
@ -250,6 +250,29 @@ class Emailer {
|
|||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
addVideoAutoBlacklistModeratorsNotification (to: string[], video: VideoModel) {
|
||||
const VIDEO_AUTO_BLACKLIST_URL = CONFIG.WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
|
||||
const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
|
||||
|
||||
const text = `Hi,\n\n` +
|
||||
`A recently added video was auto-blacklisted and requires moderator review before publishing.` +
|
||||
`\n\n` +
|
||||
`You can view it and take appropriate action on ${videoUrl}` +
|
||||
`\n\n` +
|
||||
`A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
|
||||
`\n\n` +
|
||||
`Cheers,\n` +
|
||||
`PeerTube.`
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
to,
|
||||
subject: '[PeerTube] An auto-blacklisted video is awaiting review',
|
||||
text
|
||||
}
|
||||
|
||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||
}
|
||||
|
||||
addNewUserRegistrationNotification (to: string[], user: UserModel) {
|
||||
const text = `Hi,\n\n` +
|
||||
`User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` +
|
||||
|
|
|
@ -196,9 +196,14 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
|
|||
return videoImportUpdated
|
||||
})
|
||||
|
||||
Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
|
||||
Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
|
||||
|
||||
if (videoImportUpdated.Video.VideoBlacklist) {
|
||||
Notifier.Instance.notifyOnVideoAutoBlacklist(videoImportUpdated.Video)
|
||||
} else {
|
||||
Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
|
||||
}
|
||||
|
||||
// Create transcoding jobs?
|
||||
if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
|
||||
// Put uuid because we don't have id auto incremented for now
|
||||
|
|
|
@ -85,10 +85,9 @@ async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodi
|
|||
return { videoDatabase, videoPublished }
|
||||
})
|
||||
|
||||
// don't notify prior to scheduled video update
|
||||
if (videoPublished && !videoDatabase.ScheduleVideoUpdate) {
|
||||
if (videoPublished) {
|
||||
Notifier.Instance.notifyOnNewVideo(videoDatabase)
|
||||
Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase)
|
||||
Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
|
||||
}
|
||||
|
||||
await createHlsJobIfEnabled(payload)
|
||||
|
@ -146,11 +145,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video
|
|||
return { videoDatabase, videoPublished }
|
||||
})
|
||||
|
||||
// don't notify prior to scheduled video update
|
||||
if (!videoDatabase.ScheduleVideoUpdate) {
|
||||
if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
|
||||
if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase)
|
||||
}
|
||||
if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
|
||||
if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
|
||||
|
||||
await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }))
|
||||
}
|
||||
|
|
|
@ -23,19 +23,35 @@ class Notifier {
|
|||
private constructor () {}
|
||||
|
||||
notifyOnNewVideo (video: VideoModel): void {
|
||||
// Only notify on public and published videos
|
||||
if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED) return
|
||||
// Only notify on public and published videos which are not blacklisted
|
||||
if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.VideoBlacklist) return
|
||||
|
||||
this.notifySubscribersOfNewVideo(video)
|
||||
.catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
|
||||
}
|
||||
|
||||
notifyOnPendingVideoPublished (video: VideoModel): void {
|
||||
// Only notify on public videos that has been published while the user waited transcoding/scheduled update
|
||||
if (video.waitTranscoding === false && !video.ScheduleVideoUpdate) return
|
||||
notifyOnVideoPublishedAfterTranscoding (video: VideoModel): void {
|
||||
// don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
|
||||
if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
|
||||
|
||||
this.notifyOwnedVideoHasBeenPublished(video)
|
||||
.catch(err => logger.error('Cannot notify owner that its video %s has been published.', video.url, { err }))
|
||||
.catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
|
||||
}
|
||||
|
||||
notifyOnVideoPublishedAfterScheduledUpdate (video: VideoModel): void {
|
||||
// don't notify if video is still blacklisted or waiting for transcoding
|
||||
if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
|
||||
|
||||
this.notifyOwnedVideoHasBeenPublished(video)
|
||||
.catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
|
||||
}
|
||||
|
||||
notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: VideoModel): void {
|
||||
// don't notify if video is still waiting for transcoding or scheduled update
|
||||
if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
|
||||
|
||||
this.notifyOwnedVideoHasBeenPublished(video)
|
||||
.catch(err => logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })) // tslint:disable-line:max-line-length
|
||||
}
|
||||
|
||||
notifyOnNewComment (comment: VideoCommentModel): void {
|
||||
|
@ -51,6 +67,11 @@ class Notifier {
|
|||
.catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
|
||||
}
|
||||
|
||||
notifyOnVideoAutoBlacklist (video: VideoModel): void {
|
||||
this.notifyModeratorsOfVideoAutoBlacklist(video)
|
||||
.catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err }))
|
||||
}
|
||||
|
||||
notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void {
|
||||
this.notifyVideoOwnerOfBlacklist(videoBlacklist)
|
||||
.catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
|
||||
|
@ -58,7 +79,7 @@ class Notifier {
|
|||
|
||||
notifyOnVideoUnblacklist (video: VideoModel): void {
|
||||
this.notifyVideoOwnerOfUnblacklist(video)
|
||||
.catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err }))
|
||||
.catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
|
||||
}
|
||||
|
||||
notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void {
|
||||
|
@ -268,6 +289,34 @@ class Notifier {
|
|||
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
|
||||
}
|
||||
|
||||
private async notifyModeratorsOfVideoAutoBlacklist (video: VideoModel) {
|
||||
const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
|
||||
if (moderators.length === 0) return
|
||||
|
||||
logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url)
|
||||
|
||||
function settingGetter (user: UserModel) {
|
||||
return user.NotificationSetting.videoAutoBlacklistAsModerator
|
||||
}
|
||||
async function notificationCreator (user: UserModel) {
|
||||
|
||||
const notification = await UserNotificationModel.create({
|
||||
type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
|
||||
userId: user.id,
|
||||
videoId: video.id
|
||||
})
|
||||
notification.Video = video
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
function emailSender (emails: string[]) {
|
||||
return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video)
|
||||
}
|
||||
|
||||
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
|
||||
}
|
||||
|
||||
private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) {
|
||||
const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
|
||||
if (!user) return
|
||||
|
|
|
@ -57,7 +57,7 @@ export class UpdateVideosScheduler extends AbstractScheduler {
|
|||
|
||||
for (const v of publishedVideos) {
|
||||
Notifier.Instance.notifyOnNewVideo(v)
|
||||
Notifier.Instance.notifyOnPendingVideoPublished(v)
|
||||
Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(v)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr
|
|||
myVideoImportFinished: UserNotificationSettingValue.WEB,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB,
|
||||
commentMention: UserNotificationSettingValue.WEB,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import * as sequelize from 'sequelize'
|
||||
import { CONFIG } from '../initializers/constants'
|
||||
import { VideoBlacklistType, UserRight } from '../../shared/models'
|
||||
import { VideoBlacklistModel } from '../models/video/video-blacklist'
|
||||
import { UserModel } from '../models/account/user'
|
||||
import { VideoModel } from '../models/video/video'
|
||||
import { logger } from '../helpers/logger'
|
||||
|
||||
async function autoBlacklistVideoIfNeeded (video: VideoModel, user: UserModel, transaction: sequelize.Transaction) {
|
||||
if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED) return false
|
||||
|
||||
if (user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return false
|
||||
|
||||
const sequelizeOptions = { transaction }
|
||||
const videoBlacklistToCreate = {
|
||||
videoId: video.id,
|
||||
unfederated: true,
|
||||
reason: 'Auto-blacklisted. Moderator review required.',
|
||||
type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
|
||||
}
|
||||
await VideoBlacklistModel.create(videoBlacklistToCreate, sequelizeOptions)
|
||||
logger.info('Video %s auto-blacklisted.', video.uuid)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
autoBlacklistVideoIfNeeded
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
import * as express from 'express'
|
||||
import { body, param } from 'express-validator/check'
|
||||
import { body, param, query } from 'express-validator/check'
|
||||
import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
|
||||
import { doesVideoExist } from '../../../helpers/custom-validators/videos'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { areValidationErrors } from '../utils'
|
||||
import { doesVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist'
|
||||
import {
|
||||
doesVideoBlacklistExist,
|
||||
isVideoBlacklistReasonValid,
|
||||
isVideoBlacklistTypeValid
|
||||
} from '../../../helpers/custom-validators/video-blacklist'
|
||||
|
||||
const videosBlacklistRemoveValidator = [
|
||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
||||
|
@ -65,10 +69,25 @@ const videosBlacklistUpdateValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
const videosBlacklistFiltersValidator = [
|
||||
query('type')
|
||||
.optional()
|
||||
.custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking videos blacklist filters query', { parameters: req.query })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videosBlacklistAddValidator,
|
||||
videosBlacklistRemoveValidator,
|
||||
videosBlacklistUpdateValidator
|
||||
videosBlacklistUpdateValidator,
|
||||
videosBlacklistFiltersValidator
|
||||
}
|
||||
|
|
|
@ -56,6 +56,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
|
|||
@Column
|
||||
videoAbuseAsModerator: UserNotificationSettingValue
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingVideoAutoBlacklistAsModerator',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator')
|
||||
)
|
||||
@Column
|
||||
videoAutoBlacklistAsModerator: UserNotificationSettingValue
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
|
@ -139,6 +148,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
|
|||
newCommentOnMyVideo: this.newCommentOnMyVideo,
|
||||
newVideoFromSubscription: this.newVideoFromSubscription,
|
||||
videoAbuseAsModerator: this.videoAbuseAsModerator,
|
||||
videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
|
||||
blacklistOnMyVideo: this.blacklistOnMyVideo,
|
||||
myVideoPublished: this.myVideoPublished,
|
||||
myVideoImportFinished: this.myVideoImportFinished,
|
||||
|
|
|
@ -72,7 +72,8 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
|
|||
model: VideoModel.scope(
|
||||
[
|
||||
VideoScopeNames.WITH_FILES,
|
||||
VideoScopeNames.WITH_ACCOUNT_DETAILS
|
||||
VideoScopeNames.WITH_ACCOUNT_DETAILS,
|
||||
VideoScopeNames.WITH_BLACKLISTED
|
||||
]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import {
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
Default,
|
||||
ForeignKey,
|
||||
Is, Model,
|
||||
Table,
|
||||
UpdatedAt,
|
||||
IFindOptions
|
||||
} from 'sequelize-typescript'
|
||||
import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
|
||||
import { VideoModel } from './video'
|
||||
import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
|
||||
import { VideoBlacklist } from '../../../shared/models/videos'
|
||||
import { VideoChannelModel, ScopeNames as VideoChannelScopeNames } from './video-channel'
|
||||
import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
|
||||
import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
|
||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||
|
||||
@Table({
|
||||
|
@ -25,6 +38,12 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
|
|||
@Column
|
||||
unfederated: boolean
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is('VideoBlacklistType', value => throwIfNotValid(value, isVideoBlacklistTypeValid, 'type'))
|
||||
@Column
|
||||
type: VideoBlacklistType
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
|
@ -43,19 +62,29 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
|
|||
})
|
||||
Video: VideoModel
|
||||
|
||||
static listForApi (start: number, count: number, sort: SortType) {
|
||||
const query = {
|
||||
static listForApi (start: number, count: number, sort: SortType, type?: VideoBlacklistType) {
|
||||
const query: IFindOptions<VideoBlacklistModel> = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSortOnModel(sort.sortModel, sort.sortValue),
|
||||
include: [
|
||||
{
|
||||
model: VideoModel,
|
||||
required: true
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (type) {
|
||||
query.where = { type }
|
||||
}
|
||||
|
||||
return VideoBlacklistModel.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return {
|
||||
|
@ -76,26 +105,15 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
|
|||
}
|
||||
|
||||
toFormattedJSON (): VideoBlacklist {
|
||||
const video = this.Video
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
reason: this.reason,
|
||||
unfederated: this.unfederated,
|
||||
type: this.type,
|
||||
|
||||
video: {
|
||||
id: video.id,
|
||||
name: video.name,
|
||||
uuid: video.uuid,
|
||||
description: video.description,
|
||||
duration: video.duration,
|
||||
views: video.views,
|
||||
likes: video.likes,
|
||||
dislikes: video.dislikes,
|
||||
nsfw: video.nsfw
|
||||
}
|
||||
video: this.Video.toFormattedJSON()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,13 @@ describe('Test config API validators', function () {
|
|||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -168,6 +168,7 @@ describe('Test user notifications API validators', function () {
|
|||
newVideoFromSubscription: UserNotificationSettingValue.WEB,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB,
|
||||
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB,
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
flushAndRunMultipleServers,
|
||||
flushTests,
|
||||
getBlacklistedVideosList,
|
||||
getBlacklistedVideosListWithTypeFilter,
|
||||
getVideo,
|
||||
getVideoWithToken,
|
||||
killallServers,
|
||||
|
@ -24,7 +25,7 @@ import {
|
|||
checkBadSortPagination,
|
||||
checkBadStartPagination
|
||||
} from '../../../../shared/utils/requests/check-api-params'
|
||||
import { VideoDetails } from '../../../../shared/models/videos'
|
||||
import { VideoDetails, VideoBlacklistType } from '../../../../shared/models/videos'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test video blacklist API validators', function () {
|
||||
|
@ -238,6 +239,14 @@ describe('Test video blacklist API validators', function () {
|
|||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid type', async function () {
|
||||
await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, 0, 400)
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.MANUAL)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
|
|
|
@ -7,7 +7,8 @@ import { join } from 'path'
|
|||
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
|
||||
import {
|
||||
createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest,
|
||||
makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin
|
||||
makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, uploadVideo,
|
||||
runServer, ServerInfo, setAccessTokensToServers, userLogin, updateCustomSubConfig
|
||||
} from '../../../../shared/utils'
|
||||
import {
|
||||
checkBadCountPagination,
|
||||
|
|
|
@ -62,6 +62,7 @@ function checkInitialConfig (data: CustomConfig) {
|
|||
|
||||
expect(data.import.videos.http.enabled).to.be.true
|
||||
expect(data.import.videos.torrent.enabled).to.be.true
|
||||
expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
|
||||
}
|
||||
|
||||
function checkUpdatedConfig (data: CustomConfig) {
|
||||
|
@ -103,6 +104,7 @@ function checkUpdatedConfig (data: CustomConfig) {
|
|||
|
||||
expect(data.import.videos.http.enabled).to.be.false
|
||||
expect(data.import.videos.torrent.enabled).to.be.false
|
||||
expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
|
||||
}
|
||||
|
||||
describe('Test config', function () {
|
||||
|
@ -225,6 +227,13 @@ describe('Test config', function () {
|
|||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
|
||||
|
|
|
@ -17,7 +17,9 @@ import {
|
|||
updateVideo,
|
||||
updateVideoChannel,
|
||||
userLogin,
|
||||
wait
|
||||
wait,
|
||||
getCustomConfig,
|
||||
updateCustomConfig
|
||||
} from '../../../../shared/utils'
|
||||
import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index'
|
||||
import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
|
||||
|
@ -31,6 +33,7 @@ import {
|
|||
checkNewBlacklistOnMyVideo,
|
||||
checkNewCommentOnMyVideo,
|
||||
checkNewVideoAbuseForModerators,
|
||||
checkVideoAutoBlacklistForModerators,
|
||||
checkNewVideoFromSubscription,
|
||||
checkUserRegistered,
|
||||
checkVideoIsPublished,
|
||||
|
@ -54,6 +57,7 @@ import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../sha
|
|||
import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
|
||||
import * as uuidv4 from 'uuid/v4'
|
||||
import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist'
|
||||
import { CustomConfig } from '../../../../shared/models/server'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
|
@ -92,6 +96,7 @@ describe('Test users notifications', function () {
|
|||
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
|
@ -305,7 +310,7 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
|
||||
it('Should send a new video notification after a video import', async function () {
|
||||
this.timeout(30000)
|
||||
this.timeout(100000)
|
||||
|
||||
const name = 'video import ' + uuidv4()
|
||||
|
||||
|
@ -907,6 +912,180 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Video-related notifications when video auto-blacklist is enabled', function () {
|
||||
let userBaseParams: CheckerBaseParams
|
||||
let adminBaseParamsServer1: CheckerBaseParams
|
||||
let adminBaseParamsServer2: CheckerBaseParams
|
||||
let videoUUID: string
|
||||
let videoName: string
|
||||
let currentCustomConfig: CustomConfig
|
||||
|
||||
before(async () => {
|
||||
|
||||
adminBaseParamsServer1 = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: adminNotifications,
|
||||
token: servers[0].accessToken
|
||||
}
|
||||
|
||||
adminBaseParamsServer2 = {
|
||||
server: servers[1],
|
||||
emails,
|
||||
socketNotifications: adminNotificationsServer2,
|
||||
token: servers[1].accessToken
|
||||
}
|
||||
|
||||
userBaseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userAccessToken
|
||||
}
|
||||
|
||||
const resCustomConfig = await getCustomConfig(servers[0].url, servers[0].accessToken)
|
||||
currentCustomConfig = resCustomConfig.body
|
||||
const autoBlacklistTestsCustomConfig = immutableAssign(currentCustomConfig, {
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// enable transcoding otherwise own publish notification after transcoding not expected
|
||||
autoBlacklistTestsCustomConfig.transcoding.enabled = true
|
||||
await updateCustomConfig(servers[0].url, servers[0].accessToken, autoBlacklistTestsCustomConfig)
|
||||
|
||||
await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
|
||||
await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
|
||||
|
||||
})
|
||||
|
||||
it('Should send notification to moderators on new video with auto-blacklist', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
videoName = 'video with auto-blacklist ' + uuidv4()
|
||||
const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName })
|
||||
videoUUID = resVideo.body.video.uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, videoUUID, videoName, 'presence')
|
||||
})
|
||||
|
||||
it('Should not send video publish notification if auto-blacklisted', async function () {
|
||||
await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'absence')
|
||||
})
|
||||
|
||||
it('Should not send a local user subscription notification if auto-blacklisted', async function () {
|
||||
await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'absence')
|
||||
})
|
||||
|
||||
it('Should not send a remote user subscription notification if auto-blacklisted', async function () {
|
||||
await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'absence')
|
||||
})
|
||||
|
||||
it('Should send video published and unblacklist after video unblacklisted', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoUUID)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
// FIXME: Can't test as two notifications sent to same user and util only checks last one
|
||||
// One notification might be better anyways
|
||||
// await checkNewBlacklistOnMyVideo(userBaseParams, videoUUID, videoName, 'unblacklist')
|
||||
// await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'presence')
|
||||
})
|
||||
|
||||
it('Should send a local user subscription notification after removed from blacklist', async function () {
|
||||
await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'presence')
|
||||
})
|
||||
|
||||
it('Should send a remote user subscription notification after removed from blacklist', async function () {
|
||||
await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'presence')
|
||||
})
|
||||
|
||||
it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
let updateAt = new Date(new Date().getTime() + 100000)
|
||||
|
||||
const name = 'video with auto-blacklist and future schedule ' + uuidv4()
|
||||
|
||||
const data = {
|
||||
name,
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
|
||||
const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
|
||||
const uuid = resVideo.body.video.uuid
|
||||
|
||||
await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid)
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewBlacklistOnMyVideo(userBaseParams, uuid, name, 'unblacklist')
|
||||
|
||||
// FIXME: Can't test absence as two notifications sent to same user and util only checks last one
|
||||
// One notification might be better anyways
|
||||
// await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
|
||||
|
||||
await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
|
||||
await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
|
||||
})
|
||||
|
||||
it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
// In 2 seconds
|
||||
let updateAt = new Date(new Date().getTime() + 2000)
|
||||
|
||||
const name = 'video with schedule done and still auto-blacklisted ' + uuidv4()
|
||||
|
||||
const data = {
|
||||
name,
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
|
||||
const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
|
||||
const uuid = resVideo.body.video.uuid
|
||||
|
||||
await wait(6000)
|
||||
await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
|
||||
await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
|
||||
await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
|
||||
})
|
||||
|
||||
it('Should not send a notification to moderators on new video without auto-blacklist', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
const name = 'video without auto-blacklist ' + uuidv4()
|
||||
|
||||
// admin with blacklist right will not be auto-blacklisted
|
||||
const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name })
|
||||
const uuid = resVideo.body.video.uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, uuid, name, 'absence')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await updateCustomConfig(servers[0].url, servers[0].accessToken, currentCustomConfig)
|
||||
|
||||
await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
|
||||
await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mark as read', function () {
|
||||
it('Should mark as read some notifications', async function () {
|
||||
const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3)
|
||||
|
@ -968,7 +1147,7 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
|
||||
it('Should not have notifications', async function () {
|
||||
this.timeout(10000)
|
||||
this.timeout(20000)
|
||||
|
||||
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.NONE
|
||||
|
@ -987,7 +1166,7 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
|
||||
it('Should only have web notifications', async function () {
|
||||
this.timeout(10000)
|
||||
this.timeout(20000)
|
||||
|
||||
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB
|
||||
|
@ -1013,7 +1192,7 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
|
||||
it('Should only have mail notifications', async function () {
|
||||
this.timeout(10000)
|
||||
this.timeout(20000)
|
||||
|
||||
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.EMAIL
|
||||
|
@ -1039,7 +1218,7 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
|
||||
it('Should have email and web notifications', async function () {
|
||||
this.timeout(10000)
|
||||
this.timeout(20000)
|
||||
|
||||
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
addVideoToBlacklist,
|
||||
flushAndRunMultipleServers,
|
||||
getBlacklistedVideosList,
|
||||
getBlacklistedVideosListWithTypeFilter,
|
||||
getMyVideos,
|
||||
getSortedBlacklistedVideosList,
|
||||
getVideosList,
|
||||
|
@ -22,7 +23,7 @@ import {
|
|||
} from '../../../../shared/utils/index'
|
||||
import { doubleFollow } from '../../../../shared/utils/server/follows'
|
||||
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
||||
import { VideoBlacklist } from '../../../../shared/models/videos'
|
||||
import { VideoBlacklist, VideoBlacklistType } from '../../../../shared/models/videos'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
|
@ -101,7 +102,7 @@ describe('Test video blacklist management', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('When listing blacklisted videos', function () {
|
||||
describe('When listing manually blacklisted videos', function () {
|
||||
it('Should display all the blacklisted videos', async function () {
|
||||
const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken)
|
||||
|
||||
|
@ -117,6 +118,26 @@ describe('Test video blacklist management', function () {
|
|||
}
|
||||
})
|
||||
|
||||
it('Should display all the blacklisted videos when applying manual type filter', async function () {
|
||||
const res = await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.MANUAL)
|
||||
|
||||
expect(res.body.total).to.equal(2)
|
||||
|
||||
const blacklistedVideos = res.body.data
|
||||
expect(blacklistedVideos).to.be.an('array')
|
||||
expect(blacklistedVideos.length).to.equal(2)
|
||||
})
|
||||
|
||||
it('Should display nothing when applying automatic type filter', async function () {
|
||||
const res = await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.AUTO_BEFORE_PUBLISHED) // tslint:disable:max-line-length
|
||||
|
||||
expect(res.body.total).to.equal(0)
|
||||
|
||||
const blacklistedVideos = res.body.data
|
||||
expect(blacklistedVideos).to.be.an('array')
|
||||
expect(blacklistedVideos.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('Should get the correct sort when sorting by descending id', async function () {
|
||||
const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id')
|
||||
expect(res.body.total).to.equal(2)
|
||||
|
|
|
@ -77,4 +77,13 @@ export interface CustomConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -49,6 +49,14 @@ export interface ServerConfig {
|
|||
}
|
||||
}
|
||||
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
avatar: {
|
||||
file: {
|
||||
size: {
|
||||
|
|
|
@ -8,6 +8,7 @@ export interface UserNotificationSetting {
|
|||
newVideoFromSubscription: UserNotificationSettingValue
|
||||
newCommentOnMyVideo: UserNotificationSettingValue
|
||||
videoAbuseAsModerator: UserNotificationSettingValue
|
||||
videoAutoBlacklistAsModerator: UserNotificationSettingValue
|
||||
blacklistOnMyVideo: UserNotificationSettingValue
|
||||
myVideoPublished: UserNotificationSettingValue
|
||||
myVideoImportFinished: UserNotificationSettingValue
|
||||
|
|
|
@ -13,7 +13,9 @@ export enum UserNotificationType {
|
|||
|
||||
NEW_USER_REGISTRATION = 9,
|
||||
NEW_FOLLOW = 10,
|
||||
COMMENT_MENTION = 11
|
||||
COMMENT_MENTION = 11,
|
||||
|
||||
VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12
|
||||
}
|
||||
|
||||
export interface VideoInfo {
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import { Video } from '../video.model'
|
||||
|
||||
export enum VideoBlacklistType {
|
||||
MANUAL = 1,
|
||||
AUTO_BEFORE_PUBLISHED = 2
|
||||
}
|
||||
|
||||
export interface VideoBlacklist {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
unfederated: boolean
|
||||
reason?: string
|
||||
type: VideoBlacklistType
|
||||
|
||||
video: {
|
||||
id: number
|
||||
name: string
|
||||
uuid: string
|
||||
description: string
|
||||
duration: number
|
||||
views: number
|
||||
likes: number
|
||||
dislikes: number
|
||||
nsfw: boolean
|
||||
}
|
||||
video: Video
|
||||
}
|
||||
|
|
|
@ -112,6 +112,13 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
|
|||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ function updateMyNotificationSettings (url: string, token: string, settings: Use
|
|||
})
|
||||
}
|
||||
|
||||
function getUserNotifications (
|
||||
async function getUserNotifications (
|
||||
url: string,
|
||||
token: string,
|
||||
start: number,
|
||||
|
@ -165,12 +165,15 @@ async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName
|
|||
checkVideo(notification.video, videoName, videoUUID)
|
||||
checkActor(notification.video.channel)
|
||||
} else {
|
||||
expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailFinder (email: object) {
|
||||
return email[ 'text' ].indexOf(videoUUID) !== -1
|
||||
const text = email[ 'text' ]
|
||||
return text.indexOf(videoUUID) !== -1 && text.indexOf('Your subscription') !== -1
|
||||
}
|
||||
|
||||
await checkNotification(base, notificationChecker, emailFinder, type)
|
||||
|
@ -387,6 +390,31 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
|
|||
await checkNotification(base, notificationChecker, emailFinder, type)
|
||||
}
|
||||
|
||||
async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
|
||||
const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
|
||||
|
||||
function notificationChecker (notification: UserNotification, type: CheckerType) {
|
||||
if (type === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.video.id).to.be.a('number')
|
||||
checkVideo(notification.video, videoName, videoUUID)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailFinder (email: object) {
|
||||
const text = email[ 'text' ]
|
||||
return text.indexOf(videoUUID) !== -1 && email[ 'text' ].indexOf('video-auto-blacklist/list') !== -1
|
||||
}
|
||||
|
||||
await checkNotification(base, notificationChecker, emailFinder, type)
|
||||
}
|
||||
|
||||
async function checkNewBlacklistOnMyVideo (
|
||||
base: CheckerBaseParams,
|
||||
videoUUID: string,
|
||||
|
@ -431,6 +459,7 @@ export {
|
|||
checkCommentMention,
|
||||
updateMyNotificationSettings,
|
||||
checkNewVideoAbuseForModerators,
|
||||
checkVideoAutoBlacklistForModerators,
|
||||
getUserNotifications,
|
||||
markAsReadNotifications,
|
||||
getLastNotification
|
||||
|
|
|
@ -51,6 +51,18 @@ function getBlacklistedVideosList (url: string, token: string, specialStatus = 2
|
|||
.expect('Content-Type', /json/)
|
||||
}
|
||||
|
||||
function getBlacklistedVideosListWithTypeFilter (url: string, token: string, type: number, specialStatus = 200) {
|
||||
const path = '/api/v1/videos/blacklist/'
|
||||
|
||||
return request(url)
|
||||
.get(path)
|
||||
.query({ sort: 'createdAt', type })
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', 'Bearer ' + token)
|
||||
.expect(specialStatus)
|
||||
.expect('Content-Type', /json/)
|
||||
}
|
||||
|
||||
function getSortedBlacklistedVideosList (url: string, token: string, sort: string, specialStatus = 200) {
|
||||
const path = '/api/v1/videos/blacklist/'
|
||||
|
||||
|
@ -69,6 +81,7 @@ export {
|
|||
addVideoToBlacklist,
|
||||
removeVideoFromBlacklist,
|
||||
getBlacklistedVideosList,
|
||||
getBlacklistedVideosListWithTypeFilter,
|
||||
getSortedBlacklistedVideosList,
|
||||
updateVideoBlacklist
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as request from 'supertest'
|
||||
|
||||
function changeVideoOwnership (url: string, token: string, videoId: number | string, username) {
|
||||
function changeVideoOwnership (url: string, token: string, videoId: number | string, username, expectedStatus = 204) {
|
||||
const path = '/api/v1/videos/' + videoId + '/give-ownership'
|
||||
|
||||
return request(url)
|
||||
|
@ -8,7 +8,7 @@ function changeVideoOwnership (url: string, token: string, videoId: number | str
|
|||
.set('Accept', 'application/json')
|
||||
.set('Authorization', 'Bearer ' + token)
|
||||
.send({ username })
|
||||
.expect(204)
|
||||
.expect(expectedStatus)
|
||||
}
|
||||
|
||||
function getVideoChangeOwnershipList (url: string, token: string) {
|
||||
|
|
Loading…
Reference in New Issue