aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html2
-rw-r--r--client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html39
-rw-r--r--client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.scss6
-rw-r--r--client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts18
-rw-r--r--client/src/app/shared/forms/form-validators/index.ts1
-rw-r--r--client/src/app/shared/forms/form-validators/video-blacklist-validators.service.ts19
-rw-r--r--client/src/app/shared/shared.module.ts5
-rw-r--r--client/src/app/shared/video-blacklist/video-blacklist.service.ts6
-rw-r--r--client/src/app/shared/video/video-details.model.ts2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.html31
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.scss6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.ts66
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html11
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss4
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts27
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts2
-rw-r--r--client/src/assets/images/global/delete-black.svg14
-rw-r--r--server/controllers/api/users.ts6
-rw-r--r--server/controllers/api/videos/blacklist.ts56
-rw-r--r--server/helpers/custom-validators/video-blacklist.ts32
-rw-r--r--server/initializers/constants.ts7
-rw-r--r--server/initializers/migrations/0255-video-blacklist-reason.ts25
-rw-r--r--server/lib/emailer.ts49
-rw-r--r--server/middlewares/validators/video-blacklist.ts55
-rw-r--r--server/models/video/video-blacklist.ts59
-rw-r--r--server/models/video/video.ts30
-rw-r--r--server/tests/api/check-params/video-blacklist.ts92
-rw-r--r--server/tests/api/server/email.ts51
-rw-r--r--server/tests/api/videos/video-blacklist-management.ts78
-rw-r--r--server/tests/utils/videos/video-blacklist.ts16
-rw-r--r--shared/models/videos/index.ts2
-rw-r--r--shared/models/videos/video-blacklist-create.model.ts3
-rw-r--r--shared/models/videos/video-blacklist-update.model.ts3
-rw-r--r--shared/models/videos/video-blacklist.model.ts22
-rw-r--r--shared/models/videos/video.model.ts3
35 files changed, 687 insertions, 161 deletions
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
index 08501d872..aa0e18c70 100644
--- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
@@ -9,7 +9,7 @@
9 <ng-template pTemplate="header"> 9 <ng-template pTemplate="header">
10 <tr> 10 <tr>
11 <th style="width: 40px"></th> 11 <th style="width: 40px"></th>
12 <th i18n style="width: 80px;">State</th> 12 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
13 <th i18n>Reason</th> 13 <th i18n>Reason</th>
14 <th i18n>Reporter</th> 14 <th i18n>Reporter</th>
15 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 15 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
diff --git a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
index 04f0e3b5c..78989dc58 100644
--- a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
@@ -4,30 +4,43 @@
4 4
5<p-table 5<p-table
6 [value]="blacklist" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 6 [value]="blacklist" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" 7 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
8> 8>
9 <ng-template pTemplate="header"> 9 <ng-template pTemplate="header">
10 <tr> 10 <tr>
11 <th i18n pSortableColumn="name">Name <p-sortIcon field="name"></p-sortIcon></th> 11 <th style="width: 40px"></th>
12 <th i18n>Description</th> 12 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
13 <th i18n pSortableColumn="views">Views <p-sortIcon field="views"></p-sortIcon></th>
14 <th i18n>NSFW</th> 13 <th i18n>NSFW</th>
15 <th i18n>UUID</th> 14 <th i18n>UUID</th>
16 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 15 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
17 <th></th> 16 <th style="width: 50px;"></th>
18 </tr> 17 </tr>
19 </ng-template> 18 </ng-template>
20 19
21 <ng-template pTemplate="body" let-videoBlacklist> 20 <ng-template pTemplate="body" let-videoBlacklist let-expanded="expanded">
22 <tr> 21 <tr>
23 <td>{{ videoBlacklist.name }}</td> 22 <td>
24 <td>{{ videoBlacklist.description }}</td> 23 <span *ngIf="videoBlacklist.reason" class="expander" [pRowToggler]="videoBlacklist">
25 <td>{{ videoBlacklist.views }}</td> 24 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
26 <td>{{ videoBlacklist.nsfw }}</td> 25 </span>
27 <td>{{ videoBlacklist.uuid }}</td> 26 </td>
27
28 <td>{{ videoBlacklist.video.name }}</td>
29 <td>{{ videoBlacklist.video.nsfw }}</td>
30 <td>{{ videoBlacklist.video.uuid }}</td>
28 <td>{{ videoBlacklist.createdAt }}</td> 31 <td>{{ videoBlacklist.createdAt }}</td>
32
29 <td class="action-cell"> 33 <td class="action-cell">
30 <my-delete-button i18n-label label="Unblacklist" (click)="removeVideoFromBlacklist(videoBlacklist)"></my-delete-button> 34 <my-action-dropdown i18n-label label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
35 </td>
36 </tr>
37 </ng-template>
38
39 <ng-template pTemplate="rowexpansion" let-videoBlacklist>
40 <tr class="blacklist-reason">
41 <td colspan="6">
42 <span i18n class="blacklist-reason-label">Blacklist reason:</span>
43 {{ videoBlacklist.reason }}
31 </td> 44 </td>
32 </tr> 45 </tr>
33 </ng-template> 46 </ng-template>
diff --git a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.scss b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.scss
new file mode 100644
index 000000000..5265536ca
--- /dev/null
+++ b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.scss
@@ -0,0 +1,6 @@
1@import '_variables';
2@import '_mixins';
3
4.blacklist-reason-label {
5 font-weight: $font-semibold;
6} \ No newline at end of file
diff --git a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts
index 143ec8406..00b0ac57e 100644
--- a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts
+++ b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts
@@ -5,11 +5,12 @@ import { ConfirmService } from '../../../core'
5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' 5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
6import { BlacklistedVideo } from '../../../../../../shared' 6import { BlacklistedVideo } from '../../../../../../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
8 9
9@Component({ 10@Component({
10 selector: 'my-video-blacklist-list', 11 selector: 'my-video-blacklist-list',
11 templateUrl: './video-blacklist-list.component.html', 12 templateUrl: './video-blacklist-list.component.html',
12 styleUrls: [] 13 styleUrls: [ './video-blacklist-list.component.scss' ]
13}) 14})
14export class VideoBlacklistListComponent extends RestTable implements OnInit { 15export class VideoBlacklistListComponent extends RestTable implements OnInit {
15 blacklist: BlacklistedVideo[] = [] 16 blacklist: BlacklistedVideo[] = []
@@ -18,6 +19,8 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
18 sort: SortMeta = { field: 'createdAt', order: 1 } 19 sort: SortMeta = { field: 'createdAt', order: 1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20 21
22 videoBlacklistActions: DropdownAction<BlacklistedVideo>[] = []
23
21 constructor ( 24 constructor (
22 private notificationsService: NotificationsService, 25 private notificationsService: NotificationsService,
23 private confirmService: ConfirmService, 26 private confirmService: ConfirmService,
@@ -25,6 +28,13 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
25 private i18n: I18n 28 private i18n: I18n
26 ) { 29 ) {
27 super() 30 super()
31
32 this.videoBlacklistActions = [
33 {
34 label: this.i18n('Unblacklist'),
35 handler: videoBlacklist => this.removeVideoFromBlacklist(videoBlacklist)
36 }
37 ]
28 } 38 }
29 39
30 ngOnInit () { 40 ngOnInit () {
@@ -33,17 +43,17 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
33 43
34 async removeVideoFromBlacklist (entry: BlacklistedVideo) { 44 async removeVideoFromBlacklist (entry: BlacklistedVideo) {
35 const confirmMessage = this.i18n( 45 const confirmMessage = this.i18n(
36 'Do you really want to remove this video from the blacklist ? It will be available again in the videos list.' 46 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
37 ) 47 )
38 48
39 const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist')) 49 const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist'))
40 if (res === false) return 50 if (res === false) return
41 51
42 this.videoBlacklistService.removeVideoFromBlacklist(entry.videoId).subscribe( 52 this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe(
43 () => { 53 () => {
44 this.notificationsService.success( 54 this.notificationsService.success(
45 this.i18n('Success'), 55 this.i18n('Success'),
46 this.i18n('Video {{name}} removed from the blacklist.', { name: entry.name }) 56 this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name })
47 ) 57 )
48 this.loadData() 58 this.loadData()
49 }, 59 },
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts
index 60d735ef7..9bc7615ca 100644
--- a/client/src/app/shared/forms/form-validators/index.ts
+++ b/client/src/app/shared/forms/form-validators/index.ts
@@ -5,6 +5,7 @@ export * from './login-validators.service'
5export * from './reset-password-validators.service' 5export * from './reset-password-validators.service'
6export * from './user-validators.service' 6export * from './user-validators.service'
7export * from './video-abuse-validators.service' 7export * from './video-abuse-validators.service'
8export * from './video-blacklist-validators.service'
8export * from './video-channel-validators.service' 9export * from './video-channel-validators.service'
9export * from './video-comment-validators.service' 10export * from './video-comment-validators.service'
10export * from './video-validators.service' 11export * from './video-validators.service'
diff --git a/client/src/app/shared/forms/form-validators/video-blacklist-validators.service.ts b/client/src/app/shared/forms/form-validators/video-blacklist-validators.service.ts
new file mode 100644
index 000000000..07d1f264a
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/video-blacklist-validators.service.ts
@@ -0,0 +1,19 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators } from '@angular/forms'
3import { Injectable } from '@angular/core'
4import { BuildFormValidator } from '@app/shared'
5
6@Injectable()
7export class VideoBlacklistValidatorsService {
8 readonly VIDEO_BLACKLIST_REASON: BuildFormValidator
9
10 constructor (private i18n: I18n) {
11 this.VIDEO_BLACKLIST_REASON = {
12 VALIDATORS: [ Validators.minLength(2), Validators.maxLength(300) ],
13 MESSAGES: {
14 'minlength': this.i18n('Blacklist reason must be at least 2 characters long.'),
15 'maxlength': this.i18n('Blacklist reason cannot be more than 300 characters long.')
16 }
17 }
18 }
19}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index ea7f2c887..722415a06 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -36,7 +36,7 @@ import {
36 ReactiveFileComponent, 36 ReactiveFileComponent,
37 ResetPasswordValidatorsService, 37 ResetPasswordValidatorsService,
38 UserValidatorsService, 38 UserValidatorsService,
39 VideoAbuseValidatorsService, 39 VideoAbuseValidatorsService, VideoBlacklistValidatorsService,
40 VideoChannelValidatorsService, 40 VideoChannelValidatorsService,
41 VideoCommentValidatorsService, 41 VideoCommentValidatorsService,
42 VideoValidatorsService 42 VideoValidatorsService
@@ -133,6 +133,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
133 MarkdownService, 133 MarkdownService,
134 VideoChannelService, 134 VideoChannelService,
135 VideoCaptionService, 135 VideoCaptionService,
136 VideoImportService,
136 137
137 FormValidatorService, 138 FormValidatorService,
138 CustomConfigValidatorsService, 139 CustomConfigValidatorsService,
@@ -144,7 +145,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
144 VideoCommentValidatorsService, 145 VideoCommentValidatorsService,
145 VideoValidatorsService, 146 VideoValidatorsService,
146 VideoCaptionsValidatorsService, 147 VideoCaptionsValidatorsService,
147 VideoImportService, 148 VideoBlacklistValidatorsService,
148 149
149 I18nPrimengCalendarService, 150 I18nPrimengCalendarService,
150 ScreenService, 151 ScreenService,
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 040d82c9a..a014260b1 100644
--- a/client/src/app/shared/video-blacklist/video-blacklist.service.ts
+++ b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
@@ -36,8 +36,10 @@ export class VideoBlacklistService {
36 ) 36 )
37 } 37 }
38 38
39 blacklistVideo (videoId: number) { 39 blacklistVideo (videoId: number, reason?: string) {
40 return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', {}) 40 const body = reason ? { reason } : {}
41
42 return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body)
41 .pipe( 43 .pipe(
42 map(this.restExtractor.extractDataBool), 44 map(this.restExtractor.extractDataBool),
43 catchError(res => this.restExtractor.handleError(res)) 45 catchError(res => this.restExtractor.handleError(res))
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index e500ad6fc..bdcc0bbba 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -44,7 +44,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
44 } 44 }
45 45
46 isBlackistableBy (user: AuthUser) { 46 isBlackistableBy (user: AuthUser) {
47 return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true && this.isLocal === false 47 return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
48 } 48 }
49 49
50 isUpdatableBy (user: AuthUser) { 50 isUpdatableBy (user: AuthUser) {
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
new file mode 100644
index 000000000..c436501b4
--- /dev/null
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
@@ -0,0 +1,31 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Blacklist video</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span>
5 </div>
6
7 <div class="modal-body">
8
9 <form novalidate [formGroup]="form" (ngSubmit)="blacklist()">
10 <div class="form-group">
11 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
12 </textarea>
13 <div *ngIf="formErrors.reason" class="form-error">
14 {{ formErrors.reason }}
15 </div>
16 </div>
17
18 <div class="form-group inputs">
19 <span i18n class="action-button action-button-cancel" (click)="hide()">
20 Cancel
21 </span>
22
23 <input
24 type="submit" i18n-value value="Submit" class="action-button-submit"
25 [disabled]="!form.valid"
26 >
27 </div>
28 </form>
29
30 </div>
31</ng-template>
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.scss b/client/src/app/videos/+video-watch/modal/video-blacklist.component.scss
new file mode 100644
index 000000000..afcdb9a16
--- /dev/null
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.scss
@@ -0,0 +1,6 @@
1@import 'variables';
2@import 'mixins';
3
4textarea {
5 @include peertube-textarea(100%, 100px);
6}
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
new file mode 100644
index 000000000..2c123ebed
--- /dev/null
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
@@ -0,0 +1,66 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index'
4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { RedirectService } from '@app/core'
10
11@Component({
12 selector: 'my-video-blacklist',
13 templateUrl: './video-blacklist.component.html',
14 styleUrls: [ './video-blacklist.component.scss' ]
15})
16export class VideoBlacklistComponent extends FormReactive implements OnInit {
17 @Input() video: VideoDetails = null
18
19 @ViewChild('modal') modal: NgbModal
20
21 error: string = null
22
23 private openedModal: NgbModalRef
24
25 constructor (
26 protected formValidatorService: FormValidatorService,
27 private modalService: NgbModal,
28 private videoBlacklistValidatorsService: VideoBlacklistValidatorsService,
29 private videoBlacklistService: VideoBlacklistService,
30 private notificationsService: NotificationsService,
31 private redirectService: RedirectService,
32 private i18n: I18n
33 ) {
34 super()
35 }
36
37 ngOnInit () {
38 this.buildForm({
39 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON
40 })
41 }
42
43 show () {
44 this.openedModal = this.modalService.open(this.modal, { keyboard: false })
45 }
46
47 hide () {
48 this.openedModal.close()
49 this.openedModal = null
50 }
51
52 blacklist () {
53 const reason = this.form.value[ 'reason' ] || undefined
54
55 this.videoBlacklistService.blacklistVideo(this.video.id, reason)
56 .subscribe(
57 () => {
58 this.notificationsService.success(this.i18n('Success'), this.i18n('Video blacklisted.'))
59 this.hide()
60 this.redirectService.redirectToHomepage()
61 },
62
63 err => this.notificationsService.error(this.i18n('Error'), err.message)
64 )
65 }
66}
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index dd0d628bd..f82f1c554 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -90,16 +90,16 @@
90 <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container> 90 <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container>
91 </a> 91 </a>
92 92
93 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
94 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
95 </a>
96
97 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> 93 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
98 <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container> 94 <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container>
99 </a> 95 </a>
100 96
97 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)">
98 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
99 </a>
100
101 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> 101 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
102 <span class="icon icon-blacklist"></span> <ng-container i18n>Delete</ng-container> 102 <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container>
103 </a> 103 </a>
104 </div> 104 </div>
105 </div> 105 </div>
@@ -205,4 +205,5 @@
205 <my-video-share #videoShareModal [video]="video"></my-video-share> 205 <my-video-share #videoShareModal [video]="video"></my-video-share>
206 <my-video-download #videoDownloadModal [video]="video"></my-video-download> 206 <my-video-download #videoDownloadModal [video]="video"></my-video-download>
207 <my-video-report #videoReportModal [video]="video"></my-video-report> 207 <my-video-report #videoReportModal [video]="video"></my-video-report>
208 <my-video-blacklist #videoBlacklistModal [video]="video"></my-video-blacklist>
208</ng-template> 209</ng-template>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index 7d269b31f..e63ab7bbd 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -258,6 +258,10 @@
258 &.icon-blacklist { 258 &.icon-blacklist {
259 background-image: url('../../../assets/images/video/blacklist.svg'); 259 background-image: url('../../../assets/images/video/blacklist.svg');
260 } 260 }
261
262 &.icon-delete {
263 background-image: url('../../../assets/images/global/delete-black.svg');
264 }
261 } 265 }
262 } 266 }
263 } 267 }
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 04bcc6cd1..878655d4a 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -21,6 +21,7 @@ import { MarkdownService } from '../shared'
21import { VideoDownloadComponent } from './modal/video-download.component' 21import { VideoDownloadComponent } from './modal/video-download.component'
22import { VideoReportComponent } from './modal/video-report.component' 22import { VideoReportComponent } from './modal/video-report.component'
23import { VideoShareComponent } from './modal/video-share.component' 23import { VideoShareComponent } from './modal/video-share.component'
24import { VideoBlacklistComponent } from './modal/video-blacklist.component'
24import { addContextMenu, getVideojsOptions, loadLocale } from '../../../assets/player/peertube-player' 25import { addContextMenu, getVideojsOptions, loadLocale } from '../../../assets/player/peertube-player'
25import { ServerService } from '@app/core' 26import { ServerService } from '@app/core'
26import { I18n } from '@ngx-translate/i18n-polyfill' 27import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -41,6 +42,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
41 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent 42 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
42 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent 43 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
43 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent 44 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
45 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
44 46
45 otherVideosDisplayed: Video[] = [] 47 otherVideosDisplayed: Video[] = []
46 48
@@ -156,26 +158,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
156 } 158 }
157 } 159 }
158 160
159 async blacklistVideo (event: Event) {
160 event.preventDefault()
161
162 const res = await this.confirmService.confirm(this.i18n('Do you really want to blacklist this video?'), this.i18n('Blacklist'))
163 if (res === false) return
164
165 this.videoBlacklistService.blacklistVideo(this.video.id)
166 .subscribe(
167 () => {
168 this.notificationsService.success(
169 this.i18n('Success'),
170 this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
171 )
172 this.redirectService.redirectToHomepage()
173 },
174
175 error => this.notificationsService.error(this.i18n('Error'), error.message)
176 )
177 }
178
179 showMoreDescription () { 161 showMoreDescription () {
180 if (this.completeVideoDescription === undefined) { 162 if (this.completeVideoDescription === undefined) {
181 return this.loadCompleteDescription() 163 return this.loadCompleteDescription()
@@ -230,6 +212,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
230 this.videoDownloadModal.show() 212 this.videoDownloadModal.show()
231 } 213 }
232 214
215 showBlacklistModal (event: Event) {
216 event.preventDefault()
217 this.videoBlacklistModal.show()
218 }
219
233 isUserLoggedIn () { 220 isUserLoggedIn () {
234 return this.authService.isLoggedIn() 221 return this.authService.isLoggedIn()
235 } 222 }
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
index 09d5133e4..7730919fe 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -15,6 +15,7 @@ import { VideoWatchRoutingModule } from './video-watch-routing.module'
15import { VideoWatchComponent } from './video-watch.component' 15import { VideoWatchComponent } from './video-watch.component'
16import { NgxQRCodeModule } from 'ngx-qrcode2' 16import { NgxQRCodeModule } from 'ngx-qrcode2'
17import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 17import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
18import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component'
18 19
19@NgModule({ 20@NgModule({
20 imports: [ 21 imports: [
@@ -31,6 +32,7 @@ import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
31 VideoDownloadComponent, 32 VideoDownloadComponent,
32 VideoShareComponent, 33 VideoShareComponent,
33 VideoReportComponent, 34 VideoReportComponent,
35 VideoBlacklistComponent,
34 VideoSupportComponent, 36 VideoSupportComponent,
35 VideoCommentsComponent, 37 VideoCommentsComponent,
36 VideoCommentAddComponent, 38 VideoCommentAddComponent,
diff --git a/client/src/assets/images/global/delete-black.svg b/client/src/assets/images/global/delete-black.svg
new file mode 100644
index 000000000..04ddc23aa
--- /dev/null
+++ b/client/src/assets/images/global/delete-black.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-224.000000, -159.000000)">
6 <g id="25" transform="translate(224.000000, 159.000000)">
7 <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#000" stroke-width="2"></path>
8 <rect id="Rectangle-424" fill="#000" x="2" y="4" width="20" height="2" rx="1"></rect>
9 <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#000"></path>
10 <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#000" stroke-width="2" stroke-linejoin="round"></path>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts
index 0e2be7123..543b20baa 100644
--- a/server/controllers/api/users.ts
+++ b/server/controllers/api/users.ts
@@ -201,14 +201,14 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
201 user.Account.id, 201 user.Account.id,
202 req.query.start as number, 202 req.query.start as number,
203 req.query.count as number, 203 req.query.count as number,
204 req.query.sort as VideoSortField, 204 req.query.sort as VideoSortField
205 false // Display my NSFW videos
206 ) 205 )
207 206
208 const additionalAttributes = { 207 const additionalAttributes = {
209 waitTranscoding: true, 208 waitTranscoding: true,
210 state: true, 209 state: true,
211 scheduledUpdate: true 210 scheduledUpdate: true,
211 blacklistInfo: true
212 } 212 }
213 return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) 213 return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
214} 214}
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index 8112b59b8..358f339ed 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -1,12 +1,21 @@
1import * as express from 'express' 1import * as express from 'express'
2import { BlacklistedVideo, UserRight } from '../../../../shared' 2import { BlacklistedVideo, UserRight, VideoBlacklistCreate } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { 5import {
6 asyncMiddleware, authenticate, blacklistSortValidator, ensureUserHasRight, paginationValidator, setBlacklistSort, setDefaultPagination, 6 asyncMiddleware,
7 videosBlacklistAddValidator, videosBlacklistRemoveValidator 7 authenticate,
8 blacklistSortValidator,
9 ensureUserHasRight,
10 paginationValidator,
11 setBlacklistSort,
12 setDefaultPagination,
13 videosBlacklistAddValidator,
14 videosBlacklistRemoveValidator,
15 videosBlacklistUpdateValidator
8} from '../../../middlewares' 16} from '../../../middlewares'
9import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 17import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
18import { sequelizeTypescript } from '../../../initializers'
10 19
11const blacklistRouter = express.Router() 20const blacklistRouter = express.Router()
12 21
@@ -27,6 +36,13 @@ blacklistRouter.get('/blacklist',
27 asyncMiddleware(listBlacklist) 36 asyncMiddleware(listBlacklist)
28) 37)
29 38
39blacklistRouter.put('/:videoId/blacklist',
40 authenticate,
41 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
42 asyncMiddleware(videosBlacklistUpdateValidator),
43 asyncMiddleware(updateVideoBlacklistController)
44)
45
30blacklistRouter.delete('/:videoId/blacklist', 46blacklistRouter.delete('/:videoId/blacklist',
31 authenticate, 47 authenticate,
32 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 48 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
@@ -42,17 +58,32 @@ export {
42 58
43// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
44 60
45async function addVideoToBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { 61async function addVideoToBlacklist (req: express.Request, res: express.Response) {
46 const videoInstance = res.locals.video 62 const videoInstance = res.locals.video
63 const body: VideoBlacklistCreate = req.body
47 64
48 const toCreate = { 65 const toCreate = {
49 videoId: videoInstance.id 66 videoId: videoInstance.id,
67 reason: body.reason
50 } 68 }
51 69
52 await VideoBlacklistModel.create(toCreate) 70 await VideoBlacklistModel.create(toCreate)
53 return res.type('json').status(204).end() 71 return res.type('json').status(204).end()
54} 72}
55 73
74async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
75 const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel
76 logger.info(videoBlacklist)
77
78 if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason
79
80 await sequelizeTypescript.transaction(t => {
81 return videoBlacklist.save({ transaction: t })
82 })
83
84 return res.type('json').status(204).end()
85}
86
56async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { 87async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
57 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort) 88 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort)
58 89
@@ -60,16 +91,13 @@ async function listBlacklist (req: express.Request, res: express.Response, next:
60} 91}
61 92
62async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { 93async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {
63 const blacklistedVideo = res.locals.blacklistedVideo as VideoBlacklistModel 94 const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel
64 95
65 try { 96 await sequelizeTypescript.transaction(t => {
66 await blacklistedVideo.destroy() 97 return videoBlacklist.destroy({ transaction: t })
98 })
67 99
68 logger.info('Video %s removed from blacklist.', res.locals.video.uuid) 100 logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
69 101
70 return res.sendStatus(204) 102 return res.type('json').status(204).end()
71 } catch (err) {
72 logger.error('Some error while removing video %s from blacklist.', res.locals.video.uuid, { err })
73 throw err
74 }
75} 103}
diff --git a/server/helpers/custom-validators/video-blacklist.ts b/server/helpers/custom-validators/video-blacklist.ts
new file mode 100644
index 000000000..b36b08d8b
--- /dev/null
+++ b/server/helpers/custom-validators/video-blacklist.ts
@@ -0,0 +1,32 @@
1import { Response } from 'express'
2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS } from '../../initializers'
4import { VideoBlacklistModel } from '../../models/video/video-blacklist'
5
6const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
7
8function isVideoBlacklistReasonValid (value: string) {
9 return value === null || validator.isLength(value, VIDEO_BLACKLIST_CONSTRAINTS_FIELDS.REASON)
10}
11
12async function isVideoBlacklistExist (videoId: number, res: Response) {
13 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
14
15 if (videoBlacklist === null) {
16 res.status(404)
17 .json({ error: 'Blacklisted video not found' })
18 .end()
19
20 return false
21 }
22
23 res.locals.videoBlacklist = videoBlacklist
24 return true
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 isVideoBlacklistReasonValid,
31 isVideoBlacklistExist
32}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index a008bf4c5..ff8e64330 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -15,7 +15,7 @@ let config: IConfig = require('config')
15 15
16// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
17 17
18const LAST_MIGRATION_VERSION = 250 18const LAST_MIGRATION_VERSION = 255
19 19
20// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
21 21
@@ -34,7 +34,7 @@ const SORTABLE_COLUMNS = {
34 USERS: [ 'id', 'username', 'createdAt' ], 34 USERS: [ 'id', 'username', 'createdAt' ],
35 ACCOUNTS: [ 'createdAt' ], 35 ACCOUNTS: [ 'createdAt' ],
36 JOBS: [ 'createdAt' ], 36 JOBS: [ 'createdAt' ],
37 VIDEO_ABUSES: [ 'id', 'createdAt' ], 37 VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],
38 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], 38 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
39 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ], 39 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ],
40 VIDEO_IMPORTS: [ 'createdAt' ], 40 VIDEO_IMPORTS: [ 'createdAt' ],
@@ -261,6 +261,9 @@ const CONSTRAINTS_FIELDS = {
261 REASON: { min: 2, max: 300 }, // Length 261 REASON: { min: 2, max: 300 }, // Length
262 MODERATION_COMMENT: { min: 2, max: 300 } // Length 262 MODERATION_COMMENT: { min: 2, max: 300 } // Length
263 }, 263 },
264 VIDEO_BLACKLIST: {
265 REASON: { min: 2, max: 300 } // Length
266 },
264 VIDEO_CHANNELS: { 267 VIDEO_CHANNELS: {
265 NAME: { min: 3, max: 120 }, // Length 268 NAME: { min: 3, max: 120 }, // Length
266 DESCRIPTION: { min: 3, max: 500 }, // Length 269 DESCRIPTION: { min: 3, max: 500 }, // Length
diff --git a/server/initializers/migrations/0255-video-blacklist-reason.ts b/server/initializers/migrations/0255-video-blacklist-reason.ts
new file mode 100644
index 000000000..a380e620e
--- /dev/null
+++ b/server/initializers/migrations/0255-video-blacklist-reason.ts
@@ -0,0 +1,25 @@
1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3import { VideoAbuseState } from '../../../shared/models/videos'
4
5async function up (utils: {
6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize
9}): Promise<any> {
10
11 {
12 const data = {
13 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max),
14 allowNull: true,
15 defaultValue: null
16 }
17 await utils.queryInterface.addColumn('videoBlacklist', 'reason', data)
18 }
19}
20
21function down (options) {
22 throw new Error('Not implemented.')
23}
24
25export { up, down }
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 3faeffd77..a1212878f 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -108,6 +108,55 @@ class Emailer {
108 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 108 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
109 } 109 }
110 110
111 async addVideoBlacklistReportJob (videoId: number, reason?: string) {
112 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
113 if (!video) throw new Error('Unknown Video id during Blacklist report.')
114 // It's not our user
115 if (video.remote === true) return
116
117 const user = await UserModel.loadById(video.VideoChannel.Account.userId)
118
119 const reasonString = reason ? ` for the following reason: ${reason}` : ''
120 const blockedString = `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.`
121
122 const text = 'Hi,\n\n' +
123 blockedString +
124 '\n\n' +
125 'Cheers,\n' +
126 `PeerTube.`
127
128 const to = user.email
129 const emailPayload: EmailPayload = {
130 to: [ to ],
131 subject: `[PeerTube] Video ${video.name} blacklisted`,
132 text
133 }
134
135 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
136 }
137
138 async addVideoUnblacklistReportJob (videoId: number) {
139 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
140 if (!video) throw new Error('Unknown Video id during Blacklist report.')
141
142 const user = await UserModel.loadById(video.VideoChannel.Account.userId)
143
144 const text = 'Hi,\n\n' +
145 `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` +
146 '\n\n' +
147 'Cheers,\n' +
148 `PeerTube.`
149
150 const to = user.email
151 const emailPayload: EmailPayload = {
152 to: [ to ],
153 subject: `[PeerTube] Video ${video.name} unblacklisted`,
154 text
155 }
156
157 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
158 }
159
111 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) { 160 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
112 const reasonString = reason ? ` for the following reason: ${reason}` : '' 161 const reasonString = reason ? ` for the following reason: ${reason}` : ''
113 const blockedWord = blocked ? 'blocked' : 'unblocked' 162 const blockedWord = blocked ? 'blocked' : 'unblocked'
diff --git a/server/middlewares/validators/video-blacklist.ts b/server/middlewares/validators/video-blacklist.ts
index 3c1ef1b4e..95a2b9f17 100644
--- a/server/middlewares/validators/video-blacklist.ts
+++ b/server/middlewares/validators/video-blacklist.ts
@@ -1,11 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 3import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../helpers/custom-validators/videos'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
6import { VideoModel } from '../../models/video/video'
7import { VideoBlacklistModel } from '../../models/video/video-blacklist'
8import { areValidationErrors } from './utils' 6import { areValidationErrors } from './utils'
7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
9 8
10const videosBlacklistRemoveValidator = [ 9const videosBlacklistRemoveValidator = [
11 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -15,7 +14,7 @@ const videosBlacklistRemoveValidator = [
15 14
16 if (areValidationErrors(req, res)) return 15 if (areValidationErrors(req, res)) return
17 if (!await isVideoExist(req.params.videoId, res)) return 16 if (!await isVideoExist(req.params.videoId, res)) return
18 if (!await checkVideoIsBlacklisted(res.locals.video, res)) return 17 if (!await isVideoBlacklistExist(res.locals.video.id, res)) return
19 18
20 return next() 19 return next()
21 } 20 }
@@ -23,47 +22,41 @@ const videosBlacklistRemoveValidator = [
23 22
24const videosBlacklistAddValidator = [ 23const videosBlacklistAddValidator = [
25 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 24 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
25 body('reason')
26 .optional()
27 .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
26 28
27 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 29 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
28 logger.debug('Checking videosBlacklist parameters', { parameters: req.params }) 30 logger.debug('Checking videosBlacklistAdd parameters', { parameters: req.params })
29 31
30 if (areValidationErrors(req, res)) return 32 if (areValidationErrors(req, res)) return
31 if (!await isVideoExist(req.params.videoId, res)) return 33 if (!await isVideoExist(req.params.videoId, res)) return
32 if (!checkVideoIsBlacklistable(res.locals.video, res)) return
33 34
34 return next() 35 return next()
35 } 36 }
36] 37]
37 38
38// --------------------------------------------------------------------------- 39const videosBlacklistUpdateValidator = [
40 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
41 body('reason')
42 .optional()
43 .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
39 44
40export { 45 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
41 videosBlacklistAddValidator, 46 logger.debug('Checking videosBlacklistUpdate parameters', { parameters: req.params })
42 videosBlacklistRemoveValidator
43}
44// ---------------------------------------------------------------------------
45 47
46function checkVideoIsBlacklistable (video: VideoModel, res: express.Response) { 48 if (areValidationErrors(req, res)) return
47 if (video.isOwned() === true) { 49 if (!await isVideoExist(req.params.videoId, res)) return
48 res.status(403) 50 if (!await isVideoBlacklistExist(res.locals.video.id, res)) return
49 .json({ error: 'Cannot blacklist a local video' })
50 .end()
51 51
52 return false 52 return next()
53 } 53 }
54]
54 55
55 return true 56// ---------------------------------------------------------------------------
56}
57
58async function checkVideoIsBlacklisted (video: VideoModel, res: express.Response) {
59 const blacklistedVideo = await VideoBlacklistModel.loadByVideoId(video.id)
60 if (!blacklistedVideo) {
61 res.status(404)
62 .send('Blacklisted video not found')
63
64 return false
65 }
66 57
67 res.locals.blacklistedVideo = blacklistedVideo 58export {
68 return true 59 videosBlacklistAddValidator,
60 videosBlacklistRemoveValidator,
61 videosBlacklistUpdateValidator
69} 62}
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 26167174a..1b8a338cb 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,7 +1,23 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import {
2 AfterCreate,
3 AfterDestroy,
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt, DataType,
8 ForeignKey,
9 Is,
10 Model,
11 Table,
12 UpdatedAt
13} from 'sequelize-typescript'
2import { SortType } from '../../helpers/utils' 14import { SortType } from '../../helpers/utils'
3import { getSortOnModel } from '../utils' 15import { getSortOnModel, throwIfNotValid } from '../utils'
4import { VideoModel } from './video' 16import { VideoModel } from './video'
17import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
18import { Emailer } from '../../lib/emailer'
19import { BlacklistedVideo } from '../../../shared/models/videos'
20import { CONSTRAINTS_FIELDS } from '../../initializers'
5 21
6@Table({ 22@Table({
7 tableName: 'videoBlacklist', 23 tableName: 'videoBlacklist',
@@ -14,6 +30,11 @@ import { VideoModel } from './video'
14}) 30})
15export class VideoBlacklistModel extends Model<VideoBlacklistModel> { 31export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
16 32
33 @AllowNull(true)
34 @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason'))
35 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max))
36 reason: string
37
17 @CreatedAt 38 @CreatedAt
18 createdAt: Date 39 createdAt: Date
19 40
@@ -32,6 +53,16 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
32 }) 53 })
33 Video: VideoModel 54 Video: VideoModel
34 55
56 @AfterCreate
57 static sendBlacklistEmailNotification (instance: VideoBlacklistModel) {
58 return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason)
59 }
60
61 @AfterDestroy
62 static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) {
63 return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId)
64 }
65
35 static listForApi (start: number, count: number, sort: SortType) { 66 static listForApi (start: number, count: number, sort: SortType) {
36 const query = { 67 const query = {
37 offset: start, 68 offset: start,
@@ -59,22 +90,26 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
59 return VideoBlacklistModel.findOne(query) 90 return VideoBlacklistModel.findOne(query)
60 } 91 }
61 92
62 toFormattedJSON () { 93 toFormattedJSON (): BlacklistedVideo {
63 const video = this.Video 94 const video = this.Video
64 95
65 return { 96 return {
66 id: this.id, 97 id: this.id,
67 videoId: this.videoId,
68 createdAt: this.createdAt, 98 createdAt: this.createdAt,
69 updatedAt: this.updatedAt, 99 updatedAt: this.updatedAt,
70 name: video.name, 100 reason: this.reason,
71 uuid: video.uuid, 101
72 description: video.description, 102 video: {
73 duration: video.duration, 103 id: video.id,
74 views: video.views, 104 name: video.name,
75 likes: video.likes, 105 uuid: video.uuid,
76 dislikes: video.dislikes, 106 description: video.description,
77 nsfw: video.nsfw 107 duration: video.duration,
108 views: video.views,
109 likes: video.likes,
110 dislikes: video.dislikes,
111 nsfw: video.nsfw
112 }
78 } 113 }
79 } 114 }
80} 115}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 39fe21007..f3a900bc9 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -93,6 +93,7 @@ import { VideoShareModel } from './video-share'
93import { VideoTagModel } from './video-tag' 93import { VideoTagModel } from './video-tag'
94import { ScheduleVideoUpdateModel } from './schedule-video-update' 94import { ScheduleVideoUpdateModel } from './schedule-video-update'
95import { VideoCaptionModel } from './video-caption' 95import { VideoCaptionModel } from './video-caption'
96import { VideoBlacklistModel } from './video-blacklist'
96 97
97// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
98const indexes: Sequelize.DefineIndexesOptions[] = [ 99const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -581,6 +582,15 @@ export class VideoModel extends Model<VideoModel> {
581 }) 582 })
582 ScheduleVideoUpdate: ScheduleVideoUpdateModel 583 ScheduleVideoUpdate: ScheduleVideoUpdateModel
583 584
585 @HasOne(() => VideoBlacklistModel, {
586 foreignKey: {
587 name: 'videoId',
588 allowNull: false
589 },
590 onDelete: 'cascade'
591 })
592 VideoBlacklist: VideoBlacklistModel
593
584 @HasMany(() => VideoCaptionModel, { 594 @HasMany(() => VideoCaptionModel, {
585 foreignKey: { 595 foreignKey: {
586 name: 'videoId', 596 name: 'videoId',
@@ -755,7 +765,7 @@ export class VideoModel extends Model<VideoModel> {
755 }) 765 })
756 } 766 }
757 767
758 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) { 768 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
759 const query: IFindOptions<VideoModel> = { 769 const query: IFindOptions<VideoModel> = {
760 offset: start, 770 offset: start,
761 limit: count, 771 limit: count,
@@ -777,6 +787,10 @@ export class VideoModel extends Model<VideoModel> {
777 { 787 {
778 model: ScheduleVideoUpdateModel, 788 model: ScheduleVideoUpdateModel,
779 required: false 789 required: false
790 },
791 {
792 model: VideoBlacklistModel,
793 required: false
780 } 794 }
781 ] 795 ]
782 } 796 }
@@ -788,12 +802,6 @@ export class VideoModel extends Model<VideoModel> {
788 }) 802 })
789 } 803 }
790 804
791 if (hideNSFW === true) {
792 query.where = {
793 nsfw: false
794 }
795 }
796
797 return VideoModel.findAndCountAll(query).then(({ rows, count }) => { 805 return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
798 return { 806 return {
799 data: rows, 807 data: rows,
@@ -1177,7 +1185,8 @@ export class VideoModel extends Model<VideoModel> {
1177 additionalAttributes: { 1185 additionalAttributes: {
1178 state?: boolean, 1186 state?: boolean,
1179 waitTranscoding?: boolean, 1187 waitTranscoding?: boolean,
1180 scheduledUpdate?: boolean 1188 scheduledUpdate?: boolean,
1189 blacklistInfo?: boolean
1181 } 1190 }
1182 }): Video { 1191 }): Video {
1183 const formattedAccount = this.VideoChannel.Account.toFormattedJSON() 1192 const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
@@ -1254,6 +1263,11 @@ export class VideoModel extends Model<VideoModel> {
1254 privacy: this.ScheduleVideoUpdate.privacy || undefined 1263 privacy: this.ScheduleVideoUpdate.privacy || undefined
1255 } 1264 }
1256 } 1265 }
1266
1267 if (options.additionalAttributes.blacklistInfo === true) {
1268 videoObject.blacklisted = !!this.VideoBlacklist
1269 videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null
1270 }
1257 } 1271 }
1258 1272
1259 return videoObject 1273 return videoObject
diff --git a/server/tests/api/check-params/video-blacklist.ts b/server/tests/api/check-params/video-blacklist.ts
index 6cd13d23f..415474718 100644
--- a/server/tests/api/check-params/video-blacklist.ts
+++ b/server/tests/api/check-params/video-blacklist.ts
@@ -3,13 +3,24 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 createUser, flushTests, getBlacklistedVideosList, killallServers, makePostBodyRequest, removeVideoFromBlacklist, runServer, 6 createUser,
7 ServerInfo, setAccessTokensToServers, uploadVideo, userLogin 7 flushTests,
8 getBlacklistedVideosList,
9 killallServers,
10 makePostBodyRequest,
11 makePutBodyRequest,
12 removeVideoFromBlacklist,
13 runServer,
14 ServerInfo,
15 setAccessTokensToServers,
16 uploadVideo,
17 userLogin
8} from '../../utils' 18} from '../../utils'
9import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 19import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
10 20
11describe('Test video blacklist API validators', function () { 21describe('Test video blacklist API validators', function () {
12 let server: ServerInfo 22 let server: ServerInfo
23 let notBlacklistedVideoId: number
13 let userAccessToken = '' 24 let userAccessToken = ''
14 25
15 // --------------------------------------------------------------- 26 // ---------------------------------------------------------------
@@ -28,8 +39,15 @@ describe('Test video blacklist API validators', function () {
28 await createUser(server.url, server.accessToken, username, password) 39 await createUser(server.url, server.accessToken, username, password)
29 userAccessToken = await userLogin(server, { username, password }) 40 userAccessToken = await userLogin(server, { username, password })
30 41
31 const res = await uploadVideo(server.url, server.accessToken, {}) 42 {
32 server.video = res.body.video 43 const res = await uploadVideo(server.url, server.accessToken, {})
44 server.video = res.body.video
45 }
46
47 {
48 const res = await uploadVideo(server.url, server.accessToken, {})
49 notBlacklistedVideoId = res.body.video.uuid
50 }
33 }) 51 })
34 52
35 describe('When adding a video in blacklist', function () { 53 describe('When adding a video in blacklist', function () {
@@ -59,20 +77,70 @@ describe('Test video blacklist API validators', function () {
59 await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: 403 }) 77 await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: 403 })
60 }) 78 })
61 79
62 it('Should fail with a local video', async function () { 80 it('Should fail with an invalid reason', async function () {
63 const path = basePath + server.video.id + '/blacklist' 81 const path = basePath + server.video.uuid + '/blacklist'
82 const fields = { reason: 'a'.repeat(305) }
83
84 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
85 })
86
87 it('Should succeed with the correct params', async function () {
88 const path = basePath + server.video.uuid + '/blacklist'
89 const fields = { }
90
91 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 })
92 })
93 })
94
95 describe('When updating a video in blacklist', function () {
96 const basePath = '/api/v1/videos/'
97
98 it('Should fail with a wrong video', async function () {
99 const wrongPath = '/api/v1/videos/blabla/blacklist'
100 const fields = {}
101 await makePutBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields })
102 })
103
104 it('Should fail with a video not blacklisted', async function () {
105 const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist'
64 const fields = {} 106 const fields = {}
65 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 403 }) 107 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 404 })
108 })
109
110 it('Should fail with a non authenticated user', async function () {
111 const path = basePath + server.video + '/blacklist'
112 const fields = {}
113 await makePutBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 })
114 })
115
116 it('Should fail with a non admin user', async function () {
117 const path = basePath + server.video + '/blacklist'
118 const fields = {}
119 await makePutBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: 403 })
120 })
121
122 it('Should fail with an invalid reason', async function () {
123 const path = basePath + server.video.uuid + '/blacklist'
124 const fields = { reason: 'a'.repeat(305) }
125
126 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
127 })
128
129 it('Should succeed with the correct params', async function () {
130 const path = basePath + server.video.uuid + '/blacklist'
131 const fields = { reason: 'hello' }
132
133 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 })
66 }) 134 })
67 }) 135 })
68 136
69 describe('When removing a video in blacklist', function () { 137 describe('When removing a video in blacklist', function () {
70 it('Should fail with a non authenticated user', async function () { 138 it('Should fail with a non authenticated user', async function () {
71 await removeVideoFromBlacklist(server.url, 'fake token', server.video.id, 401) 139 await removeVideoFromBlacklist(server.url, 'fake token', server.video.uuid, 401)
72 }) 140 })
73 141
74 it('Should fail with a non admin user', async function () { 142 it('Should fail with a non admin user', async function () {
75 await removeVideoFromBlacklist(server.url, userAccessToken, server.video.id, 403) 143 await removeVideoFromBlacklist(server.url, userAccessToken, server.video.uuid, 403)
76 }) 144 })
77 145
78 it('Should fail with an incorrect id', async function () { 146 it('Should fail with an incorrect id', async function () {
@@ -81,7 +149,11 @@ describe('Test video blacklist API validators', function () {
81 149
82 it('Should fail with a not blacklisted video', async function () { 150 it('Should fail with a not blacklisted video', async function () {
83 // The video was not added to the blacklist so it should fail 151 // The video was not added to the blacklist so it should fail
84 await removeVideoFromBlacklist(server.url, server.accessToken, server.video.id, 404) 152 await removeVideoFromBlacklist(server.url, server.accessToken, notBlacklistedVideoId, 404)
153 })
154
155 it('Should succeed with the correct params', async function () {
156 await removeVideoFromBlacklist(server.url, server.accessToken, server.video.uuid, 204)
85 }) 157 })
86 }) 158 })
87 159
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index 65d6a759f..db937f288 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -3,9 +3,10 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 addVideoToBlacklist,
6 askResetPassword, 7 askResetPassword,
7 blockUser, 8 blockUser,
8 createUser, 9 createUser, removeVideoFromBlacklist,
9 reportVideoAbuse, 10 reportVideoAbuse,
10 resetPassword, 11 resetPassword,
11 runServer, 12 runServer,
@@ -22,7 +23,9 @@ const expect = chai.expect
22describe('Test emails', function () { 23describe('Test emails', function () {
23 let server: ServerInfo 24 let server: ServerInfo
24 let userId: number 25 let userId: number
26 let userAccessToken: string
25 let videoUUID: string 27 let videoUUID: string
28 let videoUserUUID: string
26 let verificationString: string 29 let verificationString: string
27 const emails: object[] = [] 30 const emails: object[] = []
28 const user = { 31 const user = {
@@ -48,6 +51,16 @@ describe('Test emails', function () {
48 { 51 {
49 const res = await createUser(server.url, server.accessToken, user.username, user.password) 52 const res = await createUser(server.url, server.accessToken, user.username, user.password)
50 userId = res.body.user.id 53 userId = res.body.user.id
54
55 userAccessToken = await userLogin(server, user)
56 }
57
58 {
59 const attributes = {
60 name: 'my super user video'
61 }
62 const res = await uploadVideo(server.url, userAccessToken, attributes)
63 videoUserUUID = res.body.video.uuid
51 } 64 }
52 65
53 { 66 {
@@ -158,6 +171,42 @@ describe('Test emails', function () {
158 }) 171 })
159 }) 172 })
160 173
174 describe('When blacklisting a video', function () {
175 it('Should send the notification email', async function () {
176 this.timeout(10000)
177
178 const reason = 'my super reason'
179 await addVideoToBlacklist(server.url, server.accessToken, videoUserUUID, reason)
180
181 await waitJobs(server)
182 expect(emails).to.have.lengthOf(5)
183
184 const email = emails[4]
185
186 expect(email['from'][0]['address']).equal('test-admin@localhost')
187 expect(email['to'][0]['address']).equal('user_1@example.com')
188 expect(email['subject']).contains(' blacklisted')
189 expect(email['text']).contains('my super user video')
190 expect(email['text']).contains('my super reason')
191 })
192
193 it('Should send the notification email', async function () {
194 this.timeout(10000)
195
196 await removeVideoFromBlacklist(server.url, server.accessToken, videoUserUUID)
197
198 await waitJobs(server)
199 expect(emails).to.have.lengthOf(6)
200
201 const email = emails[5]
202
203 expect(email['from'][0]['address']).equal('test-admin@localhost')
204 expect(email['to'][0]['address']).equal('user_1@example.com')
205 expect(email['subject']).contains(' unblacklisted')
206 expect(email['text']).contains('my super user video')
207 })
208 })
209
161 after(async function () { 210 after(async function () {
162 killallServers([ server ]) 211 killallServers([ server ])
163 }) 212 })
diff --git a/server/tests/api/videos/video-blacklist-management.ts b/server/tests/api/videos/video-blacklist-management.ts
index 4d1a06436..7bf39dc99 100644
--- a/server/tests/api/videos/video-blacklist-management.ts
+++ b/server/tests/api/videos/video-blacklist-management.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expressions */ 1/* tslint:disable:no-unused-expression */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import * as lodash from 'lodash' 4import * as lodash from 'lodash'
@@ -7,29 +7,33 @@ import {
7 addVideoToBlacklist, 7 addVideoToBlacklist,
8 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
9 getBlacklistedVideosList, 9 getBlacklistedVideosList,
10 getMyVideos,
10 getSortedBlacklistedVideosList, 11 getSortedBlacklistedVideosList,
11 getVideosList, 12 getVideosList,
12 killallServers, 13 killallServers,
13 removeVideoFromBlacklist, 14 removeVideoFromBlacklist,
14 ServerInfo, 15 ServerInfo,
15 setAccessTokensToServers, 16 setAccessTokensToServers,
17 updateVideoBlacklist,
16 uploadVideo 18 uploadVideo
17} from '../../utils/index' 19} from '../../utils/index'
18import { doubleFollow } from '../../utils/server/follows' 20import { doubleFollow } from '../../utils/server/follows'
19import { waitJobs } from '../../utils/server/jobs' 21import { waitJobs } from '../../utils/server/jobs'
22import { VideoAbuse } from '../../../../shared/models/videos'
20 23
21const expect = chai.expect 24const expect = chai.expect
22const orderBy = lodash.orderBy 25const orderBy = lodash.orderBy
23 26
24describe('Test video blacklist management', function () { 27describe('Test video blacklist management', function () {
25 let servers: ServerInfo[] = [] 28 let servers: ServerInfo[] = []
29 let videoId: number
26 30
27 async function blacklistVideosOnServer (server: ServerInfo) { 31 async function blacklistVideosOnServer (server: ServerInfo) {
28 const res = await getVideosList(server.url) 32 const res = await getVideosList(server.url)
29 33
30 const videos = res.body.data 34 const videos = res.body.data
31 for (let video of videos) { 35 for (let video of videos) {
32 await addVideoToBlacklist(server.url, server.accessToken, video.id) 36 await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason')
33 } 37 }
34 } 38 }
35 39
@@ -62,53 +66,85 @@ describe('Test video blacklist management', function () {
62 66
63 expect(res.body.total).to.equal(2) 67 expect(res.body.total).to.equal(2)
64 68
65 const videos = res.body.data 69 const blacklistedVideos = res.body.data
66 expect(videos).to.be.an('array') 70 expect(blacklistedVideos).to.be.an('array')
67 expect(videos.length).to.equal(2) 71 expect(blacklistedVideos.length).to.equal(2)
72
73 for (const blacklistedVideo of blacklistedVideos) {
74 expect(blacklistedVideo.reason).to.equal('super reason')
75 videoId = blacklistedVideo.video.id
76 }
68 }) 77 })
69 78
70 it('Should get the correct sort when sorting by descending id', async function () { 79 it('Should get the correct sort when sorting by descending id', async function () {
71 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id') 80 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id')
72 expect(res.body.total).to.equal(2) 81 expect(res.body.total).to.equal(2)
73 82
74 const videos = res.body.data 83 const blacklistedVideos = res.body.data
75 expect(videos).to.be.an('array') 84 expect(blacklistedVideos).to.be.an('array')
76 expect(videos.length).to.equal(2) 85 expect(blacklistedVideos.length).to.equal(2)
77 86
78 const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ]) 87 const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ])
79 88
80 expect(videos).to.deep.equal(result) 89 expect(blacklistedVideos).to.deep.equal(result)
81 }) 90 })
82 91
83 it('Should get the correct sort when sorting by descending video name', async function () { 92 it('Should get the correct sort when sorting by descending video name', async function () {
84 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') 93 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name')
85 expect(res.body.total).to.equal(2) 94 expect(res.body.total).to.equal(2)
86 95
87 const videos = res.body.data 96 const blacklistedVideos = res.body.data
88 expect(videos).to.be.an('array') 97 expect(blacklistedVideos).to.be.an('array')
89 expect(videos.length).to.equal(2) 98 expect(blacklistedVideos.length).to.equal(2)
90 99
91 const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ]) 100 const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ])
92 101
93 expect(videos).to.deep.equal(result) 102 expect(blacklistedVideos).to.deep.equal(result)
94 }) 103 })
95 104
96 it('Should get the correct sort when sorting by ascending creation date', async function () { 105 it('Should get the correct sort when sorting by ascending creation date', async function () {
97 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') 106 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt')
98 expect(res.body.total).to.equal(2) 107 expect(res.body.total).to.equal(2)
99 108
100 const videos = res.body.data 109 const blacklistedVideos = res.body.data
101 expect(videos).to.be.an('array') 110 expect(blacklistedVideos).to.be.an('array')
102 expect(videos.length).to.equal(2) 111 expect(blacklistedVideos.length).to.equal(2)
103 112
104 const result = orderBy(res.body.data, [ 'createdAt' ]) 113 const result = orderBy(res.body.data, [ 'createdAt' ])
105 114
106 expect(videos).to.deep.equal(result) 115 expect(blacklistedVideos).to.deep.equal(result)
116 })
117 })
118
119 describe('When updating blacklisted videos', function () {
120 it('Should change the reason', async function () {
121 await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated')
122
123 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name')
124 const video = res.body.data.find(b => b.video.id === videoId)
125
126 expect(video.reason).to.equal('my super reason updated')
127 })
128 })
129
130 describe('When listing my videos', function () {
131 it('Should display blacklisted videos', async function () {
132 await blacklistVideosOnServer(servers[1])
133
134 const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 5)
135
136 expect(res.body.total).to.equal(2)
137 expect(res.body.data).to.have.lengthOf(2)
138
139 for (const video of res.body.data) {
140 expect(video.blacklisted).to.be.true
141 expect(video.blacklistedReason).to.equal('super reason')
142 }
107 }) 143 })
108 }) 144 })
109 145
110 describe('When removing a blacklisted video', function () { 146 describe('When removing a blacklisted video', function () {
111 let videoToRemove 147 let videoToRemove: VideoAbuse
112 let blacklist = [] 148 let blacklist = []
113 149
114 it('Should not have any video in videos list on server 1', async function () { 150 it('Should not have any video in videos list on server 1', async function () {
@@ -125,7 +161,7 @@ describe('Test video blacklist management', function () {
125 blacklist = res.body.data.slice(1) 161 blacklist = res.body.data.slice(1)
126 162
127 // Remove it 163 // Remove it
128 await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.videoId) 164 await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.video.id)
129 }) 165 })
130 166
131 it('Should have the ex-blacklisted video in videos list on server 1', async function () { 167 it('Should have the ex-blacklisted video in videos list on server 1', async function () {
@@ -136,8 +172,8 @@ describe('Test video blacklist management', function () {
136 expect(videos).to.be.an('array') 172 expect(videos).to.be.an('array')
137 expect(videos.length).to.equal(1) 173 expect(videos.length).to.equal(1)
138 174
139 expect(videos[0].name).to.equal(videoToRemove.name) 175 expect(videos[0].name).to.equal(videoToRemove.video.name)
140 expect(videos[0].id).to.equal(videoToRemove.videoId) 176 expect(videos[0].id).to.equal(videoToRemove.video.id)
141 }) 177 })
142 178
143 it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { 179 it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () {
diff --git a/server/tests/utils/videos/video-blacklist.ts b/server/tests/utils/videos/video-blacklist.ts
index aa0d232b6..7819f4b25 100644
--- a/server/tests/utils/videos/video-blacklist.ts
+++ b/server/tests/utils/videos/video-blacklist.ts
@@ -1,15 +1,26 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2 2
3function addVideoToBlacklist (url: string, token: string, videoId: number, specialStatus = 204) { 3function addVideoToBlacklist (url: string, token: string, videoId: number | string, reason?: string, specialStatus = 204) {
4 const path = '/api/v1/videos/' + videoId + '/blacklist' 4 const path = '/api/v1/videos/' + videoId + '/blacklist'
5 5
6 return request(url) 6 return request(url)
7 .post(path) 7 .post(path)
8 .send({ reason })
8 .set('Accept', 'application/json') 9 .set('Accept', 'application/json')
9 .set('Authorization', 'Bearer ' + token) 10 .set('Authorization', 'Bearer ' + token)
10 .expect(specialStatus) 11 .expect(specialStatus)
11} 12}
12 13
14function updateVideoBlacklist (url: string, token: string, videoId: number, reason?: string, specialStatus = 204) {
15 const path = '/api/v1/videos/' + videoId + '/blacklist'
16
17 return request(url)
18 .put(path)
19 .send({ reason })
20 .set('Accept', 'application/json')
21 .set('Authorization', 'Bearer ' + token)
22 .expect(specialStatus)}
23
13function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) { 24function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) {
14 const path = '/api/v1/videos/' + videoId + '/blacklist' 25 const path = '/api/v1/videos/' + videoId + '/blacklist'
15 26
@@ -50,5 +61,6 @@ export {
50 addVideoToBlacklist, 61 addVideoToBlacklist,
51 removeVideoFromBlacklist, 62 removeVideoFromBlacklist,
52 getBlacklistedVideosList, 63 getBlacklistedVideosList,
53 getSortedBlacklistedVideosList 64 getSortedBlacklistedVideosList,
65 updateVideoBlacklist
54} 66}
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts
index 02bf2b842..b99dd2d8f 100644
--- a/shared/models/videos/index.ts
+++ b/shared/models/videos/index.ts
@@ -6,6 +6,8 @@ export * from './video-abuse-create.model'
6export * from './video-abuse.model' 6export * from './video-abuse.model'
7export * from './video-abuse-update.model' 7export * from './video-abuse-update.model'
8export * from './video-blacklist.model' 8export * from './video-blacklist.model'
9export * from './video-blacklist-create.model'
10export * from './video-blacklist-update.model'
9export * from './video-channel-create.model' 11export * from './video-channel-create.model'
10export * from './video-channel-update.model' 12export * from './video-channel-update.model'
11export * from './video-channel.model' 13export * from './video-channel.model'
diff --git a/shared/models/videos/video-blacklist-create.model.ts b/shared/models/videos/video-blacklist-create.model.ts
new file mode 100644
index 000000000..89c69cb56
--- /dev/null
+++ b/shared/models/videos/video-blacklist-create.model.ts
@@ -0,0 +1,3 @@
1export interface VideoBlacklistCreate {
2 reason?: string
3}
diff --git a/shared/models/videos/video-blacklist-update.model.ts b/shared/models/videos/video-blacklist-update.model.ts
new file mode 100644
index 000000000..0a86cf7b0
--- /dev/null
+++ b/shared/models/videos/video-blacklist-update.model.ts
@@ -0,0 +1,3 @@
1export interface VideoBlacklistUpdate {
2 reason?: string
3}
diff --git a/shared/models/videos/video-blacklist.model.ts b/shared/models/videos/video-blacklist.model.ts
index af04502e8..a060da357 100644
--- a/shared/models/videos/video-blacklist.model.ts
+++ b/shared/models/videos/video-blacklist.model.ts
@@ -1,14 +1,18 @@
1export interface BlacklistedVideo { 1export interface BlacklistedVideo {
2 id: number 2 id: number
3 videoId: number
4 createdAt: Date 3 createdAt: Date
5 updatedAt: Date 4 updatedAt: Date
6 name: string 5 reason?: string
7 uuid: string 6
8 description: string 7 video: {
9 duration: number 8 id: number
10 views: number 9 name: string
11 likes: number 10 uuid: string
12 dislikes: number 11 description: string
13 nsfw: boolean 12 duration: number
13 views: number
14 likes: number
15 dislikes: number
16 nsfw: boolean
17 }
14} 18}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index f7bbaac76..8dfa96069 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -43,6 +43,9 @@ export interface Video {
43 state?: VideoConstant<VideoState> 43 state?: VideoConstant<VideoState>
44 scheduledUpdate?: VideoScheduleUpdate 44 scheduledUpdate?: VideoScheduleUpdate
45 45
46 blacklisted?: boolean
47 blacklistedReason?: string
48
46 account: { 49 account: {
47 id: number 50 id: number
48 uuid: string 51 uuid: string