aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/admin.module.ts8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html17
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts7
-rw-r--r--client/src/app/+admin/moderation/index.ts1
-rw-r--r--client/src/app/+admin/moderation/moderation.component.html4
-rw-r--r--client/src/app/+admin/moderation/moderation.component.ts11
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts17
-rw-r--r--client/src/app/+admin/moderation/video-auto-blacklist-list/index.ts1
-rw-r--r--client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html49
-rw-r--r--client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.scss94
-rw-r--r--client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts100
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts13
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts3
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss1
-rw-r--r--client/src/app/core/server/server.service.ts7
-rw-r--r--client/src/app/shared/users/user-notification.model.ts6
-rw-r--r--client/src/app/shared/users/user-notifications.component.html8
-rw-r--r--client/src/app/shared/video-blacklist/video-blacklist.service.ts51
-rw-r--r--config/default.yaml6
-rw-r--r--config/production.yaml.example6
-rw-r--r--server/controllers/api/config.ts14
-rw-r--r--server/controllers/api/users/my-notifications.ts1
-rw-r--r--server/controllers/api/videos/blacklist.ts25
-rw-r--r--server/controllers/api/videos/import.ts10
-rw-r--r--server/controllers/api/videos/index.ts18
-rw-r--r--server/helpers/custom-validators/video-blacklist.ts7
-rw-r--r--server/helpers/video.ts3
-rw-r--r--server/initializers/checker-before-init.ts2
-rw-r--r--server/initializers/constants.ts9
-rw-r--r--server/initializers/migrations/0350-video-blacklist-type.ts64
-rw-r--r--server/lib/activitypub/videos.ts2
-rw-r--r--server/lib/emailer.ts23
-rw-r--r--server/lib/job-queue/handlers/video-import.ts7
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts12
-rw-r--r--server/lib/notifier.ts63
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts2
-rw-r--r--server/lib/user.ts1
-rw-r--r--server/lib/video-blacklist.ts31
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts25
-rw-r--r--server/models/account/user-notification-setting.ts10
-rw-r--r--server/models/video/schedule-video-update.ts3
-rw-r--r--server/models/video/video-blacklist.ts56
-rw-r--r--server/tests/api/check-params/config.ts7
-rw-r--r--server/tests/api/check-params/user-notifications.ts1
-rw-r--r--server/tests/api/check-params/video-blacklist.ts11
-rw-r--r--server/tests/api/check-params/videos.ts3
-rw-r--r--server/tests/api/server/config.ts9
-rw-r--r--server/tests/api/users/user-notifications.ts191
-rw-r--r--server/tests/api/videos/video-blacklist.ts25
-rw-r--r--shared/models/server/custom-config.model.ts9
-rw-r--r--shared/models/server/server-config.model.ts8
-rw-r--r--shared/models/users/user-notification-setting.model.ts1
-rw-r--r--shared/models/users/user-notification.model.ts4
-rw-r--r--shared/models/videos/blacklist/video-blacklist.model.ts20
-rw-r--r--shared/utils/server/config.ts7
-rw-r--r--shared/utils/users/user-notifications.ts35
-rw-r--r--shared/utils/videos/video-blacklist.ts13
-rw-r--r--shared/utils/videos/video-change-ownership.ts4
58 files changed, 1047 insertions, 99 deletions
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index f7f347105..282d59634 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -11,7 +11,12 @@ import { JobsComponent } from './jobs/job.component'
11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' 11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
12import { JobService } from './jobs/shared/job.service' 12import { JobService } from './jobs/shared/job.service'
13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users' 13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' 14import {
15 ModerationCommentModalComponent,
16 VideoAbuseListComponent,
17 VideoBlacklistListComponent,
18 VideoAutoBlacklistListComponent
19} from './moderation'
15import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 20import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 21import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 22import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
@@ -42,6 +47,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
42 ModerationComponent, 47 ModerationComponent,
43 VideoBlacklistListComponent, 48 VideoBlacklistListComponent,
44 VideoAbuseListComponent, 49 VideoAbuseListComponent,
50 VideoAutoBlacklistListComponent,
45 ModerationCommentModalComponent, 51 ModerationCommentModalComponent,
46 InstanceServerBlocklistComponent, 52 InstanceServerBlocklistComponent,
47 InstanceAccountBlocklistComponent, 53 InstanceAccountBlocklistComponent,
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 6b654c67d..00a0d98f8 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -161,6 +161,23 @@
161 </ng-container> 161 </ng-container>
162 </ng-container> 162 </ng-container>
163 163
164 <div i18n class="inner-form-title">Auto-blacklist</div>
165
166 <ng-container formGroupName="autoBlacklist">
167 <ng-container formGroupName="videos">
168 <ng-container formGroupName="ofUsers">
169
170 <div class="form-group">
171 <my-peertube-checkbox
172 inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
173 i18n-labelText labelText="New videos of users automatically blacklisted enabled"
174 ></my-peertube-checkbox>
175 </div>
176
177 </ng-container>
178 </ng-container>
179 </ng-container>
180
164 <div i18n class="inner-form-title">Administrator</div> 181 <div i18n class="inner-form-title">Administrator</div>
165 182
166 <div class="form-group" formGroupName="admin"> 183 <div class="form-group" formGroupName="admin">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 45605e0fe..d8eb55da7 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -117,6 +117,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
117 threads: this.customConfigValidatorsService.TRANSCODING_THREADS, 117 threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
118 allowAdditionalExtensions: null, 118 allowAdditionalExtensions: null,
119 resolutions: {} 119 resolutions: {}
120 },
121 autoBlacklist: {
122 videos: {
123 ofUsers: {
124 enabled: null
125 }
126 }
120 } 127 }
121 } 128 }
122 129
diff --git a/client/src/app/+admin/moderation/index.ts b/client/src/app/+admin/moderation/index.ts
index 66e2c6a39..3c683a28c 100644
--- a/client/src/app/+admin/moderation/index.ts
+++ b/client/src/app/+admin/moderation/index.ts
@@ -1,4 +1,5 @@
1export * from './video-abuse-list' 1export * from './video-abuse-list'
2export * from './video-auto-blacklist-list'
2export * from './video-blacklist-list' 3export * from './video-blacklist-list'
3export * from './moderation.component' 4export * from './moderation.component'
4export * from './moderation.routes' 5export * from './moderation.routes'
diff --git a/client/src/app/+admin/moderation/moderation.component.html b/client/src/app/+admin/moderation/moderation.component.html
index 01457936c..b70027957 100644
--- a/client/src/app/+admin/moderation/moderation.component.html
+++ b/client/src/app/+admin/moderation/moderation.component.html
@@ -4,7 +4,9 @@
4 <div class="admin-sub-nav"> 4 <div class="admin-sub-nav">
5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a> 5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a>
6 6
7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a> 7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">{{ autoBlacklistVideosEnabled ? 'Manually blacklisted videos' : 'Blacklisted videos' }}</a>
8
9 <a *ngIf="autoBlacklistVideosEnabled && hasVideoBlacklistRight()" i18n routerLink="video-auto-blacklist/list" routerLinkActive="active">Auto-blacklisted videos</a>
8 10
9 <a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a> 11 <a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
10 12
diff --git a/client/src/app/+admin/moderation/moderation.component.ts b/client/src/app/+admin/moderation/moderation.component.ts
index 2b2618933..47154af3f 100644
--- a/client/src/app/+admin/moderation/moderation.component.ts
+++ b/client/src/app/+admin/moderation/moderation.component.ts
@@ -1,13 +1,20 @@
1import { Component } from '@angular/core' 1import { Component } from '@angular/core'
2import { UserRight } from '../../../../../shared' 2import { UserRight } from '../../../../../shared'
3import { AuthService } from '@app/core/auth/auth.service' 3import { AuthService, ServerService } from '@app/core'
4 4
5@Component({ 5@Component({
6 templateUrl: './moderation.component.html', 6 templateUrl: './moderation.component.html',
7 styleUrls: [ './moderation.component.scss' ] 7 styleUrls: [ './moderation.component.scss' ]
8}) 8})
9export class ModerationComponent { 9export class ModerationComponent {
10 constructor (private auth: AuthService) {} 10 autoBlacklistVideosEnabled: boolean
11
12 constructor (
13 private auth: AuthService,
14 private serverService: ServerService
15 ) {
16 this.autoBlacklistVideosEnabled = this.serverService.getConfig().autoBlacklist.videos.ofUsers.enabled
17 }
11 18
12 hasVideoAbusesRight () { 19 hasVideoAbusesRight () {
13 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) 20 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index 6f6dde290..a024f2bee 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -3,6 +3,7 @@ import { UserRight } from '../../../../../shared'
3import { UserRightGuard } from '@app/core' 3import { UserRightGuard } from '@app/core'
4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' 4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list' 5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
6import { VideoAutoBlacklistListComponent } from '@app/+admin/moderation/video-auto-blacklist-list'
6import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 7import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
7import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' 8import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
8 9
@@ -27,6 +28,11 @@ export const ModerationRoutes: Routes = [
27 pathMatch: 'full' 28 pathMatch: 'full'
28 }, 29 },
29 { 30 {
31 path: 'video-auto-blacklist',
32 redirectTo: 'video-auto-blacklist/list',
33 pathMatch: 'full'
34 },
35 {
30 path: 'video-abuses/list', 36 path: 'video-abuses/list',
31 component: VideoAbuseListComponent, 37 component: VideoAbuseListComponent,
32 canActivate: [ UserRightGuard ], 38 canActivate: [ UserRightGuard ],
@@ -38,6 +44,17 @@ export const ModerationRoutes: Routes = [
38 } 44 }
39 }, 45 },
40 { 46 {
47 path: 'video-auto-blacklist/list',
48 component: VideoAutoBlacklistListComponent,
49 canActivate: [ UserRightGuard ],
50 data: {
51 userRight: UserRight.MANAGE_VIDEO_BLACKLIST,
52 meta: {
53 title: 'Auto-blacklisted videos'
54 }
55 }
56 },
57 {
41 path: 'video-blacklist/list', 58 path: 'video-blacklist/list',
42 component: VideoBlacklistListComponent, 59 component: VideoBlacklistListComponent,
43 canActivate: [ UserRightGuard ], 60 canActivate: [ UserRightGuard ],
diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/index.ts b/client/src/app/+admin/moderation/video-auto-blacklist-list/index.ts
new file mode 100644
index 000000000..e3522f68c
--- /dev/null
+++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/index.ts
@@ -0,0 +1 @@
export * from './video-auto-blacklist-list.component'
diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html
new file mode 100644
index 000000000..fe579ffd7
--- /dev/null
+++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html
@@ -0,0 +1,49 @@
1<div i18n *ngIf="pagination.totalItems === 0">No results.</div>
2<div
3 myInfiniteScroller
4 [pageHeight]="pageHeight"
5 (nearOfTop)="onNearOfTop()"
6 (nearOfBottom)="onNearOfBottom()"
7 (pageChanged)="onPageChanged($event)"
8 class="videos" #videosElement
9>
10 <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
11 <div class="video" *ngFor="let video of videos; let j = index">
12 <div class="checkbox-container">
13 <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
14 </div>
15 <my-video-thumbnail [video]="video"></my-video-thumbnail>
16
17 <div class="video-info">
18 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
19 <div>{{ video.account.displayName }}</div>
20 <div>{{ video.publishedAt | myFromNow }}</div>
21 <div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
22 <div><span i18n>Sensitve: </span><span> {{ video.nsfw }}</span></div>
23 </div>
24
25 <!-- Display only once -->
26 <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
27 <div class="action-selection-mode-child">
28 <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
29 Cancel
30 </span>
31
32 <span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()">
33 <my-global-icon iconName="tick"></my-global-icon>
34 <ng-container i18n>Unblacklist</ng-container>
35 </span>
36 </div>
37 </div>
38
39 <div class="video-buttons" *ngIf="isInSelectionMode() === false">
40 <my-button
41 i18n-label
42 label="Unblacklist"
43 icon="tick"
44 (click)="removeVideoFromBlacklist(video)"
45 ></my-button>
46 </div>
47 </div>
48
49</div> \ No newline at end of file
diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.scss b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.scss
new file mode 100644
index 000000000..a73c17eb9
--- /dev/null
+++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.scss
@@ -0,0 +1,94 @@
1@import '_variables';
2@import '_mixins';
3
4.action-selection-mode {
5 width: 194px;
6 display: flex;
7 justify-content: flex-end;
8
9 .action-selection-mode-child {
10 position: fixed;
11
12 .action-button {
13 display: inline-block;
14 }
15
16 .action-button-cancel-selection {
17 @include peertube-button;
18 @include grey-button;
19
20 margin-right: 10px;
21 }
22
23 .action-button-unblacklist-selection {
24 @include peertube-button;
25 @include orange-button;
26 @include button-with-icon(21px);
27
28 my-global-icon {
29 @include apply-svg-color(#fff);
30 }
31 }
32 }
33}
34
35.video {
36 @include row-blocks;
37
38 &:first-child {
39 margin-top: 47px;
40 }
41
42 .checkbox-container {
43 display: flex;
44 align-items: center;
45 margin-right: 20px;
46 margin-left: 12px;
47 }
48
49 my-video-thumbnail {
50 margin-right: 10px;
51 }
52
53 .video-info {
54 flex-grow: 1;
55
56 .video-info-name {
57 @include disable-default-a-behaviour;
58
59 color: var(--mainForegroundColor);
60 display: block;
61 width: fit-content;
62 font-size: 16px;
63 font-weight: $font-semibold;
64 }
65 }
66
67 .video-buttons {
68 min-width: 190px;
69 }
70}
71
72@media screen and (max-width: $small-view) {
73 .video {
74 flex-direction: column;
75 height: auto;
76 text-align: center;
77
78 .video-info-name {
79 margin: auto;
80 }
81
82 input[type=checkbox] {
83 display: none;
84 }
85
86 my-video-thumbnail {
87 margin-right: 0;
88 }
89
90 .video-buttons {
91 margin-top: 10px;
92 }
93 }
94}
diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts
new file mode 100644
index 000000000..b79f574c9
--- /dev/null
+++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts
@@ -0,0 +1,100 @@
1import { Component, OnInit, OnDestroy } from '@angular/core'
2import { Location } from '@angular/common'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { Router, ActivatedRoute } from '@angular/router'
5import { AbstractVideoList } from '@app/shared/video/abstract-video-list'
6import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
7import { Notifier, AuthService } from '@app/core'
8import { Video } from '@shared/models'
9import { VideoBlacklistService } from '@app/shared'
10import { immutableAssign } from '@app/shared/misc/utils'
11import { ScreenService } from '@app/shared/misc/screen.service'
12
13@Component({
14 selector: 'my-video-auto-blacklist-list',
15 templateUrl: './video-auto-blacklist-list.component.html',
16 styleUrls: [ './video-auto-blacklist-list.component.scss' ]
17})
18export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy {
19 titlePage: string
20 currentRoute = '/admin/moderation/video-auto-blacklist/list'
21 checkedVideos: { [ id: number ]: boolean } = {}
22 pagination: ComponentPagination = {
23 currentPage: 1,
24 itemsPerPage: 5,
25 totalItems: null
26 }
27
28 protected baseVideoWidth = -1
29 protected baseVideoHeight = 155
30
31 constructor (
32 protected router: Router,
33 protected route: ActivatedRoute,
34 protected i18n: I18n,
35 protected notifier: Notifier,
36 protected location: Location,
37 protected authService: AuthService,
38 protected screenService: ScreenService,
39 private videoBlacklistService: VideoBlacklistService,
40 ) {
41 super()
42
43 this.titlePage = this.i18n('Auto-blacklisted videos')
44 }
45
46 ngOnInit () {
47 super.ngOnInit()
48 }
49
50 ngOnDestroy () {
51 super.ngOnDestroy()
52 }
53
54 abortSelectionMode () {
55 this.checkedVideos = {}
56 }
57
58 isInSelectionMode () {
59 return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
60 }
61
62 getVideosObservable (page: number) {
63 const newPagination = immutableAssign(this.pagination, { currentPage: page })
64
65 return this.videoBlacklistService.getAutoBlacklistedAsVideoList(newPagination)
66 }
67
68 generateSyndicationList () {
69 throw new Error('Method not implemented.')
70 }
71
72 removeVideoFromBlacklist (entry: Video) {
73 this.videoBlacklistService.removeVideoFromBlacklist(entry.id).subscribe(
74 () => {
75 this.notifier.success(this.i18n('Video {{name}} removed from blacklist.', { name: entry.name }))
76 this.reloadVideos()
77 },
78
79 error => this.notifier.error(error.message)
80 )
81 }
82
83 removeSelectedVideosFromBlacklist () {
84 const toReleaseVideosIds = Object.keys(this.checkedVideos)
85 .filter(k => this.checkedVideos[ k ] === true)
86 .map(k => parseInt(k, 10))
87
88 this.videoBlacklistService.removeVideoFromBlacklist(toReleaseVideosIds).subscribe(
89 () => {
90 this.notifier.success(this.i18n('{{num}} videos removed from blacklist.', { num: toReleaseVideosIds.length }))
91
92 this.abortSelectionMode()
93 this.reloadVideos()
94 },
95
96 error => this.notifier.error(error.message)
97 )
98 }
99
100}
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
index 5443d816d..f4bce7c48 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
@@ -1,9 +1,9 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { SortMeta } from 'primeng/components/common/sortmeta' 2import { SortMeta } from 'primeng/components/common/sortmeta'
3import { Notifier } from '@app/core' 3import { Notifier, ServerService } from '@app/core'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' 5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
6import { VideoBlacklist } from '../../../../../../shared' 6import { VideoBlacklist, VideoBlacklistType } from '../../../../../../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' 8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
9import { Video } from '../../../shared/video/video.model' 9import { Video } from '../../../shared/video/video.model'
@@ -20,11 +20,13 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
20 rowsPerPage = 10 20 rowsPerPage = 10
21 sort: SortMeta = { field: 'createdAt', order: 1 } 21 sort: SortMeta = { field: 'createdAt', order: 1 }
22 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 22 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
23 listBlacklistTypeFilter: VideoBlacklistType = undefined
23 24
24 videoBlacklistActions: DropdownAction<VideoBlacklist>[] = [] 25 videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
25 26
26 constructor ( 27 constructor (
27 private notifier: Notifier, 28 private notifier: Notifier,
29 private serverService: ServerService,
28 private confirmService: ConfirmService, 30 private confirmService: ConfirmService,
29 private videoBlacklistService: VideoBlacklistService, 31 private videoBlacklistService: VideoBlacklistService,
30 private markdownRenderer: MarkdownService, 32 private markdownRenderer: MarkdownService,
@@ -32,6 +34,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
32 ) { 34 ) {
33 super() 35 super()
34 36
37 // don't filter if auto-blacklist not enabled as this will be only list
38 if (this.serverService.getConfig().autoBlacklist.videos.ofUsers.enabled) {
39 this.listBlacklistTypeFilter = VideoBlacklistType.MANUAL
40 }
41
35 this.videoBlacklistActions = [ 42 this.videoBlacklistActions = [
36 { 43 {
37 label: this.i18n('Unblacklist'), 44 label: this.i18n('Unblacklist'),
@@ -77,7 +84,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
77 } 84 }
78 85
79 protected loadData () { 86 protected loadData () {
80 this.videoBlacklistService.listBlacklist(this.pagination, this.sort) 87 this.videoBlacklistService.listBlacklist(this.pagination, this.sort, this.listBlacklistTypeFilter)
81 .subscribe( 88 .subscribe(
82 async resultList => { 89 async resultList => {
83 this.totalRecords = resultList.total 90 this.totalRecords = resultList.total
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index 8d4f2c837..67ddf54da 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -31,10 +31,12 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
31 private serverService: ServerService, 31 private serverService: ServerService,
32 private notifier: Notifier 32 private notifier: Notifier
33 ) { 33 ) {
34
34 this.labelNotifications = { 35 this.labelNotifications = {
35 newVideoFromSubscription: this.i18n('New video from your subscriptions'), 36 newVideoFromSubscription: this.i18n('New video from your subscriptions'),
36 newCommentOnMyVideo: this.i18n('New comment on your video'), 37 newCommentOnMyVideo: this.i18n('New comment on your video'),
37 videoAbuseAsModerator: this.i18n('New video abuse'), 38 videoAbuseAsModerator: this.i18n('New video abuse'),
39 videoAutoBlacklistAsModerator: this.i18n('Video auto-blacklisted waiting review'),
38 blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'), 40 blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'),
39 myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'), 41 myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
40 myVideoImportFinished: this.i18n('Video import finished'), 42 myVideoImportFinished: this.i18n('Video import finished'),
@@ -46,6 +48,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
46 48
47 this.rightNotifications = { 49 this.rightNotifications = {
48 videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES, 50 videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
51 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
49 newUserRegistration: UserRight.MANAGE_USERS 52 newUserRegistration: UserRight.MANAGE_USERS
50 } 53 }
51 54
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
index f6b5faa45..d2df6f290 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
@@ -82,6 +82,7 @@
82 } 82 }
83 } 83 }
84 } 84 }
85
85 } 86 }
86 } 87 }
87 88
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index acaca8a01..b0c5d1130 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -98,6 +98,13 @@ export class ServerService {
98 videos: { 98 videos: {
99 intervalDays: 0 99 intervalDays: 0
100 } 100 }
101 },
102 autoBlacklist: {
103 videos: {
104 ofUsers: {
105 enabled: false
106 }
107 }
101 } 108 }
102 } 109 }
103 private videoCategories: Array<VideoConstant<number>> = [] 110 private videoCategories: Array<VideoConstant<number>> = []
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts
index 097830752..7d0eb5ea2 100644
--- a/client/src/app/shared/users/user-notification.model.ts
+++ b/client/src/app/shared/users/user-notification.model.ts
@@ -54,6 +54,7 @@ export class UserNotification implements UserNotificationServer {
54 videoUrl?: string 54 videoUrl?: string
55 commentUrl?: any[] 55 commentUrl?: any[]
56 videoAbuseUrl?: string 56 videoAbuseUrl?: string
57 videoAutoBlacklistUrl?: string
57 accountUrl?: string 58 accountUrl?: string
58 videoImportIdentifier?: string 59 videoImportIdentifier?: string
59 videoImportUrl?: string 60 videoImportUrl?: string
@@ -107,6 +108,11 @@ export class UserNotification implements UserNotificationServer {
107 this.videoUrl = this.buildVideoUrl(this.videoAbuse.video) 108 this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
108 break 109 break
109 110
111 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
112 this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
113 this.videoUrl = this.buildVideoUrl(this.video)
114 break
115
110 case UserNotificationType.BLACKLIST_ON_MY_VIDEO: 116 case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
111 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video) 117 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
112 break 118 break
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html
index 1c0af1bb0..6d2f2750e 100644
--- a/client/src/app/shared/users/user-notifications.component.html
+++ b/client/src/app/shared/users/user-notifications.component.html
@@ -36,6 +36,14 @@
36 </div> 36 </div>
37 </ng-container> 37 </ng-container>
38 38
39 <ng-container i18n *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS">
40 <my-global-icon iconName="no"></my-global-icon>
41
42 <div class="message">
43 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>
44 </div>
45 </ng-container>
46
39 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> 47 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
40 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 48 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
41 49
diff --git a/client/src/app/shared/video-blacklist/video-blacklist.service.ts b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
index 94e46d7c2..a9eab9b6f 100644
--- a/client/src/app/shared/video-blacklist/video-blacklist.service.ts
+++ b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
@@ -1,11 +1,13 @@
1import { catchError, map } from 'rxjs/operators' 1import { catchError, map, concatMap, toArray } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { SortMeta } from 'primeng/components/common/sortmeta' 4import { SortMeta } from 'primeng/components/common/sortmeta'
5import { Observable } from 'rxjs' 5import { from as observableFrom, Observable } from 'rxjs'
6import { VideoBlacklist, ResultList } from '../../../../../shared' 6import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared'
7import { Video } from '../video/video.model'
7import { environment } from '../../../environments/environment' 8import { environment } from '../../../environments/environment'
8import { RestExtractor, RestPagination, RestService } from '../rest' 9import { RestExtractor, RestPagination, RestService } from '../rest'
10import { ComponentPagination } from '../rest/component-pagination.model'
9 11
10@Injectable() 12@Injectable()
11export class VideoBlacklistService { 13export class VideoBlacklistService {
@@ -17,10 +19,14 @@ export class VideoBlacklistService {
17 private restExtractor: RestExtractor 19 private restExtractor: RestExtractor
18 ) {} 20 ) {}
19 21
20 listBlacklist (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoBlacklist>> { 22 listBlacklist (pagination: RestPagination, sort: SortMeta, type?: VideoBlacklistType): Observable<ResultList<VideoBlacklist>> {
21 let params = new HttpParams() 23 let params = new HttpParams()
22 params = this.restService.addRestGetParams(params, pagination, sort) 24 params = this.restService.addRestGetParams(params, pagination, sort)
23 25
26 if (type) {
27 params = params.set('type', type.toString())
28 }
29
24 return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params }) 30 return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
25 .pipe( 31 .pipe(
26 map(res => this.restExtractor.convertResultListDateToHuman(res)), 32 map(res => this.restExtractor.convertResultListDateToHuman(res)),
@@ -28,12 +34,37 @@ export class VideoBlacklistService {
28 ) 34 )
29 } 35 }
30 36
31 removeVideoFromBlacklist (videoId: number) { 37 getAutoBlacklistedAsVideoList (videoPagination: ComponentPagination): Observable<{ videos: Video[], totalVideos: number}> {
32 return this.authHttp.delete(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist') 38 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
33 .pipe( 39
34 map(this.restExtractor.extractDataBool), 40 // prioritize first created since waiting longest
35 catchError(res => this.restExtractor.handleError(res)) 41 const AUTO_BLACKLIST_SORT = 'createdAt'
36 ) 42
43 let params = new HttpParams()
44 params = this.restService.addRestGetParams(params, pagination, AUTO_BLACKLIST_SORT)
45
46 params = params.set('type', VideoBlacklistType.AUTO_BEFORE_PUBLISHED.toString())
47
48 return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
49 .pipe(
50 map(res => {
51 const videos = res.data.map(videoBlacklist => new Video(videoBlacklist.video))
52 const totalVideos = res.total
53 return { videos, totalVideos }
54 }),
55 catchError(res => this.restExtractor.handleError(res))
56 )
57 }
58
59 removeVideoFromBlacklist (videoIdArgs: number | number[]) {
60 const videoIds = Array.isArray(videoIdArgs) ? videoIdArgs : [ videoIdArgs ]
61
62 return observableFrom(videoIds)
63 .pipe(
64 concatMap(id => this.authHttp.delete(VideoBlacklistService.BASE_VIDEOS_URL + id + '/blacklist')),
65 toArray(),
66 catchError(err => this.restExtractor.handleError(err))
67 )
37 } 68 }
38 69
39 blacklistVideo (videoId: number, reason: string, unfederate: boolean) { 70 blacklistVideo (videoId: number, reason: string, unfederate: boolean) {
diff --git a/config/default.yaml b/config/default.yaml
index c5bf8e457..615910478 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -162,6 +162,12 @@ import:
162 torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) 162 torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
163 enabled: false 163 enabled: false
164 164
165auto_blacklist:
166 # New videos automatically blacklisted so moderators can review before publishing
167 videos:
168 of_users:
169 enabled: false
170
165instance: 171instance:
166 name: 'PeerTube' 172 name: 'PeerTube'
167 short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.' 173 short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 306e5576d..5299484a5 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -176,6 +176,12 @@ import:
176 torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) 176 torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
177 enabled: false 177 enabled: false
178 178
179auto_blacklist:
180 # New videos automatically blacklisted so moderators can review before publishing
181 videos:
182 of_users:
183 enabled: false
184
179# Instance settings 185# Instance settings
180instance: 186instance:
181 name: 'PeerTube' 187 name: 'PeerTube'
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 6497cda3c..bd0ba4f9d 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -94,6 +94,13 @@ async function getConfig (req: express.Request, res: express.Response) {
94 } 94 }
95 } 95 }
96 }, 96 },
97 autoBlacklist: {
98 videos: {
99 ofUsers: {
100 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
101 }
102 }
103 },
97 avatar: { 104 avatar: {
98 file: { 105 file: {
99 size: { 106 size: {
@@ -265,6 +272,13 @@ function customConfig (): CustomConfig {
265 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED 272 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
266 } 273 }
267 } 274 }
275 },
276 autoBlacklist: {
277 videos: {
278 ofUsers: {
279 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
280 }
281 }
268 } 282 }
269 } 283 }
270} 284}
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index bbafda5a6..4edad2a74 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -69,6 +69,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
69 newVideoFromSubscription: body.newVideoFromSubscription, 69 newVideoFromSubscription: body.newVideoFromSubscription,
70 newCommentOnMyVideo: body.newCommentOnMyVideo, 70 newCommentOnMyVideo: body.newCommentOnMyVideo,
71 videoAbuseAsModerator: body.videoAbuseAsModerator, 71 videoAbuseAsModerator: body.videoAbuseAsModerator,
72 videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
72 blacklistOnMyVideo: body.blacklistOnMyVideo, 73 blacklistOnMyVideo: body.blacklistOnMyVideo,
73 myVideoPublished: body.myVideoPublished, 74 myVideoPublished: body.myVideoPublished,
74 myVideoImportFinished: body.myVideoImportFinished, 75 myVideoImportFinished: body.myVideoImportFinished,
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index d0728eb59..27dcfb761 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight, VideoBlacklist, VideoBlacklistCreate } from '../../../../shared' 2import { VideoBlacklist, UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { 5import {
@@ -12,7 +12,8 @@ import {
12 setDefaultPagination, 12 setDefaultPagination,
13 videosBlacklistAddValidator, 13 videosBlacklistAddValidator,
14 videosBlacklistRemoveValidator, 14 videosBlacklistRemoveValidator,
15 videosBlacklistUpdateValidator 15 videosBlacklistUpdateValidator,
16 videosBlacklistFiltersValidator
16} from '../../../middlewares' 17} from '../../../middlewares'
17import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 18import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
18import { sequelizeTypescript } from '../../../initializers' 19import { sequelizeTypescript } from '../../../initializers'
@@ -36,6 +37,7 @@ blacklistRouter.get('/blacklist',
36 blacklistSortValidator, 37 blacklistSortValidator,
37 setBlacklistSort, 38 setBlacklistSort,
38 setDefaultPagination, 39 setDefaultPagination,
40 videosBlacklistFiltersValidator,
39 asyncMiddleware(listBlacklist) 41 asyncMiddleware(listBlacklist)
40) 42)
41 43
@@ -68,7 +70,8 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
68 const toCreate = { 70 const toCreate = {
69 videoId: videoInstance.id, 71 videoId: videoInstance.id,
70 unfederated: body.unfederate === true, 72 unfederated: body.unfederate === true,
71 reason: body.reason 73 reason: body.reason,
74 type: VideoBlacklistType.MANUAL
72 } 75 }
73 76
74 const blacklist = await VideoBlacklistModel.create(toCreate) 77 const blacklist = await VideoBlacklistModel.create(toCreate)
@@ -98,7 +101,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
98} 101}
99 102
100async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { 103async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
101 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort) 104 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.type)
102 105
103 return res.json(getFormattedObjects<VideoBlacklist, VideoBlacklistModel>(resultList.data, resultList.total)) 106 return res.json(getFormattedObjects<VideoBlacklist, VideoBlacklistModel>(resultList.data, resultList.total))
104} 107}
@@ -107,18 +110,30 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
107 const videoBlacklist = res.locals.videoBlacklist 110 const videoBlacklist = res.locals.videoBlacklist
108 const video = res.locals.video 111 const video = res.locals.video
109 112
110 await sequelizeTypescript.transaction(async t => { 113 const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
111 const unfederated = videoBlacklist.unfederated 114 const unfederated = videoBlacklist.unfederated
115 const videoBlacklistType = videoBlacklist.type
116
112 await videoBlacklist.destroy({ transaction: t }) 117 await videoBlacklist.destroy({ transaction: t })
113 118
114 // Re federate the video 119 // Re federate the video
115 if (unfederated === true) { 120 if (unfederated === true) {
116 await federateVideoIfNeeded(video, true, t) 121 await federateVideoIfNeeded(video, true, t)
117 } 122 }
123
124 return videoBlacklistType
118 }) 125 })
119 126
120 Notifier.Instance.notifyOnVideoUnblacklist(video) 127 Notifier.Instance.notifyOnVideoUnblacklist(video)
121 128
129 if (videoBlacklistType === VideoBlacklistType.AUTO_BEFORE_PUBLISHED) {
130 Notifier.Instance.notifyOnVideoPublishedAfterRemovedFromAutoBlacklist(video)
131
132 // Delete on object so new video notifications will send
133 delete video.VideoBlacklist
134 Notifier.Instance.notifyOnNewVideo(video)
135 }
136
122 logger.info('Video %s removed from blacklist.', res.locals.video.uuid) 137 logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
123 138
124 return res.type('json').status(204).end() 139 return res.type('json').status(204).end()
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index cbd2e8514..c234a1391 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -18,10 +18,12 @@ import { join } from 'path'
18import { isArray } from '../../../helpers/custom-validators/misc' 18import { isArray } from '../../../helpers/custom-validators/misc'
19import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' 19import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
20import { VideoChannelModel } from '../../../models/video/video-channel' 20import { VideoChannelModel } from '../../../models/video/video-channel'
21import { UserModel } from '../../../models/account/user'
21import * as Bluebird from 'bluebird' 22import * as Bluebird from 'bluebird'
22import * as parseTorrent from 'parse-torrent' 23import * as parseTorrent from 'parse-torrent'
23import { getSecureTorrentName } from '../../../helpers/utils' 24import { getSecureTorrentName } from '../../../helpers/utils'
24import { readFile, move } from 'fs-extra' 25import { readFile, move } from 'fs-extra'
26import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
25 27
26const auditLogger = auditLoggerFactory('video-imports') 28const auditLogger = auditLoggerFactory('video-imports')
27const videoImportsRouter = express.Router() 29const videoImportsRouter = express.Router()
@@ -85,7 +87,7 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
85 videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string 87 videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string
86 } 88 }
87 89
88 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) 90 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }, user)
89 91
90 await processThumbnail(req, video) 92 await processThumbnail(req, video)
91 await processPreview(req, video) 93 await processPreview(req, video)
@@ -128,7 +130,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
128 }).end() 130 }).end()
129 } 131 }
130 132
131 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) 133 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo, user)
132 134
133 const downloadThumbnail = !await processThumbnail(req, video) 135 const downloadThumbnail = !await processThumbnail(req, video)
134 const downloadPreview = !await processPreview(req, video) 136 const downloadPreview = !await processPreview(req, video)
@@ -156,7 +158,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
156 return res.json(videoImport.toFormattedJSON()).end() 158 return res.json(videoImport.toFormattedJSON()).end()
157} 159}
158 160
159function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo) { 161function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo, user: UserModel) {
160 const videoData = { 162 const videoData = {
161 name: body.name || importData.name || 'Unknown name', 163 name: body.name || importData.name || 'Unknown name',
162 remote: false, 164 remote: false,
@@ -218,6 +220,8 @@ function insertIntoDB (
218 const videoCreated = await video.save(sequelizeOptions) 220 const videoCreated = await video.save(sequelizeOptions)
219 videoCreated.VideoChannel = videoChannel 221 videoCreated.VideoChannel = videoChannel
220 222
223 await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
224
221 // Set tags to the video 225 // Set tags to the video
222 if (tags) { 226 if (tags) {
223 const tagInstances = await TagModel.findOrCreateTags(tags, t) 227 const tagInstances = await TagModel.findOrCreateTags(tags, t)
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 08bee97d3..393324819 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -6,6 +6,7 @@ import { processImage } from '../../../helpers/image-utils'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 7import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
8import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 8import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
9import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
9import { 10import {
10 CONFIG, 11 CONFIG,
11 MIMETYPES, 12 MIMETYPES,
@@ -193,6 +194,7 @@ async function addVideo (req: express.Request, res: express.Response) {
193 channelId: res.locals.videoChannel.id, 194 channelId: res.locals.videoChannel.id,
194 originallyPublishedAt: videoInfo.originallyPublishedAt 195 originallyPublishedAt: videoInfo.originallyPublishedAt
195 } 196 }
197
196 const video = new VideoModel(videoData) 198 const video = new VideoModel(videoData)
197 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 199 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
198 200
@@ -237,7 +239,7 @@ async function addVideo (req: express.Request, res: express.Response) {
237 // Create the torrent file 239 // Create the torrent file
238 await video.createTorrentAndSetInfoHash(videoFile) 240 await video.createTorrentAndSetInfoHash(videoFile)
239 241
240 const videoCreated = await sequelizeTypescript.transaction(async t => { 242 const { videoCreated, videoWasAutoBlacklisted } = await sequelizeTypescript.transaction(async t => {
241 const sequelizeOptions = { transaction: t } 243 const sequelizeOptions = { transaction: t }
242 244
243 const videoCreated = await video.save(sequelizeOptions) 245 const videoCreated = await video.save(sequelizeOptions)
@@ -266,15 +268,23 @@ async function addVideo (req: express.Request, res: express.Response) {
266 }, { transaction: t }) 268 }, { transaction: t })
267 } 269 }
268 270
269 await federateVideoIfNeeded(video, true, t) 271 const videoWasAutoBlacklisted = await autoBlacklistVideoIfNeeded(video, res.locals.oauth.token.User, t)
272
273 if (!videoWasAutoBlacklisted) {
274 await federateVideoIfNeeded(video, true, t)
275 }
270 276
271 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) 277 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
272 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 278 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
273 279
274 return videoCreated 280 return { videoCreated, videoWasAutoBlacklisted }
275 }) 281 })
276 282
277 Notifier.Instance.notifyOnNewVideo(videoCreated) 283 if (videoWasAutoBlacklisted) {
284 Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
285 } else {
286 Notifier.Instance.notifyOnNewVideo(videoCreated)
287 }
278 288
279 if (video.state === VideoState.TO_TRANSCODE) { 289 if (video.state === VideoState.TO_TRANSCODE) {
280 // Put uuid because we don't have id auto incremented for now 290 // Put uuid because we don't have id auto incremented for now
diff --git a/server/helpers/custom-validators/video-blacklist.ts b/server/helpers/custom-validators/video-blacklist.ts
index 25f908228..465f58a9c 100644
--- a/server/helpers/custom-validators/video-blacklist.ts
+++ b/server/helpers/custom-validators/video-blacklist.ts
@@ -1,7 +1,9 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { exists } from './misc'
3import { CONSTRAINTS_FIELDS } from '../../initializers' 4import { CONSTRAINTS_FIELDS } from '../../initializers'
4import { VideoBlacklistModel } from '../../models/video/video-blacklist' 5import { VideoBlacklistModel } from '../../models/video/video-blacklist'
6import { VideoBlacklistType } from '../../../shared/models/videos'
5 7
6const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST 8const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
7 9
@@ -24,9 +26,14 @@ async function doesVideoBlacklistExist (videoId: number, res: Response) {
24 return true 26 return true
25} 27}
26 28
29function isVideoBlacklistTypeValid (value: any) {
30 return exists(value) && validator.isInt('' + value) && VideoBlacklistType[value] !== undefined
31}
32
27// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
28 34
29export { 35export {
30 isVideoBlacklistReasonValid, 36 isVideoBlacklistReasonValid,
37 isVideoBlacklistTypeValid,
31 doesVideoBlacklistExist 38 doesVideoBlacklistExist
32} 39}
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index c90fe06c7..f6f51a297 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -1,4 +1,7 @@
1import { CONFIG } from '../initializers'
1import { VideoModel } from '../models/video/video' 2import { VideoModel } from '../models/video/video'
3import { UserRight } from '../../shared'
4import { UserModel } from '../models/account/user'
2 5
3type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' 6type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
4 7
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index ef12b3eea..e26f38564 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -20,7 +20,7 @@ function checkMissedConfig () {
20 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', 20 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
21 'redundancy.videos.strategies', 'redundancy.videos.check_interval', 21 'redundancy.videos.strategies', 'redundancy.videos.check_interval',
22 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 22 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions',
23 'import.videos.http.enabled', 'import.videos.torrent.enabled', 23 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'auto_blacklist.videos.of_users.enabled',
24 'trending.videos.interval_days', 24 'trending.videos.interval_days',
25 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 25 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
26 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt', 26 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index ff0ade17a..f59d3ef7a 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -18,7 +18,7 @@ let config: IConfig = require('config')
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
21const LAST_MIGRATION_VERSION = 345 21const LAST_MIGRATION_VERSION = 350
22 22
23// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
24 24
@@ -288,6 +288,13 @@ const CONFIG = {
288 } 288 }
289 } 289 }
290 }, 290 },
291 AUTO_BLACKLIST: {
292 VIDEOS: {
293 OF_USERS: {
294 get ENABLED () { return config.get<boolean>('auto_blacklist.videos.of_users.enabled') }
295 }
296 }
297 },
291 CACHE: { 298 CACHE: {
292 PREVIEWS: { 299 PREVIEWS: {
293 get SIZE () { return config.get<number>('cache.previews.size') } 300 get SIZE () { return config.get<number>('cache.previews.size') }
diff --git a/server/initializers/migrations/0350-video-blacklist-type.ts b/server/initializers/migrations/0350-video-blacklist-type.ts
new file mode 100644
index 000000000..4849020ef
--- /dev/null
+++ b/server/initializers/migrations/0350-video-blacklist-type.ts
@@ -0,0 +1,64 @@
1import * as Sequelize from 'sequelize'
2import { VideoBlacklistType } from '../../../shared/models/videos'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize,
8 db: any
9}): Promise<void> {
10 {
11 const data = {
12 type: Sequelize.INTEGER,
13 allowNull: true,
14 defaultValue: null
15 }
16
17 await utils.queryInterface.addColumn('videoBlacklist', 'type', data)
18 }
19
20 {
21 const query = 'UPDATE "videoBlacklist" SET "type" = ' + VideoBlacklistType.MANUAL
22 await utils.sequelize.query(query)
23 }
24
25 {
26 const data = {
27 type: Sequelize.INTEGER,
28 allowNull: false,
29 defaultValue: null
30 }
31 await utils.queryInterface.changeColumn('videoBlacklist', 'type', data)
32 }
33
34 {
35 const data = {
36 type: Sequelize.INTEGER,
37 defaultValue: null,
38 allowNull: true
39 }
40 await utils.queryInterface.addColumn('userNotificationSetting', 'videoAutoBlacklistAsModerator', data)
41 }
42
43 {
44 const query = 'UPDATE "userNotificationSetting" SET "videoAutoBlacklistAsModerator" = 3'
45 await utils.sequelize.query(query)
46 }
47
48 {
49 const data = {
50 type: Sequelize.INTEGER,
51 defaultValue: null,
52 allowNull: false
53 }
54 await utils.queryInterface.changeColumn('userNotificationSetting', 'videoAutoBlacklistAsModerator', data)
55 }
56}
57function down (options) {
58 throw new Error('Not implemented.')
59}
60
61export {
62 up,
63 down
64}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 2c932371b..d935e3f90 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -45,7 +45,7 @@ import { VideoShareModel } from '../../models/video/video-share'
45import { VideoCommentModel } from '../../models/video/video-comment' 45import { VideoCommentModel } from '../../models/video/video-comment'
46 46
47async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 47async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
48 // If the video is not private and published, we federate it 48 // If the video is not private and is published, we federate it
49 if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) { 49 if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
50 // Fetch more attributes that we will need to serialize in AP object 50 // Fetch more attributes that we will need to serialize in AP object
51 if (isArray(video.VideoCaptions) === false) { 51 if (isArray(video.VideoCaptions) === false) {
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 04e4b94b6..eec97c27e 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -250,6 +250,29 @@ class Emailer {
250 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 250 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
251 } 251 }
252 252
253 addVideoAutoBlacklistModeratorsNotification (to: string[], video: VideoModel) {
254 const VIDEO_AUTO_BLACKLIST_URL = CONFIG.WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
255 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
256
257 const text = `Hi,\n\n` +
258 `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
259 `\n\n` +
260 `You can view it and take appropriate action on ${videoUrl}` +
261 `\n\n` +
262 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
263 `\n\n` +
264 `Cheers,\n` +
265 `PeerTube.`
266
267 const emailPayload: EmailPayload = {
268 to,
269 subject: '[PeerTube] An auto-blacklisted video is awaiting review',
270 text
271 }
272
273 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
274 }
275
253 addNewUserRegistrationNotification (to: string[], user: UserModel) { 276 addNewUserRegistrationNotification (to: string[], user: UserModel) {
254 const text = `Hi,\n\n` + 277 const text = `Hi,\n\n` +
255 `User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` + 278 `User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` +
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index d96bfdf43..c5fc1061c 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -196,9 +196,14 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
196 return videoImportUpdated 196 return videoImportUpdated
197 }) 197 })
198 198
199 Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
200 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) 199 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
201 200
201 if (videoImportUpdated.Video.VideoBlacklist) {
202 Notifier.Instance.notifyOnVideoAutoBlacklist(videoImportUpdated.Video)
203 } else {
204 Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
205 }
206
202 // Create transcoding jobs? 207 // Create transcoding jobs?
203 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { 208 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
204 // Put uuid because we don't have id auto incremented for now 209 // Put uuid because we don't have id auto incremented for now
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index d9dad795e..581ec283e 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -85,10 +85,9 @@ async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodi
85 return { videoDatabase, videoPublished } 85 return { videoDatabase, videoPublished }
86 }) 86 })
87 87
88 // don't notify prior to scheduled video update 88 if (videoPublished) {
89 if (videoPublished && !videoDatabase.ScheduleVideoUpdate) {
90 Notifier.Instance.notifyOnNewVideo(videoDatabase) 89 Notifier.Instance.notifyOnNewVideo(videoDatabase)
91 Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) 90 Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
92 } 91 }
93 92
94 await createHlsJobIfEnabled(payload) 93 await createHlsJobIfEnabled(payload)
@@ -146,11 +145,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video
146 return { videoDatabase, videoPublished } 145 return { videoDatabase, videoPublished }
147 }) 146 })
148 147
149 // don't notify prior to scheduled video update 148 if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
150 if (!videoDatabase.ScheduleVideoUpdate) { 149 if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
151 if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
152 if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase)
153 }
154 150
155 await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })) 151 await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }))
156} 152}
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 501680f6b..9fe93ec0d 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -23,19 +23,35 @@ class Notifier {
23 private constructor () {} 23 private constructor () {}
24 24
25 notifyOnNewVideo (video: VideoModel): void { 25 notifyOnNewVideo (video: VideoModel): void {
26 // Only notify on public and published videos 26 // Only notify on public and published videos which are not blacklisted
27 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED) return 27 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.VideoBlacklist) return
28 28
29 this.notifySubscribersOfNewVideo(video) 29 this.notifySubscribersOfNewVideo(video)
30 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) 30 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
31 } 31 }
32 32
33 notifyOnPendingVideoPublished (video: VideoModel): void { 33 notifyOnVideoPublishedAfterTranscoding (video: VideoModel): void {
34 // Only notify on public videos that has been published while the user waited transcoding/scheduled update 34 // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
35 if (video.waitTranscoding === false && !video.ScheduleVideoUpdate) return 35 if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
36 36
37 this.notifyOwnedVideoHasBeenPublished(video) 37 this.notifyOwnedVideoHasBeenPublished(video)
38 .catch(err => logger.error('Cannot notify owner that its video %s has been published.', video.url, { err })) 38 .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
39 }
40
41 notifyOnVideoPublishedAfterScheduledUpdate (video: VideoModel): void {
42 // don't notify if video is still blacklisted or waiting for transcoding
43 if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
44
45 this.notifyOwnedVideoHasBeenPublished(video)
46 .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
47 }
48
49 notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: VideoModel): void {
50 // don't notify if video is still waiting for transcoding or scheduled update
51 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
52
53 this.notifyOwnedVideoHasBeenPublished(video)
54 .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
39 } 55 }
40 56
41 notifyOnNewComment (comment: VideoCommentModel): void { 57 notifyOnNewComment (comment: VideoCommentModel): void {
@@ -51,6 +67,11 @@ class Notifier {
51 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) 67 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
52 } 68 }
53 69
70 notifyOnVideoAutoBlacklist (video: VideoModel): void {
71 this.notifyModeratorsOfVideoAutoBlacklist(video)
72 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err }))
73 }
74
54 notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void { 75 notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void {
55 this.notifyVideoOwnerOfBlacklist(videoBlacklist) 76 this.notifyVideoOwnerOfBlacklist(videoBlacklist)
56 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) 77 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
@@ -58,7 +79,7 @@ class Notifier {
58 79
59 notifyOnVideoUnblacklist (video: VideoModel): void { 80 notifyOnVideoUnblacklist (video: VideoModel): void {
60 this.notifyVideoOwnerOfUnblacklist(video) 81 this.notifyVideoOwnerOfUnblacklist(video)
61 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err })) 82 .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
62 } 83 }
63 84
64 notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void { 85 notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void {
@@ -268,6 +289,34 @@ class Notifier {
268 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 289 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
269 } 290 }
270 291
292 private async notifyModeratorsOfVideoAutoBlacklist (video: VideoModel) {
293 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
294 if (moderators.length === 0) return
295
296 logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url)
297
298 function settingGetter (user: UserModel) {
299 return user.NotificationSetting.videoAutoBlacklistAsModerator
300 }
301 async function notificationCreator (user: UserModel) {
302
303 const notification = await UserNotificationModel.create({
304 type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
305 userId: user.id,
306 videoId: video.id
307 })
308 notification.Video = video
309
310 return notification
311 }
312
313 function emailSender (emails: string[]) {
314 return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video)
315 }
316
317 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
318 }
319
271 private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) { 320 private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) {
272 const user = await UserModel.loadByVideoId(videoBlacklist.videoId) 321 const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
273 if (!user) return 322 if (!user) return
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts
index 2618a5857..2179a2f26 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -57,7 +57,7 @@ export class UpdateVideosScheduler extends AbstractScheduler {
57 57
58 for (const v of publishedVideos) { 58 for (const v of publishedVideos) {
59 Notifier.Instance.notifyOnNewVideo(v) 59 Notifier.Instance.notifyOnNewVideo(v)
60 Notifier.Instance.notifyOnPendingVideoPublished(v) 60 Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(v)
61 } 61 }
62 } 62 }
63 63
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 02a84f15b..5588b0f76 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -106,6 +106,7 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr
106 myVideoImportFinished: UserNotificationSettingValue.WEB, 106 myVideoImportFinished: UserNotificationSettingValue.WEB,
107 myVideoPublished: UserNotificationSettingValue.WEB, 107 myVideoPublished: UserNotificationSettingValue.WEB,
108 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 108 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
109 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
109 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 110 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
110 newUserRegistration: UserNotificationSettingValue.WEB, 111 newUserRegistration: UserNotificationSettingValue.WEB,
111 commentMention: UserNotificationSettingValue.WEB, 112 commentMention: UserNotificationSettingValue.WEB,
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
new file mode 100644
index 000000000..dc4e0aed9
--- /dev/null
+++ b/server/lib/video-blacklist.ts
@@ -0,0 +1,31 @@
1import * as sequelize from 'sequelize'
2import { CONFIG } from '../initializers/constants'
3import { VideoBlacklistType, UserRight } from '../../shared/models'
4import { VideoBlacklistModel } from '../models/video/video-blacklist'
5import { UserModel } from '../models/account/user'
6import { VideoModel } from '../models/video/video'
7import { logger } from '../helpers/logger'
8
9async function autoBlacklistVideoIfNeeded (video: VideoModel, user: UserModel, transaction: sequelize.Transaction) {
10 if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED) return false
11
12 if (user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return false
13
14 const sequelizeOptions = { transaction }
15 const videoBlacklistToCreate = {
16 videoId: video.id,
17 unfederated: true,
18 reason: 'Auto-blacklisted. Moderator review required.',
19 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
20 }
21 await VideoBlacklistModel.create(videoBlacklistToCreate, sequelizeOptions)
22 logger.info('Video %s auto-blacklisted.', video.uuid)
23
24 return true
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 autoBlacklistVideoIfNeeded
31}
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index db318dcdb..1d7ddb2e3 100644
--- a/server/middlewares/validators/videos/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -1,10 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param, query } from 'express-validator/check'
3import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' 3import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { doesVideoExist } from '../../../helpers/custom-validators/videos' 4import { doesVideoExist } from '../../../helpers/custom-validators/videos'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { areValidationErrors } from '../utils' 6import { areValidationErrors } from '../utils'
7import { doesVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' 7import {
8 doesVideoBlacklistExist,
9 isVideoBlacklistReasonValid,
10 isVideoBlacklistTypeValid
11} from '../../../helpers/custom-validators/video-blacklist'
8 12
9const videosBlacklistRemoveValidator = [ 13const videosBlacklistRemoveValidator = [
10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -65,10 +69,25 @@ const videosBlacklistUpdateValidator = [
65 } 69 }
66] 70]
67 71
72const videosBlacklistFiltersValidator = [
73 query('type')
74 .optional()
75 .custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'),
76
77 (req: express.Request, res: express.Response, next: express.NextFunction) => {
78 logger.debug('Checking videos blacklist filters query', { parameters: req.query })
79
80 if (areValidationErrors(req, res)) return
81
82 return next()
83 }
84]
85
68// --------------------------------------------------------------------------- 86// ---------------------------------------------------------------------------
69 87
70export { 88export {
71 videosBlacklistAddValidator, 89 videosBlacklistAddValidator,
72 videosBlacklistRemoveValidator, 90 videosBlacklistRemoveValidator,
73 videosBlacklistUpdateValidator 91 videosBlacklistUpdateValidator,
92 videosBlacklistFiltersValidator
74} 93}
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index f1c3ac223..ba7f739b9 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -59,6 +59,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
59 @AllowNull(false) 59 @AllowNull(false)
60 @Default(null) 60 @Default(null)
61 @Is( 61 @Is(
62 'UserNotificationSettingVideoAutoBlacklistAsModerator',
63 value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator')
64 )
65 @Column
66 videoAutoBlacklistAsModerator: UserNotificationSettingValue
67
68 @AllowNull(false)
69 @Default(null)
70 @Is(
62 'UserNotificationSettingBlacklistOnMyVideo', 71 'UserNotificationSettingBlacklistOnMyVideo',
63 value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') 72 value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo')
64 ) 73 )
@@ -139,6 +148,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
139 newCommentOnMyVideo: this.newCommentOnMyVideo, 148 newCommentOnMyVideo: this.newCommentOnMyVideo,
140 newVideoFromSubscription: this.newVideoFromSubscription, 149 newVideoFromSubscription: this.newVideoFromSubscription,
141 videoAbuseAsModerator: this.videoAbuseAsModerator, 150 videoAbuseAsModerator: this.videoAbuseAsModerator,
151 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
142 blacklistOnMyVideo: this.blacklistOnMyVideo, 152 blacklistOnMyVideo: this.blacklistOnMyVideo,
143 myVideoPublished: this.myVideoPublished, 153 myVideoPublished: this.myVideoPublished,
144 myVideoImportFinished: this.myVideoImportFinished, 154 myVideoImportFinished: this.myVideoImportFinished,
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts
index 1e56562e1..abddc1111 100644
--- a/server/models/video/schedule-video-update.ts
+++ b/server/models/video/schedule-video-update.ts
@@ -72,7 +72,8 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
72 model: VideoModel.scope( 72 model: VideoModel.scope(
73 [ 73 [
74 VideoScopeNames.WITH_FILES, 74 VideoScopeNames.WITH_FILES,
75 VideoScopeNames.WITH_ACCOUNT_DETAILS 75 VideoScopeNames.WITH_ACCOUNT_DETAILS,
76 VideoScopeNames.WITH_BLACKLISTED
76 ] 77 ]
77 ) 78 )
78 } 79 }
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 3b567e488..86b1f6acb 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,8 +1,21 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import {
2 AllowNull,
3 BelongsTo,
4 Column,
5 CreatedAt,
6 DataType,
7 Default,
8 ForeignKey,
9 Is, Model,
10 Table,
11 UpdatedAt,
12 IFindOptions
13} from 'sequelize-typescript'
2import { getSortOnModel, SortType, throwIfNotValid } from '../utils' 14import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
3import { VideoModel } from './video' 15import { VideoModel } from './video'
4import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 16import { VideoChannelModel, ScopeNames as VideoChannelScopeNames } from './video-channel'
5import { VideoBlacklist } from '../../../shared/models/videos' 17import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
18import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
6import { CONSTRAINTS_FIELDS } from '../../initializers' 19import { CONSTRAINTS_FIELDS } from '../../initializers'
7 20
8@Table({ 21@Table({
@@ -25,6 +38,12 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
25 @Column 38 @Column
26 unfederated: boolean 39 unfederated: boolean
27 40
41 @AllowNull(false)
42 @Default(null)
43 @Is('VideoBlacklistType', value => throwIfNotValid(value, isVideoBlacklistTypeValid, 'type'))
44 @Column
45 type: VideoBlacklistType
46
28 @CreatedAt 47 @CreatedAt
29 createdAt: Date 48 createdAt: Date
30 49
@@ -43,19 +62,29 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
43 }) 62 })
44 Video: VideoModel 63 Video: VideoModel
45 64
46 static listForApi (start: number, count: number, sort: SortType) { 65 static listForApi (start: number, count: number, sort: SortType, type?: VideoBlacklistType) {
47 const query = { 66 const query: IFindOptions<VideoBlacklistModel> = {
48 offset: start, 67 offset: start,
49 limit: count, 68 limit: count,
50 order: getSortOnModel(sort.sortModel, sort.sortValue), 69 order: getSortOnModel(sort.sortModel, sort.sortValue),
51 include: [ 70 include: [
52 { 71 {
53 model: VideoModel, 72 model: VideoModel,
54 required: true 73 required: true,
74 include: [
75 {
76 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
77 required: true
78 }
79 ]
55 } 80 }
56 ] 81 ]
57 } 82 }
58 83
84 if (type) {
85 query.where = { type }
86 }
87
59 return VideoBlacklistModel.findAndCountAll(query) 88 return VideoBlacklistModel.findAndCountAll(query)
60 .then(({ rows, count }) => { 89 .then(({ rows, count }) => {
61 return { 90 return {
@@ -76,26 +105,15 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
76 } 105 }
77 106
78 toFormattedJSON (): VideoBlacklist { 107 toFormattedJSON (): VideoBlacklist {
79 const video = this.Video
80
81 return { 108 return {
82 id: this.id, 109 id: this.id,
83 createdAt: this.createdAt, 110 createdAt: this.createdAt,
84 updatedAt: this.updatedAt, 111 updatedAt: this.updatedAt,
85 reason: this.reason, 112 reason: this.reason,
86 unfederated: this.unfederated, 113 unfederated: this.unfederated,
114 type: this.type,
87 115
88 video: { 116 video: this.Video.toFormattedJSON()
89 id: video.id,
90 name: video.name,
91 uuid: video.uuid,
92 description: video.description,
93 duration: video.duration,
94 views: video.views,
95 likes: video.likes,
96 dislikes: video.dislikes,
97 nsfw: video.nsfw
98 }
99 } 117 }
100 } 118 }
101} 119}
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index c6b460f23..0b333e2f4 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -80,6 +80,13 @@ describe('Test config API validators', function () {
80 enabled: false 80 enabled: false
81 } 81 }
82 } 82 }
83 },
84 autoBlacklist: {
85 videos: {
86 ofUsers: {
87 enabled: false
88 }
89 }
83 } 90 }
84 } 91 }
85 92
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 714f481e9..36eaceac7 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -168,6 +168,7 @@ describe('Test user notifications API validators', function () {
168 newVideoFromSubscription: UserNotificationSettingValue.WEB, 168 newVideoFromSubscription: UserNotificationSettingValue.WEB,
169 newCommentOnMyVideo: UserNotificationSettingValue.WEB, 169 newCommentOnMyVideo: UserNotificationSettingValue.WEB,
170 videoAbuseAsModerator: UserNotificationSettingValue.WEB, 170 videoAbuseAsModerator: UserNotificationSettingValue.WEB,
171 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
171 blacklistOnMyVideo: UserNotificationSettingValue.WEB, 172 blacklistOnMyVideo: UserNotificationSettingValue.WEB,
172 myVideoImportFinished: UserNotificationSettingValue.WEB, 173 myVideoImportFinished: UserNotificationSettingValue.WEB,
173 myVideoPublished: UserNotificationSettingValue.WEB, 174 myVideoPublished: UserNotificationSettingValue.WEB,
diff --git a/server/tests/api/check-params/video-blacklist.ts b/server/tests/api/check-params/video-blacklist.ts
index 6b82643f4..fc039e847 100644
--- a/server/tests/api/check-params/video-blacklist.ts
+++ b/server/tests/api/check-params/video-blacklist.ts
@@ -8,6 +8,7 @@ import {
8 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
9 flushTests, 9 flushTests,
10 getBlacklistedVideosList, 10 getBlacklistedVideosList,
11 getBlacklistedVideosListWithTypeFilter,
11 getVideo, 12 getVideo,
12 getVideoWithToken, 13 getVideoWithToken,
13 killallServers, 14 killallServers,
@@ -24,7 +25,7 @@ import {
24 checkBadSortPagination, 25 checkBadSortPagination,
25 checkBadStartPagination 26 checkBadStartPagination
26} from '../../../../shared/utils/requests/check-api-params' 27} from '../../../../shared/utils/requests/check-api-params'
27import { VideoDetails } from '../../../../shared/models/videos' 28import { VideoDetails, VideoBlacklistType } from '../../../../shared/models/videos'
28import { expect } from 'chai' 29import { expect } from 'chai'
29 30
30describe('Test video blacklist API validators', function () { 31describe('Test video blacklist API validators', function () {
@@ -238,6 +239,14 @@ describe('Test video blacklist API validators', function () {
238 it('Should fail with an incorrect sort', async function () { 239 it('Should fail with an incorrect sort', async function () {
239 await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken) 240 await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken)
240 }) 241 })
242
243 it('Should fail with an invalid type', async function () {
244 await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, 0, 400)
245 })
246
247 it('Should succeed with the correct parameters', async function () {
248 await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.MANUAL)
249 })
241 }) 250 })
242 251
243 after(async function () { 252 after(async function () {
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index 3eccaee44..5a013b890 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -7,7 +7,8 @@ import { join } from 'path'
7import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' 7import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
8import { 8import {
9 createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest, 9 createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest,
10 makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin 10 makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, uploadVideo,
11 runServer, ServerInfo, setAccessTokensToServers, userLogin, updateCustomSubConfig
11} from '../../../../shared/utils' 12} from '../../../../shared/utils'
12import { 13import {
13 checkBadCountPagination, 14 checkBadCountPagination,
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index 42927605d..b9f05e952 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -62,6 +62,7 @@ function checkInitialConfig (data: CustomConfig) {
62 62
63 expect(data.import.videos.http.enabled).to.be.true 63 expect(data.import.videos.http.enabled).to.be.true
64 expect(data.import.videos.torrent.enabled).to.be.true 64 expect(data.import.videos.torrent.enabled).to.be.true
65 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
65} 66}
66 67
67function checkUpdatedConfig (data: CustomConfig) { 68function checkUpdatedConfig (data: CustomConfig) {
@@ -103,6 +104,7 @@ function checkUpdatedConfig (data: CustomConfig) {
103 104
104 expect(data.import.videos.http.enabled).to.be.false 105 expect(data.import.videos.http.enabled).to.be.false
105 expect(data.import.videos.torrent.enabled).to.be.false 106 expect(data.import.videos.torrent.enabled).to.be.false
107 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
106} 108}
107 109
108describe('Test config', function () { 110describe('Test config', function () {
@@ -225,6 +227,13 @@ describe('Test config', function () {
225 enabled: false 227 enabled: false
226 } 228 }
227 } 229 }
230 },
231 autoBlacklist: {
232 videos: {
233 ofUsers: {
234 enabled: true
235 }
236 }
228 } 237 }
229 } 238 }
230 await updateCustomConfig(server.url, server.accessToken, newCustomConfig) 239 await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
diff --git a/server/tests/api/users/user-notifications.ts b/server/tests/api/users/user-notifications.ts
index d573bf024..1b66df79b 100644
--- a/server/tests/api/users/user-notifications.ts
+++ b/server/tests/api/users/user-notifications.ts
@@ -17,7 +17,9 @@ import {
17 updateVideo, 17 updateVideo,
18 updateVideoChannel, 18 updateVideoChannel,
19 userLogin, 19 userLogin,
20 wait 20 wait,
21 getCustomConfig,
22 updateCustomConfig
21} from '../../../../shared/utils' 23} from '../../../../shared/utils'
22import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index' 24import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index'
23import { setAccessTokensToServers } from '../../../../shared/utils/users/login' 25import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
@@ -31,6 +33,7 @@ import {
31 checkNewBlacklistOnMyVideo, 33 checkNewBlacklistOnMyVideo,
32 checkNewCommentOnMyVideo, 34 checkNewCommentOnMyVideo,
33 checkNewVideoAbuseForModerators, 35 checkNewVideoAbuseForModerators,
36 checkVideoAutoBlacklistForModerators,
34 checkNewVideoFromSubscription, 37 checkNewVideoFromSubscription,
35 checkUserRegistered, 38 checkUserRegistered,
36 checkVideoIsPublished, 39 checkVideoIsPublished,
@@ -54,6 +57,7 @@ import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../sha
54import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' 57import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
55import * as uuidv4 from 'uuid/v4' 58import * as uuidv4 from 'uuid/v4'
56import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist' 59import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist'
60import { CustomConfig } from '../../../../shared/models/server'
57 61
58const expect = chai.expect 62const expect = chai.expect
59 63
@@ -92,6 +96,7 @@ describe('Test users notifications', function () {
92 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 96 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
93 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 97 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
94 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 98 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
99 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
95 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 100 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
96 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 101 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
97 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 102 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
@@ -305,7 +310,7 @@ describe('Test users notifications', function () {
305 }) 310 })
306 311
307 it('Should send a new video notification after a video import', async function () { 312 it('Should send a new video notification after a video import', async function () {
308 this.timeout(30000) 313 this.timeout(100000)
309 314
310 const name = 'video import ' + uuidv4() 315 const name = 'video import ' + uuidv4()
311 316
@@ -907,6 +912,180 @@ describe('Test users notifications', function () {
907 }) 912 })
908 }) 913 })
909 914
915 describe('Video-related notifications when video auto-blacklist is enabled', function () {
916 let userBaseParams: CheckerBaseParams
917 let adminBaseParamsServer1: CheckerBaseParams
918 let adminBaseParamsServer2: CheckerBaseParams
919 let videoUUID: string
920 let videoName: string
921 let currentCustomConfig: CustomConfig
922
923 before(async () => {
924
925 adminBaseParamsServer1 = {
926 server: servers[0],
927 emails,
928 socketNotifications: adminNotifications,
929 token: servers[0].accessToken
930 }
931
932 adminBaseParamsServer2 = {
933 server: servers[1],
934 emails,
935 socketNotifications: adminNotificationsServer2,
936 token: servers[1].accessToken
937 }
938
939 userBaseParams = {
940 server: servers[0],
941 emails,
942 socketNotifications: userNotifications,
943 token: userAccessToken
944 }
945
946 const resCustomConfig = await getCustomConfig(servers[0].url, servers[0].accessToken)
947 currentCustomConfig = resCustomConfig.body
948 const autoBlacklistTestsCustomConfig = immutableAssign(currentCustomConfig, {
949 autoBlacklist: {
950 videos: {
951 ofUsers: {
952 enabled: true
953 }
954 }
955 }
956 })
957 // enable transcoding otherwise own publish notification after transcoding not expected
958 autoBlacklistTestsCustomConfig.transcoding.enabled = true
959 await updateCustomConfig(servers[0].url, servers[0].accessToken, autoBlacklistTestsCustomConfig)
960
961 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
962 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
963
964 })
965
966 it('Should send notification to moderators on new video with auto-blacklist', async function () {
967 this.timeout(20000)
968
969 videoName = 'video with auto-blacklist ' + uuidv4()
970 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName })
971 videoUUID = resVideo.body.video.uuid
972
973 await waitJobs(servers)
974 await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, videoUUID, videoName, 'presence')
975 })
976
977 it('Should not send video publish notification if auto-blacklisted', async function () {
978 await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'absence')
979 })
980
981 it('Should not send a local user subscription notification if auto-blacklisted', async function () {
982 await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'absence')
983 })
984
985 it('Should not send a remote user subscription notification if auto-blacklisted', async function () {
986 await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'absence')
987 })
988
989 it('Should send video published and unblacklist after video unblacklisted', async function () {
990 this.timeout(20000)
991
992 await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoUUID)
993
994 await waitJobs(servers)
995
996 // FIXME: Can't test as two notifications sent to same user and util only checks last one
997 // One notification might be better anyways
998 // await checkNewBlacklistOnMyVideo(userBaseParams, videoUUID, videoName, 'unblacklist')
999 // await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'presence')
1000 })
1001
1002 it('Should send a local user subscription notification after removed from blacklist', async function () {
1003 await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'presence')
1004 })
1005
1006 it('Should send a remote user subscription notification after removed from blacklist', async function () {
1007 await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'presence')
1008 })
1009
1010 it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () {
1011 this.timeout(20000)
1012
1013 let updateAt = new Date(new Date().getTime() + 100000)
1014
1015 const name = 'video with auto-blacklist and future schedule ' + uuidv4()
1016
1017 const data = {
1018 name,
1019 privacy: VideoPrivacy.PRIVATE,
1020 scheduleUpdate: {
1021 updateAt: updateAt.toISOString(),
1022 privacy: VideoPrivacy.PUBLIC
1023 }
1024 }
1025
1026 const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
1027 const uuid = resVideo.body.video.uuid
1028
1029 await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid)
1030
1031 await waitJobs(servers)
1032 await checkNewBlacklistOnMyVideo(userBaseParams, uuid, name, 'unblacklist')
1033
1034 // FIXME: Can't test absence as two notifications sent to same user and util only checks last one
1035 // One notification might be better anyways
1036 // await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
1037
1038 await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
1039 await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
1040 })
1041
1042 it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () {
1043 this.timeout(20000)
1044
1045 // In 2 seconds
1046 let updateAt = new Date(new Date().getTime() + 2000)
1047
1048 const name = 'video with schedule done and still auto-blacklisted ' + uuidv4()
1049
1050 const data = {
1051 name,
1052 privacy: VideoPrivacy.PRIVATE,
1053 scheduleUpdate: {
1054 updateAt: updateAt.toISOString(),
1055 privacy: VideoPrivacy.PUBLIC
1056 }
1057 }
1058
1059 const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
1060 const uuid = resVideo.body.video.uuid
1061
1062 await wait(6000)
1063 await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
1064 await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
1065 await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
1066 })
1067
1068 it('Should not send a notification to moderators on new video without auto-blacklist', async function () {
1069 this.timeout(20000)
1070
1071 const name = 'video without auto-blacklist ' + uuidv4()
1072
1073 // admin with blacklist right will not be auto-blacklisted
1074 const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name })
1075 const uuid = resVideo.body.video.uuid
1076
1077 await waitJobs(servers)
1078 await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, uuid, name, 'absence')
1079 })
1080
1081 after(async () => {
1082 await updateCustomConfig(servers[0].url, servers[0].accessToken, currentCustomConfig)
1083
1084 await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
1085 await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
1086 })
1087 })
1088
910 describe('Mark as read', function () { 1089 describe('Mark as read', function () {
911 it('Should mark as read some notifications', async function () { 1090 it('Should mark as read some notifications', async function () {
912 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3) 1091 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3)
@@ -968,7 +1147,7 @@ describe('Test users notifications', function () {
968 }) 1147 })
969 1148
970 it('Should not have notifications', async function () { 1149 it('Should not have notifications', async function () {
971 this.timeout(10000) 1150 this.timeout(20000)
972 1151
973 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { 1152 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
974 newVideoFromSubscription: UserNotificationSettingValue.NONE 1153 newVideoFromSubscription: UserNotificationSettingValue.NONE
@@ -987,7 +1166,7 @@ describe('Test users notifications', function () {
987 }) 1166 })
988 1167
989 it('Should only have web notifications', async function () { 1168 it('Should only have web notifications', async function () {
990 this.timeout(10000) 1169 this.timeout(20000)
991 1170
992 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { 1171 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
993 newVideoFromSubscription: UserNotificationSettingValue.WEB 1172 newVideoFromSubscription: UserNotificationSettingValue.WEB
@@ -1013,7 +1192,7 @@ describe('Test users notifications', function () {
1013 }) 1192 })
1014 1193
1015 it('Should only have mail notifications', async function () { 1194 it('Should only have mail notifications', async function () {
1016 this.timeout(10000) 1195 this.timeout(20000)
1017 1196
1018 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { 1197 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
1019 newVideoFromSubscription: UserNotificationSettingValue.EMAIL 1198 newVideoFromSubscription: UserNotificationSettingValue.EMAIL
@@ -1039,7 +1218,7 @@ describe('Test users notifications', function () {
1039 }) 1218 })
1040 1219
1041 it('Should have email and web notifications', async function () { 1220 it('Should have email and web notifications', async function () {
1042 this.timeout(10000) 1221 this.timeout(20000)
1043 1222
1044 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { 1223 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
1045 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL 1224 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
diff --git a/server/tests/api/videos/video-blacklist.ts b/server/tests/api/videos/video-blacklist.ts
index d39ad63b4..10b412a80 100644
--- a/server/tests/api/videos/video-blacklist.ts
+++ b/server/tests/api/videos/video-blacklist.ts
@@ -7,6 +7,7 @@ import {
7 addVideoToBlacklist, 7 addVideoToBlacklist,
8 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
9 getBlacklistedVideosList, 9 getBlacklistedVideosList,
10 getBlacklistedVideosListWithTypeFilter,
10 getMyVideos, 11 getMyVideos,
11 getSortedBlacklistedVideosList, 12 getSortedBlacklistedVideosList,
12 getVideosList, 13 getVideosList,
@@ -22,7 +23,7 @@ import {
22} from '../../../../shared/utils/index' 23} from '../../../../shared/utils/index'
23import { doubleFollow } from '../../../../shared/utils/server/follows' 24import { doubleFollow } from '../../../../shared/utils/server/follows'
24import { waitJobs } from '../../../../shared/utils/server/jobs' 25import { waitJobs } from '../../../../shared/utils/server/jobs'
25import { VideoBlacklist } from '../../../../shared/models/videos' 26import { VideoBlacklist, VideoBlacklistType } from '../../../../shared/models/videos'
26 27
27const expect = chai.expect 28const expect = chai.expect
28 29
@@ -101,7 +102,7 @@ describe('Test video blacklist management', function () {
101 }) 102 })
102 }) 103 })
103 104
104 describe('When listing blacklisted videos', function () { 105 describe('When listing manually blacklisted videos', function () {
105 it('Should display all the blacklisted videos', async function () { 106 it('Should display all the blacklisted videos', async function () {
106 const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken) 107 const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken)
107 108
@@ -117,6 +118,26 @@ describe('Test video blacklist management', function () {
117 } 118 }
118 }) 119 })
119 120
121 it('Should display all the blacklisted videos when applying manual type filter', async function () {
122 const res = await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.MANUAL)
123
124 expect(res.body.total).to.equal(2)
125
126 const blacklistedVideos = res.body.data
127 expect(blacklistedVideos).to.be.an('array')
128 expect(blacklistedVideos.length).to.equal(2)
129 })
130
131 it('Should display nothing when applying automatic type filter', async function () {
132 const res = await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.AUTO_BEFORE_PUBLISHED) // tslint:disable:max-line-length
133
134 expect(res.body.total).to.equal(0)
135
136 const blacklistedVideos = res.body.data
137 expect(blacklistedVideos).to.be.an('array')
138 expect(blacklistedVideos.length).to.equal(0)
139 })
140
120 it('Should get the correct sort when sorting by descending id', async function () { 141 it('Should get the correct sort when sorting by descending id', async function () {
121 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id') 142 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id')
122 expect(res.body.total).to.equal(2) 143 expect(res.body.total).to.equal(2)
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 20b261426..1607b40a8 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -77,4 +77,13 @@ export interface CustomConfig {
77 } 77 }
78 } 78 }
79 } 79 }
80
81 autoBlacklist: {
82 videos: {
83 ofUsers: {
84 enabled: boolean
85 }
86 }
87 }
88
80} 89}
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 0200d88ca..dcc45be8a 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -49,6 +49,14 @@ export interface ServerConfig {
49 } 49 }
50 } 50 }
51 51
52 autoBlacklist: {
53 videos: {
54 ofUsers: {
55 enabled: boolean
56 }
57 }
58 }
59
52 avatar: { 60 avatar: {
53 file: { 61 file: {
54 size: { 62 size: {
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts
index 531e12bba..57b33e4b8 100644
--- a/shared/models/users/user-notification-setting.model.ts
+++ b/shared/models/users/user-notification-setting.model.ts
@@ -8,6 +8,7 @@ export interface UserNotificationSetting {
8 newVideoFromSubscription: UserNotificationSettingValue 8 newVideoFromSubscription: UserNotificationSettingValue
9 newCommentOnMyVideo: UserNotificationSettingValue 9 newCommentOnMyVideo: UserNotificationSettingValue
10 videoAbuseAsModerator: UserNotificationSettingValue 10 videoAbuseAsModerator: UserNotificationSettingValue
11 videoAutoBlacklistAsModerator: UserNotificationSettingValue
11 blacklistOnMyVideo: UserNotificationSettingValue 12 blacklistOnMyVideo: UserNotificationSettingValue
12 myVideoPublished: UserNotificationSettingValue 13 myVideoPublished: UserNotificationSettingValue
13 myVideoImportFinished: UserNotificationSettingValue 14 myVideoImportFinished: UserNotificationSettingValue
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
index 186b62612..19892b61a 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -13,7 +13,9 @@ export enum UserNotificationType {
13 13
14 NEW_USER_REGISTRATION = 9, 14 NEW_USER_REGISTRATION = 9,
15 NEW_FOLLOW = 10, 15 NEW_FOLLOW = 10,
16 COMMENT_MENTION = 11 16 COMMENT_MENTION = 11,
17
18 VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12
17} 19}
18 20
19export interface VideoInfo { 21export interface VideoInfo {
diff --git a/shared/models/videos/blacklist/video-blacklist.model.ts b/shared/models/videos/blacklist/video-blacklist.model.ts
index 4bd976190..68d59e489 100644
--- a/shared/models/videos/blacklist/video-blacklist.model.ts
+++ b/shared/models/videos/blacklist/video-blacklist.model.ts
@@ -1,19 +1,17 @@
1import { Video } from '../video.model'
2
3export enum VideoBlacklistType {
4 MANUAL = 1,
5 AUTO_BEFORE_PUBLISHED = 2
6}
7
1export interface VideoBlacklist { 8export interface VideoBlacklist {
2 id: number 9 id: number
3 createdAt: Date 10 createdAt: Date
4 updatedAt: Date 11 updatedAt: Date
5 unfederated: boolean 12 unfederated: boolean
6 reason?: string 13 reason?: string
14 type: VideoBlacklistType
7 15
8 video: { 16 video: Video
9 id: number
10 name: string
11 uuid: string
12 description: string
13 duration: number
14 views: number
15 likes: number
16 dislikes: number
17 nsfw: boolean
18 }
19} 17}
diff --git a/shared/utils/server/config.ts b/shared/utils/server/config.ts
index 0e16af0f2..eaa493a93 100644
--- a/shared/utils/server/config.ts
+++ b/shared/utils/server/config.ts
@@ -112,6 +112,13 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
112 enabled: false 112 enabled: false
113 } 113 }
114 } 114 }
115 },
116 autoBlacklist: {
117 videos: {
118 ofUsers: {
119 enabled: false
120 }
121 }
115 } 122 }
116 } 123 }
117 124
diff --git a/shared/utils/users/user-notifications.ts b/shared/utils/users/user-notifications.ts
index c8ed7df30..e3a79f523 100644
--- a/shared/utils/users/user-notifications.ts
+++ b/shared/utils/users/user-notifications.ts
@@ -18,7 +18,7 @@ function updateMyNotificationSettings (url: string, token: string, settings: Use
18 }) 18 })
19} 19}
20 20
21function getUserNotifications ( 21async function getUserNotifications (
22 url: string, 22 url: string,
23 token: string, 23 token: string,
24 start: number, 24 start: number,
@@ -165,12 +165,15 @@ async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName
165 checkVideo(notification.video, videoName, videoUUID) 165 checkVideo(notification.video, videoName, videoUUID)
166 checkActor(notification.video.channel) 166 checkActor(notification.video.channel)
167 } else { 167 } else {
168 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) 168 expect(notification).to.satisfy((n: UserNotification) => {
169 return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
170 })
169 } 171 }
170 } 172 }
171 173
172 function emailFinder (email: object) { 174 function emailFinder (email: object) {
173 return email[ 'text' ].indexOf(videoUUID) !== -1 175 const text = email[ 'text' ]
176 return text.indexOf(videoUUID) !== -1 && text.indexOf('Your subscription') !== -1
174 } 177 }
175 178
176 await checkNotification(base, notificationChecker, emailFinder, type) 179 await checkNotification(base, notificationChecker, emailFinder, type)
@@ -387,6 +390,31 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
387 await checkNotification(base, notificationChecker, emailFinder, type) 390 await checkNotification(base, notificationChecker, emailFinder, type)
388} 391}
389 392
393async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
394 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
395
396 function notificationChecker (notification: UserNotification, type: CheckerType) {
397 if (type === 'presence') {
398 expect(notification).to.not.be.undefined
399 expect(notification.type).to.equal(notificationType)
400
401 expect(notification.video.id).to.be.a('number')
402 checkVideo(notification.video, videoName, videoUUID)
403 } else {
404 expect(notification).to.satisfy((n: UserNotification) => {
405 return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
406 })
407 }
408 }
409
410 function emailFinder (email: object) {
411 const text = email[ 'text' ]
412 return text.indexOf(videoUUID) !== -1 && email[ 'text' ].indexOf('video-auto-blacklist/list') !== -1
413 }
414
415 await checkNotification(base, notificationChecker, emailFinder, type)
416}
417
390async function checkNewBlacklistOnMyVideo ( 418async function checkNewBlacklistOnMyVideo (
391 base: CheckerBaseParams, 419 base: CheckerBaseParams,
392 videoUUID: string, 420 videoUUID: string,
@@ -431,6 +459,7 @@ export {
431 checkCommentMention, 459 checkCommentMention,
432 updateMyNotificationSettings, 460 updateMyNotificationSettings,
433 checkNewVideoAbuseForModerators, 461 checkNewVideoAbuseForModerators,
462 checkVideoAutoBlacklistForModerators,
434 getUserNotifications, 463 getUserNotifications,
435 markAsReadNotifications, 464 markAsReadNotifications,
436 getLastNotification 465 getLastNotification
diff --git a/shared/utils/videos/video-blacklist.ts b/shared/utils/videos/video-blacklist.ts
index f2ae0ed26..82d5b7e31 100644
--- a/shared/utils/videos/video-blacklist.ts
+++ b/shared/utils/videos/video-blacklist.ts
@@ -51,6 +51,18 @@ function getBlacklistedVideosList (url: string, token: string, specialStatus = 2
51 .expect('Content-Type', /json/) 51 .expect('Content-Type', /json/)
52} 52}
53 53
54function getBlacklistedVideosListWithTypeFilter (url: string, token: string, type: number, specialStatus = 200) {
55 const path = '/api/v1/videos/blacklist/'
56
57 return request(url)
58 .get(path)
59 .query({ sort: 'createdAt', type })
60 .set('Accept', 'application/json')
61 .set('Authorization', 'Bearer ' + token)
62 .expect(specialStatus)
63 .expect('Content-Type', /json/)
64}
65
54function getSortedBlacklistedVideosList (url: string, token: string, sort: string, specialStatus = 200) { 66function getSortedBlacklistedVideosList (url: string, token: string, sort: string, specialStatus = 200) {
55 const path = '/api/v1/videos/blacklist/' 67 const path = '/api/v1/videos/blacklist/'
56 68
@@ -69,6 +81,7 @@ export {
69 addVideoToBlacklist, 81 addVideoToBlacklist,
70 removeVideoFromBlacklist, 82 removeVideoFromBlacklist,
71 getBlacklistedVideosList, 83 getBlacklistedVideosList,
84 getBlacklistedVideosListWithTypeFilter,
72 getSortedBlacklistedVideosList, 85 getSortedBlacklistedVideosList,
73 updateVideoBlacklist 86 updateVideoBlacklist
74} 87}
diff --git a/shared/utils/videos/video-change-ownership.ts b/shared/utils/videos/video-change-ownership.ts
index f288692ea..371d02000 100644
--- a/shared/utils/videos/video-change-ownership.ts
+++ b/shared/utils/videos/video-change-ownership.ts
@@ -1,6 +1,6 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2 2
3function changeVideoOwnership (url: string, token: string, videoId: number | string, username) { 3function changeVideoOwnership (url: string, token: string, videoId: number | string, username, expectedStatus = 204) {
4 const path = '/api/v1/videos/' + videoId + '/give-ownership' 4 const path = '/api/v1/videos/' + videoId + '/give-ownership'
5 5
6 return request(url) 6 return request(url)
@@ -8,7 +8,7 @@ function changeVideoOwnership (url: string, token: string, videoId: number | str
8 .set('Accept', 'application/json') 8 .set('Accept', 'application/json')
9 .set('Authorization', 'Bearer ' + token) 9 .set('Authorization', 'Bearer ' + token)
10 .send({ username }) 10 .send({ username })
11 .expect(204) 11 .expect(expectedStatus)
12} 12}
13 13
14function getVideoChangeOwnershipList (url: string, token: string) { 14function getVideoChangeOwnershipList (url: string, token: string) {