<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>
<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>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+.blacklist-reason-label {
+ font-weight: $font-semibold;
+}
\ No newline at end of file
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[] = []
sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+ videoBlacklistActions: DropdownAction<BlacklistedVideo>[] = []
+
constructor (
private notificationsService: NotificationsService,
private confirmService: ConfirmService,
private i18n: I18n
) {
super()
+
+ this.videoBlacklistActions = [
+ {
+ label: this.i18n('Unblacklist'),
+ handler: videoBlacklist => this.removeVideoFromBlacklist(videoBlacklist)
+ }
+ ]
}
ngOnInit () {
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()
},
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'
--- /dev/null
+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.')
+ }
+ }
+ }
+}
ReactiveFileComponent,
ResetPasswordValidatorsService,
UserValidatorsService,
- VideoAbuseValidatorsService,
+ VideoAbuseValidatorsService, VideoBlacklistValidatorsService,
VideoChannelValidatorsService,
VideoCommentValidatorsService,
VideoValidatorsService
MarkdownService,
VideoChannelService,
VideoCaptionService,
+ VideoImportService,
FormValidatorService,
CustomConfigValidatorsService,
VideoCommentValidatorsService,
VideoValidatorsService,
VideoCaptionsValidatorsService,
- VideoImportService,
+ VideoBlacklistValidatorsService,
I18nPrimengCalendarService,
ScreenService,
)
}
- 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))
}
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) {
--- /dev/null
+<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>
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+textarea {
+ @include peertube-textarea(100%, 100px);
+}
--- /dev/null
+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)
+ )
+ }
+}
<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>
&.icon-blacklist {
background-image: url('../../../assets/images/video/blacklist.svg');
}
+
+ &.icon-delete {
+ background-image: url('../../../assets/images/global/delete-black.svg');
+ }
}
}
}
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'
@ViewChild('videoShareModal') videoShareModal: VideoShareComponent
@ViewChild('videoReportModal') videoReportModal: VideoReportComponent
@ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
+ @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
otherVideosDisplayed: Video[] = []
}
}
- 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()
this.videoDownloadModal.show()
}
+ showBlacklistModal (event: Event) {
+ event.preventDefault()
+ this.videoBlacklistModal.show()
+ }
+
isUserLoggedIn () {
return this.authService.isLoggedIn()
}
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: [
VideoDownloadComponent,
VideoShareComponent,
VideoReportComponent,
+ VideoBlacklistComponent,
VideoSupportComponent,
VideoCommentsComponent,
VideoCommentAddComponent,
--- /dev/null
+<?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>
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 }))
}
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()
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),
// ---------------------------------------------------------------------------
-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)
}
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()
}
--- /dev/null
+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
+}
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 250
+const LAST_MIGRATION_VERSION = 255
// ---------------------------------------------------------------------------
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' ],
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
--- /dev/null
+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 }
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'
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'),
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()
}
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
}
-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',
})
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
})
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,
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
+ }
}
}
}
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[] = [
})
ScheduleVideoUpdate: ScheduleVideoUpdateModel
+ @HasOne(() => VideoBlacklistModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoBlacklist: VideoBlacklistModel
+
@HasMany(() => VideoCaptionModel, {
foreignKey: {
name: 'videoId',
})
}
- 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,
{
model: ScheduleVideoUpdateModel,
required: false
+ },
+ {
+ model: VideoBlacklistModel,
+ required: false
}
]
}
})
}
- if (hideNSFW === true) {
- query.where = {
- nsfw: false
- }
- }
-
return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
return {
data: rows,
additionalAttributes: {
state?: boolean,
waitTranscoding?: boolean,
- scheduledUpdate?: boolean
+ scheduledUpdate?: boolean,
+ blacklistInfo?: boolean
}
}): Video {
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
privacy: this.ScheduleVideoUpdate.privacy || undefined
}
}
+
+ if (options.additionalAttributes.blacklistInfo === true) {
+ videoObject.blacklisted = !!this.VideoBlacklist
+ videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null
+ }
}
return videoObject
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 = ''
// ---------------------------------------------------------------
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 () {
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 () {
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)
})
})
import * as chai from 'chai'
import 'mocha'
import {
+ addVideoToBlacklist,
askResetPassword,
blockUser,
- createUser,
+ createUser, removeVideoFromBlacklist,
reportVideoAbuse,
resetPassword,
runServer,
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 = {
{
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
}
{
})
})
+ 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 ])
})
-/* tslint:disable:no-unused-expressions */
+/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import * as lodash from 'lodash'
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')
}
}
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 () {
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 () {
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 () {
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'
addVideoToBlacklist,
removeVideoFromBlacklist,
getBlacklistedVideosList,
- getSortedBlacklistedVideosList
+ getSortedBlacklistedVideosList,
+ updateVideoBlacklist
}
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'
--- /dev/null
+export interface VideoBlacklistCreate {
+ reason?: string
+}
--- /dev/null
+export interface VideoBlacklistUpdate {
+ reason?: string
+}
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
+ }
}
state?: VideoConstant<VideoState>
scheduledUpdate?: VideoScheduleUpdate
+ blacklisted?: boolean
+ blacklistedReason?: string
+
account: {
id: number
uuid: string