Add blacklist reason field
authorChocobozzz <me@florianbigard.com>
Mon, 13 Aug 2018 14:57:13 +0000 (16:57 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 14 Aug 2018 07:27:18 +0000 (09:27 +0200)
35 files changed:
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.scss [new file with mode: 0644]
client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts
client/src/app/shared/forms/form-validators/index.ts
client/src/app/shared/forms/form-validators/video-blacklist-validators.service.ts [new file with mode: 0644]
client/src/app/shared/shared.module.ts
client/src/app/shared/video-blacklist/video-blacklist.service.ts
client/src/app/shared/video/video-details.model.ts
client/src/app/videos/+video-watch/modal/video-blacklist.component.html [new file with mode: 0644]
client/src/app/videos/+video-watch/modal/video-blacklist.component.scss [new file with mode: 0644]
client/src/app/videos/+video-watch/modal/video-blacklist.component.ts [new file with mode: 0644]
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.scss
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/app/videos/+video-watch/video-watch.module.ts
client/src/assets/images/global/delete-black.svg [new file with mode: 0644]
server/controllers/api/users.ts
server/controllers/api/videos/blacklist.ts
server/helpers/custom-validators/video-blacklist.ts [new file with mode: 0644]
server/initializers/constants.ts
server/initializers/migrations/0255-video-blacklist-reason.ts [new file with mode: 0644]
server/lib/emailer.ts
server/middlewares/validators/video-blacklist.ts
server/models/video/video-blacklist.ts
server/models/video/video.ts
server/tests/api/check-params/video-blacklist.ts
server/tests/api/server/email.ts
server/tests/api/videos/video-blacklist-management.ts
server/tests/utils/videos/video-blacklist.ts
shared/models/videos/index.ts
shared/models/videos/video-blacklist-create.model.ts [new file with mode: 0644]
shared/models/videos/video-blacklist-update.model.ts [new file with mode: 0644]
shared/models/videos/video-blacklist.model.ts
shared/models/videos/video.model.ts

index 08501d872d6b365c1f664b848c067203951edde9..aa0e18c70e94db873b2126a09de90fd5b6a71a3e 100644 (file)
@@ -9,7 +9,7 @@
   <ng-template pTemplate="header">
     <tr>
       <th style="width: 40px"></th>
-      <th i18n style="width: 80px;">State</th>
+      <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
       <th i18n>Reason</th>
       <th i18n>Reporter</th>
       <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
index 04f0e3b5c243479dacc80e68f3e8f219cf88a242..78989dc586f5e269dce41ae12657b99144034967 100644 (file)
@@ -4,30 +4,43 @@
 
 <p-table
   [value]="blacklist" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
+  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
 >
   <ng-template pTemplate="header">
     <tr>
-      <th i18n pSortableColumn="name">Name <p-sortIcon field="name"></p-sortIcon></th>
-      <th i18n>Description</th>
-      <th i18n pSortableColumn="views">Views <p-sortIcon field="views"></p-sortIcon></th>
+      <th style="width: 40px"></th>
+      <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
       <th i18n>NSFW</th>
       <th i18n>UUID</th>
-      <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th></th>
+      <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
+      <th style="width: 50px;"></th>
     </tr>
   </ng-template>
 
-  <ng-template pTemplate="body" let-videoBlacklist>
+  <ng-template pTemplate="body" let-videoBlacklist let-expanded="expanded">
     <tr>
-      <td>{{ videoBlacklist.name }}</td>
-      <td>{{ videoBlacklist.description }}</td>
-      <td>{{ videoBlacklist.views }}</td>
-      <td>{{ videoBlacklist.nsfw }}</td>
-      <td>{{ videoBlacklist.uuid }}</td>
+      <td>
+        <span *ngIf="videoBlacklist.reason" class="expander" [pRowToggler]="videoBlacklist">
+          <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
+        </span>
+      </td>
+
+      <td>{{ videoBlacklist.video.name }}</td>
+      <td>{{ videoBlacklist.video.nsfw }}</td>
+      <td>{{ videoBlacklist.video.uuid }}</td>
       <td>{{ videoBlacklist.createdAt }}</td>
+
       <td class="action-cell">
-        <my-delete-button i18n-label label="Unblacklist" (click)="removeVideoFromBlacklist(videoBlacklist)"></my-delete-button>
+        <my-action-dropdown i18n-label label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
+      </td>
+    </tr>
+  </ng-template>
+
+  <ng-template pTemplate="rowexpansion" let-videoBlacklist>
+    <tr class="blacklist-reason">
+      <td colspan="6">
+        <span i18n class="blacklist-reason-label">Blacklist reason:</span>
+        {{ videoBlacklist.reason }}
       </td>
     </tr>
   </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 (file)
index 0000000..5265536
--- /dev/null
@@ -0,0 +1,6 @@
+@import '_variables';
+@import '_mixins';
+
+.blacklist-reason-label {
+  font-weight: $font-semibold;
+}
\ No newline at end of file
index 143ec8406fbcc45abb65fe6cf4d60ae3f9000d97..00b0ac57ef1a4a58e5ffb266c1649af7f56bb5b0 100644 (file)
@@ -5,11 +5,12 @@ import { ConfirmService } from '../../../core'
 import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
 import { BlacklistedVideo } from '../../../../../../shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
 
 @Component({
   selector: 'my-video-blacklist-list',
   templateUrl: './video-blacklist-list.component.html',
-  styleUrls: []
+  styleUrls: [ './video-blacklist-list.component.scss' ]
 })
 export class VideoBlacklistListComponent extends RestTable implements OnInit {
   blacklist: BlacklistedVideo[] = []
@@ -18,6 +19,8 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
   sort: SortMeta = { field: 'createdAt', order: 1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
+  videoBlacklistActions: DropdownAction<BlacklistedVideo>[] = []
+
   constructor (
     private notificationsService: NotificationsService,
     private confirmService: ConfirmService,
@@ -25,6 +28,13 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
     private i18n: I18n
   ) {
     super()
+
+    this.videoBlacklistActions = [
+      {
+        label: this.i18n('Unblacklist'),
+        handler: videoBlacklist => this.removeVideoFromBlacklist(videoBlacklist)
+      }
+    ]
   }
 
   ngOnInit () {
@@ -33,17 +43,17 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
 
   async removeVideoFromBlacklist (entry: BlacklistedVideo) {
     const confirmMessage = this.i18n(
-      'Do you really want to remove this video from the blacklist ? It will be available again in the videos list.'
+      'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
     )
 
     const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist'))
     if (res === false) return
 
-    this.videoBlacklistService.removeVideoFromBlacklist(entry.videoId).subscribe(
+    this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe(
       () => {
         this.notificationsService.success(
           this.i18n('Success'),
-          this.i18n('Video {{name}} removed from the blacklist.', { name: entry.name })
+          this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name })
         )
         this.loadData()
       },
index 60d735ef705b7ac8e19d89a9eddfa97b252c2277..9bc7615ca81411edffbaee3d344619052cb2207f 100644 (file)
@@ -5,6 +5,7 @@ export * from './login-validators.service'
 export * from './reset-password-validators.service'
 export * from './user-validators.service'
 export * from './video-abuse-validators.service'
+export * from './video-blacklist-validators.service'
 export * from './video-channel-validators.service'
 export * from './video-comment-validators.service'
 export * 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 (file)
index 0000000..07d1f26
--- /dev/null
@@ -0,0 +1,19 @@
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { Validators } from '@angular/forms'
+import { Injectable } from '@angular/core'
+import { BuildFormValidator } from '@app/shared'
+
+@Injectable()
+export class VideoBlacklistValidatorsService {
+  readonly VIDEO_BLACKLIST_REASON: BuildFormValidator
+
+  constructor (private i18n: I18n) {
+    this.VIDEO_BLACKLIST_REASON = {
+      VALIDATORS: [ Validators.minLength(2), Validators.maxLength(300) ],
+      MESSAGES: {
+        'minlength': this.i18n('Blacklist reason must be at least 2 characters long.'),
+        'maxlength': this.i18n('Blacklist reason cannot be more than 300 characters long.')
+      }
+    }
+  }
+}
index ea7f2c88769309f0f9cb7a0942a41be837fdf24f..722415a06bf3e22011b0bcbca487eaea8af64d70 100644 (file)
@@ -36,7 +36,7 @@ import {
   ReactiveFileComponent,
   ResetPasswordValidatorsService,
   UserValidatorsService,
-  VideoAbuseValidatorsService,
+  VideoAbuseValidatorsService, VideoBlacklistValidatorsService,
   VideoChannelValidatorsService,
   VideoCommentValidatorsService,
   VideoValidatorsService
@@ -133,6 +133,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
     MarkdownService,
     VideoChannelService,
     VideoCaptionService,
+    VideoImportService,
 
     FormValidatorService,
     CustomConfigValidatorsService,
@@ -144,7 +145,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
     VideoCommentValidatorsService,
     VideoValidatorsService,
     VideoCaptionsValidatorsService,
-    VideoImportService,
+    VideoBlacklistValidatorsService,
 
     I18nPrimengCalendarService,
     ScreenService,
index 040d82c9a0f9a8ade1b3a8fcdcc029203afbb346..a014260b1203edc39904f4a063db4406dcc5bb2d 100644 (file)
@@ -36,8 +36,10 @@ export class VideoBlacklistService {
                )
   }
 
-  blacklistVideo (videoId: number) {
-    return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', {})
+  blacklistVideo (videoId: number, reason?: string) {
+    const body = reason ? { reason } : {}
+
+    return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body)
                .pipe(
                  map(this.restExtractor.extractDataBool),
                  catchError(res => this.restExtractor.handleError(res))
index e500ad6fc37208ef22c81aa92d8f1768b4cfbace..bdcc0bbba0ba1b0038f68ecd0948f93451cf6541 100644 (file)
@@ -44,7 +44,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
   }
 
   isBlackistableBy (user: AuthUser) {
-    return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true && this.isLocal === false
+    return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
   }
 
   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 (file)
index 0000000..c436501
--- /dev/null
@@ -0,0 +1,31 @@
+<ng-template #modal>
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Blacklist video</h4>
+    <span class="close" aria-label="Close" role="button" (click)="hide()"></span>
+  </div>
+
+  <div class="modal-body">
+
+    <form novalidate [formGroup]="form" (ngSubmit)="blacklist()">
+      <div class="form-group">
+        <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
+        </textarea>
+        <div *ngIf="formErrors.reason" class="form-error">
+          {{ formErrors.reason }}
+        </div>
+      </div>
+
+      <div class="form-group inputs">
+        <span i18n class="action-button action-button-cancel" (click)="hide()">
+          Cancel
+        </span>
+
+        <input
+          type="submit" i18n-value value="Submit" class="action-button-submit"
+          [disabled]="!form.valid"
+        >
+      </div>
+    </form>
+
+  </div>
+</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 (file)
index 0000000..afcdb9a
--- /dev/null
@@ -0,0 +1,6 @@
+@import 'variables';
+@import 'mixins';
+
+textarea {
+  @include peertube-textarea(100%, 100px);
+}
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 (file)
index 0000000..2c123eb
--- /dev/null
@@ -0,0 +1,66 @@
+import { Component, Input, OnInit, ViewChild } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index'
+import { VideoDetails } from '../../../shared/video/video-details.model'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
+import { RedirectService } from '@app/core'
+
+@Component({
+  selector: 'my-video-blacklist',
+  templateUrl: './video-blacklist.component.html',
+  styleUrls: [ './video-blacklist.component.scss' ]
+})
+export class VideoBlacklistComponent extends FormReactive implements OnInit {
+  @Input() video: VideoDetails = null
+
+  @ViewChild('modal') modal: NgbModal
+
+  error: string = null
+
+  private openedModal: NgbModalRef
+
+  constructor (
+    protected formValidatorService: FormValidatorService,
+    private modalService: NgbModal,
+    private videoBlacklistValidatorsService: VideoBlacklistValidatorsService,
+    private videoBlacklistService: VideoBlacklistService,
+    private notificationsService: NotificationsService,
+    private redirectService: RedirectService,
+    private i18n: I18n
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.buildForm({
+      reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON
+    })
+  }
+
+  show () {
+    this.openedModal = this.modalService.open(this.modal, { keyboard: false })
+  }
+
+  hide () {
+    this.openedModal.close()
+    this.openedModal = null
+  }
+
+  blacklist () {
+    const reason = this.form.value[ 'reason' ] || undefined
+
+    this.videoBlacklistService.blacklistVideo(this.video.id, reason)
+        .subscribe(
+          () => {
+            this.notificationsService.success(this.i18n('Success'), this.i18n('Video blacklisted.'))
+            this.hide()
+            this.redirectService.redirectToHomepage()
+          },
+
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
+  }
+}
index dd0d628bdd524c7a1adb23421274f75943aa332a..f82f1c5542c5abdc8e3e0d3739a30d41123844e0 100644 (file)
                   <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container>
                 </a>
 
-                <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
-                  <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
-                </a>
-
                 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
                   <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container>
                 </a>
 
+                <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)">
+                  <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
+                </a>
+
                 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
-                  <span class="icon icon-blacklist"></span> <ng-container i18n>Delete</ng-container>
+                  <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container>
                 </a>
               </div>
             </div>
   <my-video-share #videoShareModal [video]="video"></my-video-share>
   <my-video-download #videoDownloadModal [video]="video"></my-video-download>
   <my-video-report #videoReportModal [video]="video"></my-video-report>
+  <my-video-blacklist #videoBlacklistModal [video]="video"></my-video-blacklist>
 </ng-template>
index 7d269b31f698a21ae3717eb9d08bf3f72e782eb8..e63ab7bbd86275f92023fad05959039794309c80 100644 (file)
                 &.icon-blacklist {
                   background-image: url('../../../assets/images/video/blacklist.svg');
                 }
+
+                &.icon-delete {
+                  background-image: url('../../../assets/images/global/delete-black.svg');
+                }
               }
             }
           }
index 04bcc6cd1f4ed744a45a364cad221c6ca433bbaf..878655d4a898f0b6e6500e4e3ffa18357e98d189 100644 (file)
@@ -21,6 +21,7 @@ import { MarkdownService } from '../shared'
 import { VideoDownloadComponent } from './modal/video-download.component'
 import { VideoReportComponent } from './modal/video-report.component'
 import { VideoShareComponent } from './modal/video-share.component'
+import { VideoBlacklistComponent } from './modal/video-blacklist.component'
 import { addContextMenu, getVideojsOptions, loadLocale } from '../../../assets/player/peertube-player'
 import { ServerService } from '@app/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -41,6 +42,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
   @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
   @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
+  @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
 
   otherVideosDisplayed: Video[] = []
 
@@ -156,26 +158,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     }
   }
 
-  async blacklistVideo (event: Event) {
-    event.preventDefault()
-
-    const res = await this.confirmService.confirm(this.i18n('Do you really want to blacklist this video?'), this.i18n('Blacklist'))
-    if (res === false) return
-
-    this.videoBlacklistService.blacklistVideo(this.video.id)
-        .subscribe(
-          () => {
-            this.notificationsService.success(
-              this.i18n('Success'),
-              this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
-            )
-            this.redirectService.redirectToHomepage()
-          },
-
-          error => this.notificationsService.error(this.i18n('Error'), error.message)
-        )
-  }
-
   showMoreDescription () {
     if (this.completeVideoDescription === undefined) {
       return this.loadCompleteDescription()
@@ -230,6 +212,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.videoDownloadModal.show()
   }
 
+  showBlacklistModal (event: Event) {
+    event.preventDefault()
+    this.videoBlacklistModal.show()
+  }
+
   isUserLoggedIn () {
     return this.authService.isLoggedIn()
   }
index 09d5133e4dfd3176712ffeefd42dfe6c37a98862..7730919fe0a36621a039fd5323381413a2bc5feb 100644 (file)
@@ -15,6 +15,7 @@ import { VideoWatchRoutingModule } from './video-watch-routing.module'
 import { VideoWatchComponent } from './video-watch.component'
 import { NgxQRCodeModule } from 'ngx-qrcode2'
 import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
+import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component'
 
 @NgModule({
   imports: [
@@ -31,6 +32,7 @@ import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
     VideoDownloadComponent,
     VideoShareComponent,
     VideoReportComponent,
+    VideoBlacklistComponent,
     VideoSupportComponent,
     VideoCommentsComponent,
     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 (file)
index 0000000..04ddc23
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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">
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Artboard-4" transform="translate(-224.000000, -159.000000)">
+            <g id="25" transform="translate(224.000000, 159.000000)">
+                <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>
+                <rect id="Rectangle-424" fill="#000" x="2" y="4" width="20" height="2" rx="1"></rect>
+                <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>
+                <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>
+            </g>
+        </g>
+    </g>
+</svg>
index 0e2be71231b75adc02bc9642936c780518ded90e..543b20baa647604a9339a62c370b77419ab49761 100644 (file)
@@ -201,14 +201,14 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
     user.Account.id,
     req.query.start as number,
     req.query.count as number,
-    req.query.sort as VideoSortField,
-    false // Display my NSFW videos
+    req.query.sort as VideoSortField
   )
 
   const additionalAttributes = {
     waitTranscoding: true,
     state: true,
-    scheduledUpdate: true
+    scheduledUpdate: true,
+    blacklistInfo: true
   }
   return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
 }
index 8112b59b804464e6eb0fc3653e81d8d6f5876fcc..358f339edaf8d46b207c6188607ca4089536a2cd 100644 (file)
@@ -1,12 +1,21 @@
 import * as express from 'express'
-import { BlacklistedVideo, UserRight } from '../../../../shared'
+import { BlacklistedVideo, UserRight, VideoBlacklistCreate } from '../../../../shared'
 import { logger } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
 import {
-  asyncMiddleware, authenticate, blacklistSortValidator, ensureUserHasRight, paginationValidator, setBlacklistSort, setDefaultPagination,
-  videosBlacklistAddValidator, videosBlacklistRemoveValidator
+  asyncMiddleware,
+  authenticate,
+  blacklistSortValidator,
+  ensureUserHasRight,
+  paginationValidator,
+  setBlacklistSort,
+  setDefaultPagination,
+  videosBlacklistAddValidator,
+  videosBlacklistRemoveValidator,
+  videosBlacklistUpdateValidator
 } from '../../../middlewares'
 import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
+import { sequelizeTypescript } from '../../../initializers'
 
 const blacklistRouter = express.Router()
 
@@ -27,6 +36,13 @@ blacklistRouter.get('/blacklist',
   asyncMiddleware(listBlacklist)
 )
 
+blacklistRouter.put('/:videoId/blacklist',
+  authenticate,
+  ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
+  asyncMiddleware(videosBlacklistUpdateValidator),
+  asyncMiddleware(updateVideoBlacklistController)
+)
+
 blacklistRouter.delete('/:videoId/blacklist',
   authenticate,
   ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
@@ -42,17 +58,32 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function addVideoToBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
+async function addVideoToBlacklist (req: express.Request, res: express.Response) {
   const videoInstance = res.locals.video
+  const body: VideoBlacklistCreate = req.body
 
   const toCreate = {
-    videoId: videoInstance.id
+    videoId: videoInstance.id,
+    reason: body.reason
   }
 
   await VideoBlacklistModel.create(toCreate)
   return res.type('json').status(204).end()
 }
 
+async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
+  const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel
+  logger.info(videoBlacklist)
+
+  if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason
+
+  await sequelizeTypescript.transaction(t => {
+    return videoBlacklist.save({ transaction: t })
+  })
+
+  return res.type('json').status(204).end()
+}
+
 async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
   const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort)
 
@@ -60,16 +91,13 @@ async function listBlacklist (req: express.Request, res: express.Response, next:
 }
 
 async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {
-  const blacklistedVideo = res.locals.blacklistedVideo as VideoBlacklistModel
+  const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel
 
-  try {
-    await blacklistedVideo.destroy()
+  await sequelizeTypescript.transaction(t => {
+    return videoBlacklist.destroy({ transaction: t })
+  })
 
-    logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
+  logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
 
-    return res.sendStatus(204)
-  } catch (err) {
-    logger.error('Some error while removing video %s from blacklist.', res.locals.video.uuid, { err })
-    throw err
-  }
+  return res.type('json').status(204).end()
 }
diff --git a/server/helpers/custom-validators/video-blacklist.ts b/server/helpers/custom-validators/video-blacklist.ts
new file mode 100644 (file)
index 0000000..b36b08d
--- /dev/null
@@ -0,0 +1,32 @@
+import { Response } from 'express'
+import * as validator from 'validator'
+import { CONSTRAINTS_FIELDS } from '../../initializers'
+import { VideoBlacklistModel } from '../../models/video/video-blacklist'
+
+const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
+
+function isVideoBlacklistReasonValid (value: string) {
+  return value === null || validator.isLength(value, VIDEO_BLACKLIST_CONSTRAINTS_FIELDS.REASON)
+}
+
+async function isVideoBlacklistExist (videoId: number, res: Response) {
+  const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
+
+  if (videoBlacklist === null) {
+    res.status(404)
+       .json({ error: 'Blacklisted video not found' })
+       .end()
+
+    return false
+  }
+
+  res.locals.videoBlacklist = videoBlacklist
+  return true
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isVideoBlacklistReasonValid,
+  isVideoBlacklistExist
+}
index a008bf4c57905cd39b117ae3cebbd22953ac97f5..ff8e64330f9153d7270df910ba9469dbc046607c 100644 (file)
@@ -15,7 +15,7 @@ let config: IConfig = require('config')
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 250
+const LAST_MIGRATION_VERSION = 255
 
 // ---------------------------------------------------------------------------
 
@@ -34,7 +34,7 @@ const SORTABLE_COLUMNS = {
   USERS: [ 'id', 'username', 'createdAt' ],
   ACCOUNTS: [ 'createdAt' ],
   JOBS: [ 'createdAt' ],
-  VIDEO_ABUSES: [ 'id', 'createdAt' ],
+  VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],
   VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
   VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ],
   VIDEO_IMPORTS: [ 'createdAt' ],
@@ -261,6 +261,9 @@ const CONSTRAINTS_FIELDS = {
     REASON: { min: 2, max: 300 }, // Length
     MODERATION_COMMENT: { min: 2, max: 300 } // Length
   },
+  VIDEO_BLACKLIST: {
+    REASON: { min: 2, max: 300 } // Length
+  },
   VIDEO_CHANNELS: {
     NAME: { min: 3, max: 120 }, // Length
     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 (file)
index 0000000..a380e62
--- /dev/null
@@ -0,0 +1,25 @@
+import * as Sequelize from 'sequelize'
+import { CONSTRAINTS_FIELDS } from '../constants'
+import { VideoAbuseState } from '../../../shared/models/videos'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+}): Promise<any> {
+
+  {
+    const data = {
+      type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max),
+      allowNull: true,
+      defaultValue: null
+    }
+    await utils.queryInterface.addColumn('videoBlacklist', 'reason', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export { up, down }
index 3faeffd7705c3c58bd5b96c0987cbaa01781ee36..a1212878fa3acf31efe9d5ed2b53d769963be75e 100644 (file)
@@ -108,6 +108,55 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
+  async addVideoBlacklistReportJob (videoId: number, reason?: string) {
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
+    if (!video) throw new Error('Unknown Video id during Blacklist report.')
+    // It's not our user
+    if (video.remote === true) return
+
+    const user = await UserModel.loadById(video.VideoChannel.Account.userId)
+
+    const reasonString = reason ? ` for the following reason: ${reason}` : ''
+    const blockedString = `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.`
+
+    const text = 'Hi,\n\n' +
+      blockedString +
+      '\n\n' +
+      'Cheers,\n' +
+      `PeerTube.`
+
+    const to = user.email
+    const emailPayload: EmailPayload = {
+      to: [ to ],
+      subject: `[PeerTube] Video ${video.name} blacklisted`,
+      text
+    }
+
+    return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+  }
+
+  async addVideoUnblacklistReportJob (videoId: number) {
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
+    if (!video) throw new Error('Unknown Video id during Blacklist report.')
+
+    const user = await UserModel.loadById(video.VideoChannel.Account.userId)
+
+    const text = 'Hi,\n\n' +
+      `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` +
+      '\n\n' +
+      'Cheers,\n' +
+      `PeerTube.`
+
+    const to = user.email
+    const emailPayload: EmailPayload = {
+      to: [ to ],
+      subject: `[PeerTube] Video ${video.name} unblacklisted`,
+      text
+    }
+
+    return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+  }
+
   addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
     const reasonString = reason ? ` for the following reason: ${reason}` : ''
     const blockedWord = blocked ? 'blocked' : 'unblocked'
index 3c1ef1b4ed391ff391ab9321cf6454d3350256b2..95a2b9f170824919f27658380d6dfff863ddbf20 100644 (file)
@@ -1,11 +1,10 @@
 import * as express from 'express'
-import { param } from 'express-validator/check'
+import { body, param } from 'express-validator/check'
 import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
 import { isVideoExist } from '../../helpers/custom-validators/videos'
 import { logger } from '../../helpers/logger'
-import { VideoModel } from '../../models/video/video'
-import { VideoBlacklistModel } from '../../models/video/video-blacklist'
 import { areValidationErrors } from './utils'
+import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
 
 const videosBlacklistRemoveValidator = [
   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -15,7 +14,7 @@ const videosBlacklistRemoveValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await isVideoExist(req.params.videoId, res)) return
-    if (!await checkVideoIsBlacklisted(res.locals.video, res)) return
+    if (!await isVideoBlacklistExist(res.locals.video.id, res)) return
 
     return next()
   }
@@ -23,47 +22,41 @@ const videosBlacklistRemoveValidator = [
 
 const videosBlacklistAddValidator = [
   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+  body('reason')
+    .optional()
+    .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking videosBlacklist parameters', { parameters: req.params })
+    logger.debug('Checking videosBlacklistAdd parameters', { parameters: req.params })
 
     if (areValidationErrors(req, res)) return
     if (!await isVideoExist(req.params.videoId, res)) return
-    if (!checkVideoIsBlacklistable(res.locals.video, res)) return
 
     return next()
   }
 ]
 
-// ---------------------------------------------------------------------------
+const videosBlacklistUpdateValidator = [
+  param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+  body('reason')
+    .optional()
+    .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
 
-export {
-  videosBlacklistAddValidator,
-  videosBlacklistRemoveValidator
-}
-// ---------------------------------------------------------------------------
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking videosBlacklistUpdate parameters', { parameters: req.params })
 
-function checkVideoIsBlacklistable (video: VideoModel, res: express.Response) {
-  if (video.isOwned() === true) {
-    res.status(403)
-              .json({ error: 'Cannot blacklist a local video' })
-              .end()
+    if (areValidationErrors(req, res)) return
+    if (!await isVideoExist(req.params.videoId, res)) return
+    if (!await isVideoBlacklistExist(res.locals.video.id, res)) return
 
-    return false
+    return next()
   }
+]
 
-  return true
-}
-
-async function checkVideoIsBlacklisted (video: VideoModel, res: express.Response) {
-  const blacklistedVideo = await VideoBlacklistModel.loadByVideoId(video.id)
-  if (!blacklistedVideo) {
-    res.status(404)
-      .send('Blacklisted video not found')
-
-    return false
-  }
+// ---------------------------------------------------------------------------
 
-  res.locals.blacklistedVideo = blacklistedVideo
-  return true
+export {
+  videosBlacklistAddValidator,
+  videosBlacklistRemoveValidator,
+  videosBlacklistUpdateValidator
 }
index 26167174abe8ff6234045ae0ab14897f6e64e234..1b8a338cb5e334e23dd5b1611ccec08bcfa88e5e 100644 (file)
@@ -1,7 +1,23 @@
-import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import {
+  AfterCreate,
+  AfterDestroy,
+  AllowNull,
+  BelongsTo,
+  Column,
+  CreatedAt, DataType,
+  ForeignKey,
+  Is,
+  Model,
+  Table,
+  UpdatedAt
+} from 'sequelize-typescript'
 import { SortType } from '../../helpers/utils'
-import { getSortOnModel } from '../utils'
+import { getSortOnModel, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
+import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
+import { Emailer } from '../../lib/emailer'
+import { BlacklistedVideo } from '../../../shared/models/videos'
+import { CONSTRAINTS_FIELDS } from '../../initializers'
 
 @Table({
   tableName: 'videoBlacklist',
@@ -14,6 +30,11 @@ import { VideoModel } from './video'
 })
 export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
 
+  @AllowNull(true)
+  @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason'))
+  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max))
+  reason: string
+
   @CreatedAt
   createdAt: Date
 
@@ -32,6 +53,16 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
   })
   Video: VideoModel
 
+  @AfterCreate
+  static sendBlacklistEmailNotification (instance: VideoBlacklistModel) {
+    return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason)
+  }
+
+  @AfterDestroy
+  static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) {
+    return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId)
+  }
+
   static listForApi (start: number, count: number, sort: SortType) {
     const query = {
       offset: start,
@@ -59,22 +90,26 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
     return VideoBlacklistModel.findOne(query)
   }
 
-  toFormattedJSON () {
+  toFormattedJSON (): BlacklistedVideo {
     const video = this.Video
 
     return {
       id: this.id,
-      videoId: this.videoId,
       createdAt: this.createdAt,
       updatedAt: this.updatedAt,
-      name: video.name,
-      uuid: video.uuid,
-      description: video.description,
-      duration: video.duration,
-      views: video.views,
-      likes: video.likes,
-      dislikes: video.dislikes,
-      nsfw: video.nsfw
+      reason: this.reason,
+
+      video: {
+        id: video.id,
+        name: video.name,
+        uuid: video.uuid,
+        description: video.description,
+        duration: video.duration,
+        views: video.views,
+        likes: video.likes,
+        dislikes: video.dislikes,
+        nsfw: video.nsfw
+      }
     }
   }
 }
index 39fe2100789b94b9e2ea768cadbebd72bffd12b5..f3a900bc981e9a1863f7bb77cb3de5c40d64cbd6 100644 (file)
@@ -93,6 +93,7 @@ import { VideoShareModel } from './video-share'
 import { VideoTagModel } from './video-tag'
 import { ScheduleVideoUpdateModel } from './schedule-video-update'
 import { VideoCaptionModel } from './video-caption'
+import { VideoBlacklistModel } from './video-blacklist'
 
 // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
 const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -581,6 +582,15 @@ export class VideoModel extends Model<VideoModel> {
   })
   ScheduleVideoUpdate: ScheduleVideoUpdateModel
 
+  @HasOne(() => VideoBlacklistModel, {
+    foreignKey: {
+      name: 'videoId',
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
+  VideoBlacklist: VideoBlacklistModel
+
   @HasMany(() => VideoCaptionModel, {
     foreignKey: {
       name: 'videoId',
@@ -755,7 +765,7 @@ export class VideoModel extends Model<VideoModel> {
     })
   }
 
-  static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
+  static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
     const query: IFindOptions<VideoModel> = {
       offset: start,
       limit: count,
@@ -777,6 +787,10 @@ export class VideoModel extends Model<VideoModel> {
         {
           model: ScheduleVideoUpdateModel,
           required: false
+        },
+        {
+          model: VideoBlacklistModel,
+          required: false
         }
       ]
     }
@@ -788,12 +802,6 @@ export class VideoModel extends Model<VideoModel> {
       })
     }
 
-    if (hideNSFW === true) {
-      query.where = {
-        nsfw: false
-      }
-    }
-
     return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
       return {
         data: rows,
@@ -1177,7 +1185,8 @@ export class VideoModel extends Model<VideoModel> {
     additionalAttributes: {
       state?: boolean,
       waitTranscoding?: boolean,
-      scheduledUpdate?: boolean
+      scheduledUpdate?: boolean,
+      blacklistInfo?: boolean
     }
   }): Video {
     const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
@@ -1254,6 +1263,11 @@ export class VideoModel extends Model<VideoModel> {
           privacy: this.ScheduleVideoUpdate.privacy || undefined
         }
       }
+
+      if (options.additionalAttributes.blacklistInfo === true) {
+        videoObject.blacklisted = !!this.VideoBlacklist
+        videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null
+      }
     }
 
     return videoObject
index 6cd13d23f77fe5557b6a21cfe285bf12995654ed..4154747183e7b1e20951c02e0c7710354f1e0327 100644 (file)
@@ -3,13 +3,24 @@
 import 'mocha'
 
 import {
-  createUser, flushTests, getBlacklistedVideosList, killallServers, makePostBodyRequest, removeVideoFromBlacklist, runServer,
-  ServerInfo, setAccessTokensToServers, uploadVideo, userLogin
+  createUser,
+  flushTests,
+  getBlacklistedVideosList,
+  killallServers,
+  makePostBodyRequest,
+  makePutBodyRequest,
+  removeVideoFromBlacklist,
+  runServer,
+  ServerInfo,
+  setAccessTokensToServers,
+  uploadVideo,
+  userLogin
 } from '../../utils'
 import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
 
 describe('Test video blacklist API validators', function () {
   let server: ServerInfo
+  let notBlacklistedVideoId: number
   let userAccessToken = ''
 
   // ---------------------------------------------------------------
@@ -28,8 +39,15 @@ describe('Test video blacklist API validators', function () {
     await createUser(server.url, server.accessToken, username, password)
     userAccessToken = await userLogin(server, { username, password })
 
-    const res = await uploadVideo(server.url, server.accessToken, {})
-    server.video = res.body.video
+    {
+      const res = await uploadVideo(server.url, server.accessToken, {})
+      server.video = res.body.video
+    }
+
+    {
+      const res = await uploadVideo(server.url, server.accessToken, {})
+      notBlacklistedVideoId = res.body.video.uuid
+    }
   })
 
   describe('When adding a video in blacklist', function () {
@@ -59,20 +77,70 @@ describe('Test video blacklist API validators', function () {
       await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: 403 })
     })
 
-    it('Should fail with a local video', async function () {
-      const path = basePath + server.video.id + '/blacklist'
+    it('Should fail with an invalid reason', async function () {
+      const path = basePath + server.video.uuid + '/blacklist'
+      const fields = { reason: 'a'.repeat(305) }
+
+      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+    })
+
+    it('Should succeed with the correct params', async function () {
+      const path = basePath + server.video.uuid + '/blacklist'
+      const fields = { }
+
+      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 })
+    })
+  })
+
+  describe('When updating a video in blacklist', function () {
+    const basePath = '/api/v1/videos/'
+
+    it('Should fail with a wrong video', async function () {
+      const wrongPath = '/api/v1/videos/blabla/blacklist'
+      const fields = {}
+      await makePutBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields })
+    })
+
+    it('Should fail with a video not blacklisted', async function () {
+      const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist'
       const fields = {}
-      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 403 })
+      await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 404 })
+    })
+
+    it('Should fail with a non authenticated user', async function () {
+      const path = basePath + server.video + '/blacklist'
+      const fields = {}
+      await makePutBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 })
+    })
+
+    it('Should fail with a non admin user', async function () {
+      const path = basePath + server.video + '/blacklist'
+      const fields = {}
+      await makePutBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: 403 })
+    })
+
+    it('Should fail with an invalid reason', async function () {
+      const path = basePath + server.video.uuid + '/blacklist'
+      const fields = { reason: 'a'.repeat(305) }
+
+      await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+    })
+
+    it('Should succeed with the correct params', async function () {
+      const path = basePath + server.video.uuid + '/blacklist'
+      const fields = { reason: 'hello' }
+
+      await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 })
     })
   })
 
   describe('When removing a video in blacklist', function () {
     it('Should fail with a non authenticated user', async function () {
-      await removeVideoFromBlacklist(server.url, 'fake token', server.video.id, 401)
+      await removeVideoFromBlacklist(server.url, 'fake token', server.video.uuid, 401)
     })
 
     it('Should fail with a non admin user', async function () {
-      await removeVideoFromBlacklist(server.url, userAccessToken, server.video.id, 403)
+      await removeVideoFromBlacklist(server.url, userAccessToken, server.video.uuid, 403)
     })
 
     it('Should fail with an incorrect id', async function () {
@@ -81,7 +149,11 @@ describe('Test video blacklist API validators', function () {
 
     it('Should fail with a not blacklisted video', async function () {
       // The video was not added to the blacklist so it should fail
-      await removeVideoFromBlacklist(server.url, server.accessToken, server.video.id, 404)
+      await removeVideoFromBlacklist(server.url, server.accessToken, notBlacklistedVideoId, 404)
+    })
+
+    it('Should succeed with the correct params', async function () {
+      await removeVideoFromBlacklist(server.url, server.accessToken, server.video.uuid, 204)
     })
   })
 
index 65d6a759f6507915142cfb36982ed12d844de3e5..db937f2884f6dfe1fb40b330733eb53c15aea5f0 100644 (file)
@@ -3,9 +3,10 @@
 import * as chai from 'chai'
 import 'mocha'
 import {
+  addVideoToBlacklist,
   askResetPassword,
   blockUser,
-  createUser,
+  createUser, removeVideoFromBlacklist,
   reportVideoAbuse,
   resetPassword,
   runServer,
@@ -22,7 +23,9 @@ const expect = chai.expect
 describe('Test emails', function () {
   let server: ServerInfo
   let userId: number
+  let userAccessToken: string
   let videoUUID: string
+  let videoUserUUID: string
   let verificationString: string
   const emails: object[] = []
   const user = {
@@ -48,6 +51,16 @@ describe('Test emails', function () {
     {
       const res = await createUser(server.url, server.accessToken, user.username, user.password)
       userId = res.body.user.id
+
+      userAccessToken = await userLogin(server, user)
+    }
+
+    {
+      const attributes = {
+        name: 'my super user video'
+      }
+      const res = await uploadVideo(server.url, userAccessToken, attributes)
+      videoUserUUID = res.body.video.uuid
     }
 
     {
@@ -158,6 +171,42 @@ describe('Test emails', function () {
     })
   })
 
+  describe('When blacklisting a video', function () {
+    it('Should send the notification email', async function () {
+      this.timeout(10000)
+
+      const reason = 'my super reason'
+      await addVideoToBlacklist(server.url, server.accessToken, videoUserUUID, reason)
+
+      await waitJobs(server)
+      expect(emails).to.have.lengthOf(5)
+
+      const email = emails[4]
+
+      expect(email['from'][0]['address']).equal('test-admin@localhost')
+      expect(email['to'][0]['address']).equal('user_1@example.com')
+      expect(email['subject']).contains(' blacklisted')
+      expect(email['text']).contains('my super user video')
+      expect(email['text']).contains('my super reason')
+    })
+
+    it('Should send the notification email', async function () {
+      this.timeout(10000)
+
+      await removeVideoFromBlacklist(server.url, server.accessToken, videoUserUUID)
+
+      await waitJobs(server)
+      expect(emails).to.have.lengthOf(6)
+
+      const email = emails[5]
+
+      expect(email['from'][0]['address']).equal('test-admin@localhost')
+      expect(email['to'][0]['address']).equal('user_1@example.com')
+      expect(email['subject']).contains(' unblacklisted')
+      expect(email['text']).contains('my super user video')
+    })
+  })
+
   after(async function () {
     killallServers([ server ])
   })
index 4d1a064366a20964b0af9caa4a445b0ce9935a35..7bf39dc9962fbb838ad23cee8c4290d63485b832 100644 (file)
@@ -1,4 +1,4 @@
-/* tslint:disable:no-unused-expressions */
+/* tslint:disable:no-unused-expression */
 
 import * as chai from 'chai'
 import * as lodash from 'lodash'
@@ -7,29 +7,33 @@ import {
   addVideoToBlacklist,
   flushAndRunMultipleServers,
   getBlacklistedVideosList,
+  getMyVideos,
   getSortedBlacklistedVideosList,
   getVideosList,
   killallServers,
   removeVideoFromBlacklist,
   ServerInfo,
   setAccessTokensToServers,
+  updateVideoBlacklist,
   uploadVideo
 } from '../../utils/index'
 import { doubleFollow } from '../../utils/server/follows'
 import { waitJobs } from '../../utils/server/jobs'
+import { VideoAbuse } from '../../../../shared/models/videos'
 
 const expect = chai.expect
 const orderBy = lodash.orderBy
 
 describe('Test video blacklist management', function () {
   let servers: ServerInfo[] = []
+  let videoId: number
 
   async function blacklistVideosOnServer (server: ServerInfo) {
     const res = await getVideosList(server.url)
 
     const videos = res.body.data
     for (let video of videos) {
-      await addVideoToBlacklist(server.url, server.accessToken, video.id)
+      await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason')
     }
   }
 
@@ -62,53 +66,85 @@ describe('Test video blacklist management', function () {
 
       expect(res.body.total).to.equal(2)
 
-      const videos = res.body.data
-      expect(videos).to.be.an('array')
-      expect(videos.length).to.equal(2)
+      const blacklistedVideos = res.body.data
+      expect(blacklistedVideos).to.be.an('array')
+      expect(blacklistedVideos.length).to.equal(2)
+
+      for (const blacklistedVideo of blacklistedVideos) {
+        expect(blacklistedVideo.reason).to.equal('super reason')
+        videoId = blacklistedVideo.video.id
+      }
     })
 
     it('Should get the correct sort when sorting by descending id', async function () {
       const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id')
       expect(res.body.total).to.equal(2)
 
-      const videos = res.body.data
-      expect(videos).to.be.an('array')
-      expect(videos.length).to.equal(2)
+      const blacklistedVideos = res.body.data
+      expect(blacklistedVideos).to.be.an('array')
+      expect(blacklistedVideos.length).to.equal(2)
 
       const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ])
 
-      expect(videos).to.deep.equal(result)
+      expect(blacklistedVideos).to.deep.equal(result)
     })
 
     it('Should get the correct sort when sorting by descending video name', async function () {
       const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name')
       expect(res.body.total).to.equal(2)
 
-      const videos = res.body.data
-      expect(videos).to.be.an('array')
-      expect(videos.length).to.equal(2)
+      const blacklistedVideos = res.body.data
+      expect(blacklistedVideos).to.be.an('array')
+      expect(blacklistedVideos.length).to.equal(2)
 
       const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ])
 
-      expect(videos).to.deep.equal(result)
+      expect(blacklistedVideos).to.deep.equal(result)
     })
 
     it('Should get the correct sort when sorting by ascending creation date', async function () {
       const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt')
       expect(res.body.total).to.equal(2)
 
-      const videos = res.body.data
-      expect(videos).to.be.an('array')
-      expect(videos.length).to.equal(2)
+      const blacklistedVideos = res.body.data
+      expect(blacklistedVideos).to.be.an('array')
+      expect(blacklistedVideos.length).to.equal(2)
 
       const result = orderBy(res.body.data, [ 'createdAt' ])
 
-      expect(videos).to.deep.equal(result)
+      expect(blacklistedVideos).to.deep.equal(result)
+    })
+  })
+
+  describe('When updating blacklisted videos', function () {
+    it('Should change the reason', async function () {
+      await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated')
+
+      const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name')
+      const video = res.body.data.find(b => b.video.id === videoId)
+
+      expect(video.reason).to.equal('my super reason updated')
+    })
+  })
+
+  describe('When listing my videos', function () {
+    it('Should display blacklisted videos', async function () {
+      await blacklistVideosOnServer(servers[1])
+
+      const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 5)
+
+      expect(res.body.total).to.equal(2)
+      expect(res.body.data).to.have.lengthOf(2)
+
+      for (const video of res.body.data) {
+        expect(video.blacklisted).to.be.true
+        expect(video.blacklistedReason).to.equal('super reason')
+      }
     })
   })
 
   describe('When removing a blacklisted video', function () {
-    let videoToRemove
+    let videoToRemove: VideoAbuse
     let blacklist = []
 
     it('Should not have any video in videos list on server 1', async function () {
@@ -125,7 +161,7 @@ describe('Test video blacklist management', function () {
       blacklist = res.body.data.slice(1)
 
       // Remove it
-      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.videoId)
+      await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.video.id)
     })
 
     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 () {
       expect(videos).to.be.an('array')
       expect(videos.length).to.equal(1)
 
-      expect(videos[0].name).to.equal(videoToRemove.name)
-      expect(videos[0].id).to.equal(videoToRemove.videoId)
+      expect(videos[0].name).to.equal(videoToRemove.video.name)
+      expect(videos[0].id).to.equal(videoToRemove.video.id)
     })
 
     it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () {
index aa0d232b6c5b23ef6fb503f5608c7024fea9400f..7819f4b25bcffec8e2ac20b67934b9e83859c71f 100644 (file)
@@ -1,15 +1,26 @@
 import * as request from 'supertest'
 
-function addVideoToBlacklist (url: string, token: string, videoId: number, specialStatus = 204) {
+function addVideoToBlacklist (url: string, token: string, videoId: number | string, reason?: string, specialStatus = 204) {
   const path = '/api/v1/videos/' + videoId + '/blacklist'
 
   return request(url)
           .post(path)
+          .send({ reason })
           .set('Accept', 'application/json')
           .set('Authorization', 'Bearer ' + token)
           .expect(specialStatus)
 }
 
+function updateVideoBlacklist (url: string, token: string, videoId: number, reason?: string, specialStatus = 204) {
+  const path = '/api/v1/videos/' + videoId + '/blacklist'
+
+  return request(url)
+    .put(path)
+    .send({ reason })
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + token)
+    .expect(specialStatus)}
+
 function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) {
   const path = '/api/v1/videos/' + videoId + '/blacklist'
 
@@ -50,5 +61,6 @@ export {
   addVideoToBlacklist,
   removeVideoFromBlacklist,
   getBlacklistedVideosList,
-  getSortedBlacklistedVideosList
+  getSortedBlacklistedVideosList,
+  updateVideoBlacklist
 }
index 02bf2b84248057b4d2ac2f8167b695ede78cb94c..b99dd2d8fdb1058c3715f1bad00a8ba71bd98535 100644 (file)
@@ -6,6 +6,8 @@ export * from './video-abuse-create.model'
 export * from './video-abuse.model'
 export * from './video-abuse-update.model'
 export * from './video-blacklist.model'
+export * from './video-blacklist-create.model'
+export * from './video-blacklist-update.model'
 export * from './video-channel-create.model'
 export * from './video-channel-update.model'
 export * 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 (file)
index 0000000..89c69cb
--- /dev/null
@@ -0,0 +1,3 @@
+export interface VideoBlacklistCreate {
+  reason?: string
+}
diff --git a/shared/models/videos/video-blacklist-update.model.ts b/shared/models/videos/video-blacklist-update.model.ts
new file mode 100644 (file)
index 0000000..0a86cf7
--- /dev/null
@@ -0,0 +1,3 @@
+export interface VideoBlacklistUpdate {
+  reason?: string
+}
index af04502e8b61f682fb12e4f91b43560020a996e0..a060da357bbbb4c857716ed6aa4d60d7e0e9c9af 100644 (file)
@@ -1,14 +1,18 @@
 export interface BlacklistedVideo {
   id: number
-  videoId: number
   createdAt: Date
   updatedAt: Date
-  name: string
-  uuid: string
-  description: string
-  duration: number
-  views: number
-  likes: number
-  dislikes: number
-  nsfw: boolean
+  reason?: string
+
+  video: {
+    id: number
+    name: string
+    uuid: string
+    description: string
+    duration: number
+    views: number
+    likes: number
+    dislikes: number
+    nsfw: boolean
+  }
 }
index f7bbaac76bd13d7b0e6fcddde2e0eadea5b05dcb..8dfa96069283529a1bbd0ef5634f7b1d00e79dbb 100644 (file)
@@ -43,6 +43,9 @@ export interface Video {
   state?: VideoConstant<VideoState>
   scheduledUpdate?: VideoScheduleUpdate
 
+  blacklisted?: boolean
+  blacklistedReason?: string
+
   account: {
     id: number
     uuid: string