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:
Josh Morel 2019-04-02 05:26:47 -04:00 committed by Chocobozzz
parent 12fed49eba
commit 7ccddd7b52
58 changed files with 1047 additions and 99 deletions

View File

@ -11,7 +11,12 @@ import { JobsComponent } from './jobs/job.component'
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
import { JobService } from './jobs/shared/job.service' import { JobService } from './jobs/shared/job.service'
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users' 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 { ModerationComponent } from '@app/+admin/moderation/moderation.component'
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
@ -42,6 +47,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
ModerationComponent, ModerationComponent,
VideoBlacklistListComponent, VideoBlacklistListComponent,
VideoAbuseListComponent, VideoAbuseListComponent,
VideoAutoBlacklistListComponent,
ModerationCommentModalComponent, ModerationCommentModalComponent,
InstanceServerBlocklistComponent, InstanceServerBlocklistComponent,
InstanceAccountBlocklistComponent, InstanceAccountBlocklistComponent,

View File

@ -161,6 +161,23 @@
</ng-container> </ng-container>
</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 i18n class="inner-form-title">Administrator</div>
<div class="form-group" formGroupName="admin"> <div class="form-group" formGroupName="admin">

View File

@ -117,6 +117,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
threads: this.customConfigValidatorsService.TRANSCODING_THREADS, threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
allowAdditionalExtensions: null, allowAdditionalExtensions: null,
resolutions: {} resolutions: {}
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: null
}
}
} }
} }

View File

@ -1,4 +1,5 @@
export * from './video-abuse-list' export * from './video-abuse-list'
export * from './video-auto-blacklist-list'
export * from './video-blacklist-list' export * from './video-blacklist-list'
export * from './moderation.component' export * from './moderation.component'
export * from './moderation.routes' export * from './moderation.routes'

View File

@ -4,7 +4,9 @@
<div class="admin-sub-nav"> <div class="admin-sub-nav">
<a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a> <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> <a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>

View File

@ -1,13 +1,20 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { UserRight } from '../../../../../shared' import { UserRight } from '../../../../../shared'
import { AuthService } from '@app/core/auth/auth.service' import { AuthService, ServerService } from '@app/core'
@Component({ @Component({
templateUrl: './moderation.component.html', templateUrl: './moderation.component.html',
styleUrls: [ './moderation.component.scss' ] styleUrls: [ './moderation.component.scss' ]
}) })
export class ModerationComponent { 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 () { hasVideoAbusesRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)

View File

@ -3,6 +3,7 @@ import { UserRight } from '../../../../../shared'
import { UserRightGuard } from '@app/core' import { UserRightGuard } from '@app/core'
import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-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 { ModerationComponent } from '@app/+admin/moderation/moderation.component'
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
@ -26,6 +27,11 @@ export const ModerationRoutes: Routes = [
redirectTo: 'video-blacklist/list', redirectTo: 'video-blacklist/list',
pathMatch: 'full' pathMatch: 'full'
}, },
{
path: 'video-auto-blacklist',
redirectTo: 'video-auto-blacklist/list',
pathMatch: 'full'
},
{ {
path: 'video-abuses/list', path: 'video-abuses/list',
component: VideoAbuseListComponent, 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', path: 'video-blacklist/list',
component: VideoBlacklistListComponent, component: VideoBlacklistListComponent,

View File

@ -0,0 +1 @@
export * from './video-auto-blacklist-list.component'

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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)
)
}
}

View File

@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { SortMeta } from 'primeng/components/common/sortmeta' import { SortMeta } from 'primeng/components/common/sortmeta'
import { Notifier } from '@app/core' import { Notifier, ServerService } from '@app/core'
import { ConfirmService } from '../../../core' import { ConfirmService } from '../../../core'
import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
import { VideoBlacklist } from '../../../../../../shared' import { VideoBlacklist, VideoBlacklistType } from '../../../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
import { Video } from '../../../shared/video/video.model' import { Video } from '../../../shared/video/video.model'
@ -20,11 +20,13 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
rowsPerPage = 10 rowsPerPage = 10
sort: SortMeta = { field: 'createdAt', order: 1 } sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
listBlacklistTypeFilter: VideoBlacklistType = undefined
videoBlacklistActions: DropdownAction<VideoBlacklist>[] = [] videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
constructor ( constructor (
private notifier: Notifier, private notifier: Notifier,
private serverService: ServerService,
private confirmService: ConfirmService, private confirmService: ConfirmService,
private videoBlacklistService: VideoBlacklistService, private videoBlacklistService: VideoBlacklistService,
private markdownRenderer: MarkdownService, private markdownRenderer: MarkdownService,
@ -32,6 +34,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
) { ) {
super() 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 = [ this.videoBlacklistActions = [
{ {
label: this.i18n('Unblacklist'), label: this.i18n('Unblacklist'),
@ -77,7 +84,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
} }
protected loadData () { protected loadData () {
this.videoBlacklistService.listBlacklist(this.pagination, this.sort) this.videoBlacklistService.listBlacklist(this.pagination, this.sort, this.listBlacklistTypeFilter)
.subscribe( .subscribe(
async resultList => { async resultList => {
this.totalRecords = resultList.total this.totalRecords = resultList.total

View File

@ -31,10 +31,12 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
private serverService: ServerService, private serverService: ServerService,
private notifier: Notifier private notifier: Notifier
) { ) {
this.labelNotifications = { this.labelNotifications = {
newVideoFromSubscription: this.i18n('New video from your subscriptions'), newVideoFromSubscription: this.i18n('New video from your subscriptions'),
newCommentOnMyVideo: this.i18n('New comment on your video'), newCommentOnMyVideo: this.i18n('New comment on your video'),
videoAbuseAsModerator: this.i18n('New video abuse'), videoAbuseAsModerator: this.i18n('New video abuse'),
videoAutoBlacklistAsModerator: this.i18n('Video auto-blacklisted waiting review'),
blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'), blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'),
myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'), myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
myVideoImportFinished: this.i18n('Video import finished'), myVideoImportFinished: this.i18n('Video import finished'),
@ -46,6 +48,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
this.rightNotifications = { this.rightNotifications = {
videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES, videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
newUserRegistration: UserRight.MANAGE_USERS newUserRegistration: UserRight.MANAGE_USERS
} }

View File

@ -82,6 +82,7 @@
} }
} }
} }
} }
} }

View File

@ -98,6 +98,13 @@ export class ServerService {
videos: { videos: {
intervalDays: 0 intervalDays: 0
} }
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: false
}
}
} }
} }
private videoCategories: Array<VideoConstant<number>> = [] private videoCategories: Array<VideoConstant<number>> = []

View File

@ -54,6 +54,7 @@ export class UserNotification implements UserNotificationServer {
videoUrl?: string videoUrl?: string
commentUrl?: any[] commentUrl?: any[]
videoAbuseUrl?: string videoAbuseUrl?: string
videoAutoBlacklistUrl?: string
accountUrl?: string accountUrl?: string
videoImportIdentifier?: string videoImportIdentifier?: string
videoImportUrl?: string videoImportUrl?: string
@ -107,6 +108,11 @@ export class UserNotification implements UserNotificationServer {
this.videoUrl = this.buildVideoUrl(this.videoAbuse.video) this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
break 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: case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video) this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
break break

View File

@ -36,6 +36,14 @@
</div> </div>
</ng-container> </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"> <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />

View File

@ -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 { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { SortMeta } from 'primeng/components/common/sortmeta' import { SortMeta } from 'primeng/components/common/sortmeta'
import { Observable } from 'rxjs' import { from as observableFrom, Observable } from 'rxjs'
import { VideoBlacklist, ResultList } from '../../../../../shared' import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared'
import { Video } from '../video/video.model'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
import { RestExtractor, RestPagination, RestService } from '../rest' import { RestExtractor, RestPagination, RestService } from '../rest'
import { ComponentPagination } from '../rest/component-pagination.model'
@Injectable() @Injectable()
export class VideoBlacklistService { export class VideoBlacklistService {
@ -17,10 +19,14 @@ export class VideoBlacklistService {
private restExtractor: RestExtractor 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() let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort) 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 }) return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
.pipe( .pipe(
map(res => this.restExtractor.convertResultListDateToHuman(res)), map(res => this.restExtractor.convertResultListDateToHuman(res)),
@ -28,12 +34,37 @@ export class VideoBlacklistService {
) )
} }
removeVideoFromBlacklist (videoId: number) { getAutoBlacklistedAsVideoList (videoPagination: ComponentPagination): Observable<{ videos: Video[], totalVideos: number}> {
return this.authHttp.delete(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist') const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
.pipe(
map(this.restExtractor.extractDataBool), // prioritize first created since waiting longest
catchError(res => this.restExtractor.handleError(res)) 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) { blacklistVideo (videoId: number, reason: string, unfederate: boolean) {

View File

@ -162,6 +162,12 @@ import:
torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
enabled: false enabled: false
auto_blacklist:
# New videos automatically blacklisted so moderators can review before publishing
videos:
of_users:
enabled: false
instance: instance:
name: 'PeerTube' name: 'PeerTube'
short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.' short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'

View File

@ -176,6 +176,12 @@ import:
torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
enabled: false enabled: false
auto_blacklist:
# New videos automatically blacklisted so moderators can review before publishing
videos:
of_users:
enabled: false
# Instance settings # Instance settings
instance: instance:
name: 'PeerTube' name: 'PeerTube'

View File

@ -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: { avatar: {
file: { file: {
size: { size: {
@ -265,6 +272,13 @@ function customConfig (): CustomConfig {
enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
} }
} }
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
}
}
} }
} }
} }

View File

@ -69,6 +69,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
newVideoFromSubscription: body.newVideoFromSubscription, newVideoFromSubscription: body.newVideoFromSubscription,
newCommentOnMyVideo: body.newCommentOnMyVideo, newCommentOnMyVideo: body.newCommentOnMyVideo,
videoAbuseAsModerator: body.videoAbuseAsModerator, videoAbuseAsModerator: body.videoAbuseAsModerator,
videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: body.blacklistOnMyVideo, blacklistOnMyVideo: body.blacklistOnMyVideo,
myVideoPublished: body.myVideoPublished, myVideoPublished: body.myVideoPublished,
myVideoImportFinished: body.myVideoImportFinished, myVideoImportFinished: body.myVideoImportFinished,

View File

@ -1,5 +1,5 @@
import * as express from 'express' import * as express from 'express'
import { UserRight, VideoBlacklist, VideoBlacklistCreate } from '../../../../shared' import { VideoBlacklist, UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils' import { getFormattedObjects } from '../../../helpers/utils'
import { import {
@ -12,7 +12,8 @@ import {
setDefaultPagination, setDefaultPagination,
videosBlacklistAddValidator, videosBlacklistAddValidator,
videosBlacklistRemoveValidator, videosBlacklistRemoveValidator,
videosBlacklistUpdateValidator videosBlacklistUpdateValidator,
videosBlacklistFiltersValidator
} from '../../../middlewares' } from '../../../middlewares'
import { VideoBlacklistModel } from '../../../models/video/video-blacklist' import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
@ -36,6 +37,7 @@ blacklistRouter.get('/blacklist',
blacklistSortValidator, blacklistSortValidator,
setBlacklistSort, setBlacklistSort,
setDefaultPagination, setDefaultPagination,
videosBlacklistFiltersValidator,
asyncMiddleware(listBlacklist) asyncMiddleware(listBlacklist)
) )
@ -68,7 +70,8 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
const toCreate = { const toCreate = {
videoId: videoInstance.id, videoId: videoInstance.id,
unfederated: body.unfederate === true, unfederated: body.unfederate === true,
reason: body.reason reason: body.reason,
type: VideoBlacklistType.MANUAL
} }
const blacklist = await VideoBlacklistModel.create(toCreate) 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) { 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)) 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 videoBlacklist = res.locals.videoBlacklist
const video = res.locals.video const video = res.locals.video
await sequelizeTypescript.transaction(async t => { const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
const unfederated = videoBlacklist.unfederated const unfederated = videoBlacklist.unfederated
const videoBlacklistType = videoBlacklist.type
await videoBlacklist.destroy({ transaction: t }) await videoBlacklist.destroy({ transaction: t })
// Re federate the video // Re federate the video
if (unfederated === true) { if (unfederated === true) {
await federateVideoIfNeeded(video, true, t) await federateVideoIfNeeded(video, true, t)
} }
return videoBlacklistType
}) })
Notifier.Instance.notifyOnVideoUnblacklist(video) 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) logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
return res.type('json').status(204).end() return res.type('json').status(204).end()

View File

@ -18,10 +18,12 @@ import { join } from 'path'
import { isArray } from '../../../helpers/custom-validators/misc' import { isArray } from '../../../helpers/custom-validators/misc'
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
import { VideoChannelModel } from '../../../models/video/video-channel' import { VideoChannelModel } from '../../../models/video/video-channel'
import { UserModel } from '../../../models/account/user'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import * as parseTorrent from 'parse-torrent' import * as parseTorrent from 'parse-torrent'
import { getSecureTorrentName } from '../../../helpers/utils' import { getSecureTorrentName } from '../../../helpers/utils'
import { readFile, move } from 'fs-extra' import { readFile, move } from 'fs-extra'
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
const auditLogger = auditLoggerFactory('video-imports') const auditLogger = auditLoggerFactory('video-imports')
const videoImportsRouter = express.Router() 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 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 processThumbnail(req, video)
await processPreview(req, video) await processPreview(req, video)
@ -128,7 +130,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
}).end() }).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 downloadThumbnail = !await processThumbnail(req, video)
const downloadPreview = !await processPreview(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() 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 = { const videoData = {
name: body.name || importData.name || 'Unknown name', name: body.name || importData.name || 'Unknown name',
remote: false, remote: false,
@ -218,6 +220,8 @@ function insertIntoDB (
const videoCreated = await video.save(sequelizeOptions) const videoCreated = await video.save(sequelizeOptions)
videoCreated.VideoChannel = videoChannel videoCreated.VideoChannel = videoChannel
await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
// Set tags to the video // Set tags to the video
if (tags) { if (tags) {
const tagInstances = await TagModel.findOrCreateTags(tags, t) const tagInstances = await TagModel.findOrCreateTags(tags, t)

View File

@ -6,6 +6,7 @@ import { processImage } from '../../../helpers/image-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { getFormattedObjects, getServerActor } from '../../../helpers/utils' import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
import { import {
CONFIG, CONFIG,
MIMETYPES, MIMETYPES,
@ -193,6 +194,7 @@ async function addVideo (req: express.Request, res: express.Response) {
channelId: res.locals.videoChannel.id, channelId: res.locals.videoChannel.id,
originallyPublishedAt: videoInfo.originallyPublishedAt originallyPublishedAt: videoInfo.originallyPublishedAt
} }
const video = new VideoModel(videoData) const video = new VideoModel(videoData)
video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 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 // Create the torrent file
await video.createTorrentAndSetInfoHash(videoFile) await video.createTorrentAndSetInfoHash(videoFile)
const videoCreated = await sequelizeTypescript.transaction(async t => { const { videoCreated, videoWasAutoBlacklisted } = await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t } const sequelizeOptions = { transaction: t }
const videoCreated = await video.save(sequelizeOptions) const videoCreated = await video.save(sequelizeOptions)
@ -266,15 +268,23 @@ async function addVideo (req: express.Request, res: express.Response) {
}, { transaction: t }) }, { 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())) auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 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) { if (video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now // Put uuid because we don't have id auto incremented for now

View File

@ -1,7 +1,9 @@
import { Response } from 'express' import { Response } from 'express'
import * as validator from 'validator' import * as validator from 'validator'
import { exists } from './misc'
import { CONSTRAINTS_FIELDS } from '../../initializers' import { CONSTRAINTS_FIELDS } from '../../initializers'
import { VideoBlacklistModel } from '../../models/video/video-blacklist' import { VideoBlacklistModel } from '../../models/video/video-blacklist'
import { VideoBlacklistType } from '../../../shared/models/videos'
const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
@ -24,9 +26,14 @@ async function doesVideoBlacklistExist (videoId: number, res: Response) {
return true return true
} }
function isVideoBlacklistTypeValid (value: any) {
return exists(value) && validator.isInt('' + value) && VideoBlacklistType[value] !== undefined
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
isVideoBlacklistReasonValid, isVideoBlacklistReasonValid,
isVideoBlacklistTypeValid,
doesVideoBlacklistExist doesVideoBlacklistExist
} }

View File

@ -1,4 +1,7 @@
import { CONFIG } from '../initializers'
import { VideoModel } from '../models/video/video' 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' type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'

View File

@ -20,7 +20,7 @@ function checkMissedConfig () {
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
'redundancy.videos.strategies', 'redundancy.videos.check_interval', 'redundancy.videos.strategies', 'redundancy.videos.check_interval',
'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', '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', 'trending.videos.interval_days',
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt', 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',

View File

@ -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: { CACHE: {
PREVIEWS: { PREVIEWS: {
get SIZE () { return config.get<number>('cache.previews.size') } get SIZE () { return config.get<number>('cache.previews.size') }

View File

@ -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
}

View File

@ -45,7 +45,7 @@ import { VideoShareModel } from '../../models/video/video-share'
import { VideoCommentModel } from '../../models/video/video-comment' import { VideoCommentModel } from '../../models/video/video-comment'
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 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) { if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
// Fetch more attributes that we will need to serialize in AP object // Fetch more attributes that we will need to serialize in AP object
if (isArray(video.VideoCaptions) === false) { if (isArray(video.VideoCaptions) === false) {

View File

@ -250,6 +250,29 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 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) { addNewUserRegistrationNotification (to: string[], user: UserModel) {
const text = `Hi,\n\n` + const text = `Hi,\n\n` +
`User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` + `User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` +

View File

@ -196,9 +196,14 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
return videoImportUpdated return videoImportUpdated
}) })
Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
if (videoImportUpdated.Video.VideoBlacklist) {
Notifier.Instance.notifyOnVideoAutoBlacklist(videoImportUpdated.Video)
} else {
Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
}
// Create transcoding jobs? // Create transcoding jobs?
if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now // Put uuid because we don't have id auto incremented for now

View File

@ -85,10 +85,9 @@ async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodi
return { videoDatabase, videoPublished } return { videoDatabase, videoPublished }
}) })
// don't notify prior to scheduled video update if (videoPublished) {
if (videoPublished && !videoDatabase.ScheduleVideoUpdate) {
Notifier.Instance.notifyOnNewVideo(videoDatabase) Notifier.Instance.notifyOnNewVideo(videoDatabase)
Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
} }
await createHlsJobIfEnabled(payload) await createHlsJobIfEnabled(payload)
@ -146,11 +145,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video
return { videoDatabase, videoPublished } return { videoDatabase, videoPublished }
}) })
// don't notify prior to scheduled video update if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
if (!videoDatabase.ScheduleVideoUpdate) { if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase)
}
await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })) await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }))
} }

View File

@ -23,19 +23,35 @@ class Notifier {
private constructor () {} private constructor () {}
notifyOnNewVideo (video: VideoModel): void { notifyOnNewVideo (video: VideoModel): void {
// Only notify on public and published videos // Only notify on public and published videos which are not blacklisted
if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED) return if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.VideoBlacklist) return
this.notifySubscribersOfNewVideo(video) this.notifySubscribersOfNewVideo(video)
.catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
} }
notifyOnPendingVideoPublished (video: VideoModel): void { notifyOnVideoPublishedAfterTranscoding (video: VideoModel): void {
// Only notify on public videos that has been published while the user waited transcoding/scheduled update // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
if (video.waitTranscoding === false && !video.ScheduleVideoUpdate) return if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
this.notifyOwnedVideoHasBeenPublished(video) 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 { 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 })) .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 { notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void {
this.notifyVideoOwnerOfBlacklist(videoBlacklist) this.notifyVideoOwnerOfBlacklist(videoBlacklist)
.catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) .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 { notifyOnVideoUnblacklist (video: VideoModel): void {
this.notifyVideoOwnerOfUnblacklist(video) 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 { notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void {
@ -268,6 +289,34 @@ class Notifier {
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 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) { private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) {
const user = await UserModel.loadByVideoId(videoBlacklist.videoId) const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
if (!user) return if (!user) return

View File

@ -57,7 +57,7 @@ export class UpdateVideosScheduler extends AbstractScheduler {
for (const v of publishedVideos) { for (const v of publishedVideos) {
Notifier.Instance.notifyOnNewVideo(v) Notifier.Instance.notifyOnNewVideo(v)
Notifier.Instance.notifyOnPendingVideoPublished(v) Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(v)
} }
} }

View File

@ -106,6 +106,7 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr
myVideoImportFinished: UserNotificationSettingValue.WEB, myVideoImportFinished: UserNotificationSettingValue.WEB,
myVideoPublished: UserNotificationSettingValue.WEB, myVideoPublished: UserNotificationSettingValue.WEB,
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newUserRegistration: UserNotificationSettingValue.WEB, newUserRegistration: UserNotificationSettingValue.WEB,
commentMention: UserNotificationSettingValue.WEB, commentMention: UserNotificationSettingValue.WEB,

View File

@ -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
}

View File

@ -1,10 +1,14 @@
import * as express from 'express' 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 { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
import { doesVideoExist } from '../../../helpers/custom-validators/videos' import { doesVideoExist } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { areValidationErrors } from '../utils' 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 = [ const videosBlacklistRemoveValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 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 { export {
videosBlacklistAddValidator, videosBlacklistAddValidator,
videosBlacklistRemoveValidator, videosBlacklistRemoveValidator,
videosBlacklistUpdateValidator videosBlacklistUpdateValidator,
videosBlacklistFiltersValidator
} }

View File

@ -56,6 +56,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
@Column @Column
videoAbuseAsModerator: UserNotificationSettingValue videoAbuseAsModerator: UserNotificationSettingValue
@AllowNull(false)
@Default(null)
@Is(
'UserNotificationSettingVideoAutoBlacklistAsModerator',
value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator')
)
@Column
videoAutoBlacklistAsModerator: UserNotificationSettingValue
@AllowNull(false) @AllowNull(false)
@Default(null) @Default(null)
@Is( @Is(
@ -139,6 +148,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
newCommentOnMyVideo: this.newCommentOnMyVideo, newCommentOnMyVideo: this.newCommentOnMyVideo,
newVideoFromSubscription: this.newVideoFromSubscription, newVideoFromSubscription: this.newVideoFromSubscription,
videoAbuseAsModerator: this.videoAbuseAsModerator, videoAbuseAsModerator: this.videoAbuseAsModerator,
videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: this.blacklistOnMyVideo, blacklistOnMyVideo: this.blacklistOnMyVideo,
myVideoPublished: this.myVideoPublished, myVideoPublished: this.myVideoPublished,
myVideoImportFinished: this.myVideoImportFinished, myVideoImportFinished: this.myVideoImportFinished,

View File

@ -72,7 +72,8 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
model: VideoModel.scope( model: VideoModel.scope(
[ [
VideoScopeNames.WITH_FILES, VideoScopeNames.WITH_FILES,
VideoScopeNames.WITH_ACCOUNT_DETAILS VideoScopeNames.WITH_ACCOUNT_DETAILS,
VideoScopeNames.WITH_BLACKLISTED
] ]
) )
} }

View File

@ -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 { getSortOnModel, SortType, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' import { VideoChannelModel, ScopeNames as VideoChannelScopeNames } from './video-channel'
import { VideoBlacklist } from '../../../shared/models/videos' import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
import { CONSTRAINTS_FIELDS } from '../../initializers' import { CONSTRAINTS_FIELDS } from '../../initializers'
@Table({ @Table({
@ -25,6 +38,12 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
@Column @Column
unfederated: boolean unfederated: boolean
@AllowNull(false)
@Default(null)
@Is('VideoBlacklistType', value => throwIfNotValid(value, isVideoBlacklistTypeValid, 'type'))
@Column
type: VideoBlacklistType
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -43,19 +62,29 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
}) })
Video: VideoModel Video: VideoModel
static listForApi (start: number, count: number, sort: SortType) { static listForApi (start: number, count: number, sort: SortType, type?: VideoBlacklistType) {
const query = { const query: IFindOptions<VideoBlacklistModel> = {
offset: start, offset: start,
limit: count, limit: count,
order: getSortOnModel(sort.sortModel, sort.sortValue), order: getSortOnModel(sort.sortModel, sort.sortValue),
include: [ include: [
{ {
model: VideoModel, 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) return VideoBlacklistModel.findAndCountAll(query)
.then(({ rows, count }) => { .then(({ rows, count }) => {
return { return {
@ -76,26 +105,15 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
} }
toFormattedJSON (): VideoBlacklist { toFormattedJSON (): VideoBlacklist {
const video = this.Video
return { return {
id: this.id, id: this.id,
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
reason: this.reason, reason: this.reason,
unfederated: this.unfederated, unfederated: this.unfederated,
type: this.type,
video: { video: this.Video.toFormattedJSON()
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
}
} }
} }
} }

View File

@ -80,6 +80,13 @@ describe('Test config API validators', function () {
enabled: false enabled: false
} }
} }
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: false
}
}
} }
} }

View File

@ -168,6 +168,7 @@ describe('Test user notifications API validators', function () {
newVideoFromSubscription: UserNotificationSettingValue.WEB, newVideoFromSubscription: UserNotificationSettingValue.WEB,
newCommentOnMyVideo: UserNotificationSettingValue.WEB, newCommentOnMyVideo: UserNotificationSettingValue.WEB,
videoAbuseAsModerator: UserNotificationSettingValue.WEB, videoAbuseAsModerator: UserNotificationSettingValue.WEB,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
blacklistOnMyVideo: UserNotificationSettingValue.WEB, blacklistOnMyVideo: UserNotificationSettingValue.WEB,
myVideoImportFinished: UserNotificationSettingValue.WEB, myVideoImportFinished: UserNotificationSettingValue.WEB,
myVideoPublished: UserNotificationSettingValue.WEB, myVideoPublished: UserNotificationSettingValue.WEB,

View File

@ -8,6 +8,7 @@ import {
flushAndRunMultipleServers, flushAndRunMultipleServers,
flushTests, flushTests,
getBlacklistedVideosList, getBlacklistedVideosList,
getBlacklistedVideosListWithTypeFilter,
getVideo, getVideo,
getVideoWithToken, getVideoWithToken,
killallServers, killallServers,
@ -24,7 +25,7 @@ import {
checkBadSortPagination, checkBadSortPagination,
checkBadStartPagination checkBadStartPagination
} from '../../../../shared/utils/requests/check-api-params' } from '../../../../shared/utils/requests/check-api-params'
import { VideoDetails } from '../../../../shared/models/videos' import { VideoDetails, VideoBlacklistType } from '../../../../shared/models/videos'
import { expect } from 'chai' import { expect } from 'chai'
describe('Test video blacklist API validators', function () { 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 () { it('Should fail with an incorrect sort', async function () {
await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken) 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 () { after(async function () {

View File

@ -7,7 +7,8 @@ import { join } from 'path'
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
import { import {
createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest, 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' } from '../../../../shared/utils'
import { import {
checkBadCountPagination, checkBadCountPagination,

View File

@ -62,6 +62,7 @@ function checkInitialConfig (data: CustomConfig) {
expect(data.import.videos.http.enabled).to.be.true expect(data.import.videos.http.enabled).to.be.true
expect(data.import.videos.torrent.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) { 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.http.enabled).to.be.false
expect(data.import.videos.torrent.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 () { describe('Test config', function () {
@ -225,6 +227,13 @@ describe('Test config', function () {
enabled: false enabled: false
} }
} }
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: true
}
}
} }
} }
await updateCustomConfig(server.url, server.accessToken, newCustomConfig) await updateCustomConfig(server.url, server.accessToken, newCustomConfig)

View File

@ -17,7 +17,9 @@ import {
updateVideo, updateVideo,
updateVideoChannel, updateVideoChannel,
userLogin, userLogin,
wait wait,
getCustomConfig,
updateCustomConfig
} from '../../../../shared/utils' } from '../../../../shared/utils'
import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index' import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index'
import { setAccessTokensToServers } from '../../../../shared/utils/users/login' import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
@ -31,6 +33,7 @@ import {
checkNewBlacklistOnMyVideo, checkNewBlacklistOnMyVideo,
checkNewCommentOnMyVideo, checkNewCommentOnMyVideo,
checkNewVideoAbuseForModerators, checkNewVideoAbuseForModerators,
checkVideoAutoBlacklistForModerators,
checkNewVideoFromSubscription, checkNewVideoFromSubscription,
checkUserRegistered, checkUserRegistered,
checkVideoIsPublished, checkVideoIsPublished,
@ -54,6 +57,7 @@ import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../sha
import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
import * as uuidv4 from 'uuid/v4' import * as uuidv4 from 'uuid/v4'
import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist' import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist'
import { CustomConfig } from '../../../../shared/models/server'
const expect = chai.expect const expect = chai.expect
@ -92,6 +96,7 @@ describe('Test users notifications', function () {
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
myVideoPublished: 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 () { it('Should send a new video notification after a video import', async function () {
this.timeout(30000) this.timeout(100000)
const name = 'video import ' + uuidv4() 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 () { describe('Mark as read', function () {
it('Should mark as read some notifications', async function () { it('Should mark as read some notifications', async function () {
const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3) 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 () { it('Should not have notifications', async function () {
this.timeout(10000) this.timeout(20000)
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
newVideoFromSubscription: UserNotificationSettingValue.NONE newVideoFromSubscription: UserNotificationSettingValue.NONE
@ -987,7 +1166,7 @@ describe('Test users notifications', function () {
}) })
it('Should only have web notifications', async function () { it('Should only have web notifications', async function () {
this.timeout(10000) this.timeout(20000)
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
newVideoFromSubscription: UserNotificationSettingValue.WEB newVideoFromSubscription: UserNotificationSettingValue.WEB
@ -1013,7 +1192,7 @@ describe('Test users notifications', function () {
}) })
it('Should only have mail notifications', async function () { it('Should only have mail notifications', async function () {
this.timeout(10000) this.timeout(20000)
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
newVideoFromSubscription: UserNotificationSettingValue.EMAIL newVideoFromSubscription: UserNotificationSettingValue.EMAIL
@ -1039,7 +1218,7 @@ describe('Test users notifications', function () {
}) })
it('Should have email and web notifications', async function () { it('Should have email and web notifications', async function () {
this.timeout(10000) this.timeout(20000)
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL

View File

@ -7,6 +7,7 @@ import {
addVideoToBlacklist, addVideoToBlacklist,
flushAndRunMultipleServers, flushAndRunMultipleServers,
getBlacklistedVideosList, getBlacklistedVideosList,
getBlacklistedVideosListWithTypeFilter,
getMyVideos, getMyVideos,
getSortedBlacklistedVideosList, getSortedBlacklistedVideosList,
getVideosList, getVideosList,
@ -22,7 +23,7 @@ import {
} from '../../../../shared/utils/index' } from '../../../../shared/utils/index'
import { doubleFollow } from '../../../../shared/utils/server/follows' import { doubleFollow } from '../../../../shared/utils/server/follows'
import { waitJobs } from '../../../../shared/utils/server/jobs' import { waitJobs } from '../../../../shared/utils/server/jobs'
import { VideoBlacklist } from '../../../../shared/models/videos' import { VideoBlacklist, VideoBlacklistType } from '../../../../shared/models/videos'
const expect = chai.expect 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 () { it('Should display all the blacklisted videos', async function () {
const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken) 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 () { it('Should get the correct sort when sorting by descending id', async function () {
const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id') const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id')
expect(res.body.total).to.equal(2) expect(res.body.total).to.equal(2)

View File

@ -77,4 +77,13 @@ export interface CustomConfig {
} }
} }
} }
autoBlacklist: {
videos: {
ofUsers: {
enabled: boolean
}
}
}
} }

View File

@ -49,6 +49,14 @@ export interface ServerConfig {
} }
} }
autoBlacklist: {
videos: {
ofUsers: {
enabled: boolean
}
}
}
avatar: { avatar: {
file: { file: {
size: { size: {

View File

@ -8,6 +8,7 @@ export interface UserNotificationSetting {
newVideoFromSubscription: UserNotificationSettingValue newVideoFromSubscription: UserNotificationSettingValue
newCommentOnMyVideo: UserNotificationSettingValue newCommentOnMyVideo: UserNotificationSettingValue
videoAbuseAsModerator: UserNotificationSettingValue videoAbuseAsModerator: UserNotificationSettingValue
videoAutoBlacklistAsModerator: UserNotificationSettingValue
blacklistOnMyVideo: UserNotificationSettingValue blacklistOnMyVideo: UserNotificationSettingValue
myVideoPublished: UserNotificationSettingValue myVideoPublished: UserNotificationSettingValue
myVideoImportFinished: UserNotificationSettingValue myVideoImportFinished: UserNotificationSettingValue

View File

@ -13,7 +13,9 @@ export enum UserNotificationType {
NEW_USER_REGISTRATION = 9, NEW_USER_REGISTRATION = 9,
NEW_FOLLOW = 10, NEW_FOLLOW = 10,
COMMENT_MENTION = 11 COMMENT_MENTION = 11,
VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12
} }
export interface VideoInfo { export interface VideoInfo {

View File

@ -1,19 +1,17 @@
import { Video } from '../video.model'
export enum VideoBlacklistType {
MANUAL = 1,
AUTO_BEFORE_PUBLISHED = 2
}
export interface VideoBlacklist { export interface VideoBlacklist {
id: number id: number
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
unfederated: boolean unfederated: boolean
reason?: string reason?: string
type: VideoBlacklistType
video: { video: Video
id: number
name: string
uuid: string
description: string
duration: number
views: number
likes: number
dislikes: number
nsfw: boolean
}
} }

View File

@ -112,6 +112,13 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
enabled: false enabled: false
} }
} }
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: false
}
}
} }
} }

View File

@ -18,7 +18,7 @@ function updateMyNotificationSettings (url: string, token: string, settings: Use
}) })
} }
function getUserNotifications ( async function getUserNotifications (
url: string, url: string,
token: string, token: string,
start: number, start: number,
@ -165,12 +165,15 @@ async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName
checkVideo(notification.video, videoName, videoUUID) checkVideo(notification.video, videoName, videoUUID)
checkActor(notification.video.channel) checkActor(notification.video.channel)
} else { } 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) { 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) await checkNotification(base, notificationChecker, emailFinder, type)
@ -387,6 +390,31 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
await checkNotification(base, notificationChecker, emailFinder, type) 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 ( async function checkNewBlacklistOnMyVideo (
base: CheckerBaseParams, base: CheckerBaseParams,
videoUUID: string, videoUUID: string,
@ -431,6 +459,7 @@ export {
checkCommentMention, checkCommentMention,
updateMyNotificationSettings, updateMyNotificationSettings,
checkNewVideoAbuseForModerators, checkNewVideoAbuseForModerators,
checkVideoAutoBlacklistForModerators,
getUserNotifications, getUserNotifications,
markAsReadNotifications, markAsReadNotifications,
getLastNotification getLastNotification

View File

@ -51,6 +51,18 @@ function getBlacklistedVideosList (url: string, token: string, specialStatus = 2
.expect('Content-Type', /json/) .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) { function getSortedBlacklistedVideosList (url: string, token: string, sort: string, specialStatus = 200) {
const path = '/api/v1/videos/blacklist/' const path = '/api/v1/videos/blacklist/'
@ -69,6 +81,7 @@ export {
addVideoToBlacklist, addVideoToBlacklist,
removeVideoFromBlacklist, removeVideoFromBlacklist,
getBlacklistedVideosList, getBlacklistedVideosList,
getBlacklistedVideosListWithTypeFilter,
getSortedBlacklistedVideosList, getSortedBlacklistedVideosList,
updateVideoBlacklist updateVideoBlacklist
} }

View File

@ -1,6 +1,6 @@
import * as request from 'supertest' 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' const path = '/api/v1/videos/' + videoId + '/give-ownership'
return request(url) return request(url)
@ -8,7 +8,7 @@ function changeVideoOwnership (url: string, token: string, videoId: number | str
.set('Accept', 'application/json') .set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token) .set('Authorization', 'Bearer ' + token)
.send({ username }) .send({ username })
.expect(204) .expect(expectedStatus)
} }
function getVideoChangeOwnershipList (url: string, token: string) { function getVideoChangeOwnershipList (url: string, token: string) {