From 26b7305a232e547709f433a6edf700bf495935d8 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 13 Aug 2018 16:57:13 +0200 Subject: [PATCH] Add blacklist reason field --- .../video-abuse-list.component.html | 2 +- .../video-blacklist-list.component.html | 39 +++++--- .../video-blacklist-list.component.scss | 6 ++ .../video-blacklist-list.component.ts | 18 +++- .../app/shared/forms/form-validators/index.ts | 1 + .../video-blacklist-validators.service.ts | 19 ++++ client/src/app/shared/shared.module.ts | 5 +- .../video-blacklist.service.ts | 6 +- .../app/shared/video/video-details.model.ts | 2 +- .../modal/video-blacklist.component.html | 31 +++++++ .../modal/video-blacklist.component.scss | 6 ++ .../modal/video-blacklist.component.ts | 66 +++++++++++++ .../+video-watch/video-watch.component.html | 11 ++- .../+video-watch/video-watch.component.scss | 4 + .../+video-watch/video-watch.component.ts | 27 ++---- .../videos/+video-watch/video-watch.module.ts | 2 + .../src/assets/images/global/delete-black.svg | 14 +++ server/controllers/api/users.ts | 6 +- server/controllers/api/videos/blacklist.ts | 56 ++++++++--- .../custom-validators/video-blacklist.ts | 32 +++++++ server/initializers/constants.ts | 7 +- .../migrations/0255-video-blacklist-reason.ts | 25 +++++ server/lib/emailer.ts | 49 ++++++++++ .../middlewares/validators/video-blacklist.ts | 55 +++++------ server/models/video/video-blacklist.ts | 59 +++++++++--- server/models/video/video.ts | 30 ++++-- .../tests/api/check-params/video-blacklist.ts | 92 +++++++++++++++++-- server/tests/api/server/email.ts | 51 +++++++++- .../api/videos/video-blacklist-management.ts | 78 +++++++++++----- server/tests/utils/videos/video-blacklist.ts | 16 +++- shared/models/videos/index.ts | 2 + .../videos/video-blacklist-create.model.ts | 3 + .../videos/video-blacklist-update.model.ts | 3 + shared/models/videos/video-blacklist.model.ts | 22 +++-- shared/models/videos/video.model.ts | 3 + 35 files changed, 687 insertions(+), 161 deletions(-) create mode 100644 client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.scss create mode 100644 client/src/app/shared/forms/form-validators/video-blacklist-validators.service.ts create mode 100644 client/src/app/videos/+video-watch/modal/video-blacklist.component.html create mode 100644 client/src/app/videos/+video-watch/modal/video-blacklist.component.scss create mode 100644 client/src/app/videos/+video-watch/modal/video-blacklist.component.ts create mode 100644 client/src/assets/images/global/delete-black.svg create mode 100644 server/helpers/custom-validators/video-blacklist.ts create mode 100644 server/initializers/migrations/0255-video-blacklist-reason.ts create mode 100644 shared/models/videos/video-blacklist-create.model.ts create mode 100644 shared/models/videos/video-blacklist-update.model.ts 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 @@ - State + State Reason Reporter Created 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 @@ - Name - Description - Views + + Video name NSFW UUID - Created - + Date + - + - {{ videoBlacklist.name }} - {{ videoBlacklist.description }} - {{ videoBlacklist.views }} - {{ videoBlacklist.nsfw }} - {{ videoBlacklist.uuid }} + + + + + + + {{ videoBlacklist.video.name }} + {{ videoBlacklist.video.nsfw }} + {{ videoBlacklist.video.uuid }} {{ videoBlacklist.createdAt }} + - + + + + + + + + + Blacklist reason: + {{ videoBlacklist.reason }} 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 @@ +@import '_variables'; +@import '_mixins'; + +.blacklist-reason-label { + font-weight: $font-semibold; +} \ 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' 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[] = [] + 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() }, 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' 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 index 000000000..07d1f264a --- /dev/null +++ b/client/src/app/shared/forms/form-validators/video-blacklist-validators.service.ts @@ -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.') + } + } + } +} 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 { 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, 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 { ) } - 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)) 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 { } 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 index 000000000..c436501b4 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html @@ -0,0 +1,31 @@ + + + + + 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 @@ +@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 index 000000000..2c123ebed --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts @@ -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) + ) + } +} 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 @@ Report - - Blacklist - - Update + + Blacklist + + - Delete + Delete @@ -205,4 +205,5 @@ + 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 @@ &.icon-blacklist { background-image: url('../../../assets/images/video/blacklist.svg'); } + + &.icon-delete { + background-image: url('../../../assets/images/global/delete-black.svg'); + } } } } 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' 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() } 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' 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 index 000000000..04ddc23aa --- /dev/null +++ b/client/src/assets/images/global/delete-black.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + 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: 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 })) } 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 @@ 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 index 000000000..b36b08d8b --- /dev/null +++ b/server/helpers/custom-validators/video-blacklist.ts @@ -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 +} 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') // --------------------------------------------------------------------------- -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 index 000000000..a380e620e --- /dev/null +++ b/server/initializers/migrations/0255-video-blacklist-reason.ts @@ -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 { + + { + 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 } 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 { 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' 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 @@ 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 } 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 @@ -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 { + @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 { }) 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 { 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 + } } } } 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' 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 { }) 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 { }) } - 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 = { offset: start, limit: count, @@ -777,6 +787,10 @@ export class VideoModel extends Model { { model: ScheduleVideoUpdateModel, required: false + }, + { + model: VideoBlacklistModel, + required: false } ] } @@ -788,12 +802,6 @@ export class VideoModel extends Model { }) } - 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 { 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 { privacy: this.ScheduleVideoUpdate.privacy || undefined } } + + if (options.additionalAttributes.blacklistInfo === true) { + videoObject.blacklisted = !!this.VideoBlacklist + videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null + } } 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 @@ 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) }) }) 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 @@ 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 ]) }) 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 @@ -/* 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 () { 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 @@ 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 } 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' 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 index 000000000..89c69cb56 --- /dev/null +++ b/shared/models/videos/video-blacklist-create.model.ts @@ -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 index 000000000..0a86cf7b0 --- /dev/null +++ b/shared/models/videos/video-blacklist-update.model.ts @@ -0,0 +1,3 @@ +export interface VideoBlacklistUpdate { + reason?: string +} 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 @@ 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 + } } 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 { state?: VideoConstant scheduledUpdate?: VideoScheduleUpdate + blacklisted?: boolean + blacklistedReason?: string + account: { id: number uuid: string -- 2.41.0