import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
import { JobService } from './jobs/shared/job.service'
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
-import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
+import {
+ ModerationCommentModalComponent,
+ VideoAbuseListComponent,
+ VideoBlacklistListComponent,
+ VideoAutoBlacklistListComponent
+} from './moderation'
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
ModerationComponent,
VideoBlacklistListComponent,
VideoAbuseListComponent,
+ VideoAutoBlacklistListComponent,
ModerationCommentModalComponent,
InstanceServerBlocklistComponent,
InstanceAccountBlocklistComponent,
</ng-container>
</ng-container>
+ <div i18n class="inner-form-title">Auto-blacklist</div>
+
+ <ng-container formGroupName="autoBlacklist">
+ <ng-container formGroupName="videos">
+ <ng-container formGroupName="ofUsers">
+
+ <div class="form-group">
+ <my-peertube-checkbox
+ inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
+ i18n-labelText labelText="New videos of users automatically blacklisted enabled"
+ ></my-peertube-checkbox>
+ </div>
+
+ </ng-container>
+ </ng-container>
+ </ng-container>
+
<div i18n class="inner-form-title">Administrator</div>
<div class="form-group" formGroupName="admin">
threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
allowAdditionalExtensions: null,
resolutions: {}
+ },
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: null
+ }
+ }
}
}
export * from './video-abuse-list'
+export * from './video-auto-blacklist-list'
export * from './video-blacklist-list'
export * from './moderation.component'
export * from './moderation.routes'
<div class="admin-sub-nav">
<a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a>
- <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a>
+ <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">{{ autoBlacklistVideosEnabled ? 'Manually blacklisted videos' : 'Blacklisted videos' }}</a>
+
+ <a *ngIf="autoBlacklistVideosEnabled && hasVideoBlacklistRight()" i18n routerLink="video-auto-blacklist/list" routerLinkActive="active">Auto-blacklisted videos</a>
<a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
import { Component } from '@angular/core'
import { UserRight } from '../../../../../shared'
-import { AuthService } from '@app/core/auth/auth.service'
+import { AuthService, ServerService } from '@app/core'
@Component({
templateUrl: './moderation.component.html',
styleUrls: [ './moderation.component.scss' ]
})
export class ModerationComponent {
- constructor (private auth: AuthService) {}
+ autoBlacklistVideosEnabled: boolean
+
+ constructor (
+ private auth: AuthService,
+ private serverService: ServerService
+ ) {
+ this.autoBlacklistVideosEnabled = this.serverService.getConfig().autoBlacklist.videos.ofUsers.enabled
+ }
hasVideoAbusesRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
import { UserRightGuard } from '@app/core'
import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
+import { VideoAutoBlacklistListComponent } from '@app/+admin/moderation/video-auto-blacklist-list'
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
redirectTo: 'video-blacklist/list',
pathMatch: 'full'
},
+ {
+ path: 'video-auto-blacklist',
+ redirectTo: 'video-auto-blacklist/list',
+ pathMatch: 'full'
+ },
{
path: 'video-abuses/list',
component: VideoAbuseListComponent,
}
}
},
+ {
+ path: 'video-auto-blacklist/list',
+ component: VideoAutoBlacklistListComponent,
+ canActivate: [ UserRightGuard ],
+ data: {
+ userRight: UserRight.MANAGE_VIDEO_BLACKLIST,
+ meta: {
+ title: 'Auto-blacklisted videos'
+ }
+ }
+ },
{
path: 'video-blacklist/list',
component: VideoBlacklistListComponent,
--- /dev/null
+export * from './video-auto-blacklist-list.component'
--- /dev/null
+<div i18n *ngIf="pagination.totalItems === 0">No results.</div>
+<div
+ myInfiniteScroller
+ [pageHeight]="pageHeight"
+ (nearOfTop)="onNearOfTop()"
+ (nearOfBottom)="onNearOfBottom()"
+ (pageChanged)="onPageChanged($event)"
+ class="videos" #videosElement
+>
+ <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
+ <div class="video" *ngFor="let video of videos; let j = index">
+ <div class="checkbox-container">
+ <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
+ </div>
+ <my-video-thumbnail [video]="video"></my-video-thumbnail>
+
+ <div class="video-info">
+ <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+ <div>{{ video.account.displayName }}</div>
+ <div>{{ video.publishedAt | myFromNow }}</div>
+ <div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
+ <div><span i18n>Sensitve: </span><span> {{ video.nsfw }}</span></div>
+ </div>
+
+ <!-- Display only once -->
+ <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
+ <div class="action-selection-mode-child">
+ <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
+ Cancel
+ </span>
+
+ <span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()">
+ <my-global-icon iconName="tick"></my-global-icon>
+ <ng-container i18n>Unblacklist</ng-container>
+ </span>
+ </div>
+ </div>
+
+ <div class="video-buttons" *ngIf="isInSelectionMode() === false">
+ <my-button
+ i18n-label
+ label="Unblacklist"
+ icon="tick"
+ (click)="removeVideoFromBlacklist(video)"
+ ></my-button>
+ </div>
+ </div>
+
+</div>
\ No newline at end of file
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+.action-selection-mode {
+ width: 194px;
+ display: flex;
+ justify-content: flex-end;
+
+ .action-selection-mode-child {
+ position: fixed;
+
+ .action-button {
+ display: inline-block;
+ }
+
+ .action-button-cancel-selection {
+ @include peertube-button;
+ @include grey-button;
+
+ margin-right: 10px;
+ }
+
+ .action-button-unblacklist-selection {
+ @include peertube-button;
+ @include orange-button;
+ @include button-with-icon(21px);
+
+ my-global-icon {
+ @include apply-svg-color(#fff);
+ }
+ }
+ }
+}
+
+.video {
+ @include row-blocks;
+
+ &:first-child {
+ margin-top: 47px;
+ }
+
+ .checkbox-container {
+ display: flex;
+ align-items: center;
+ margin-right: 20px;
+ margin-left: 12px;
+ }
+
+ my-video-thumbnail {
+ margin-right: 10px;
+ }
+
+ .video-info {
+ flex-grow: 1;
+
+ .video-info-name {
+ @include disable-default-a-behaviour;
+
+ color: var(--mainForegroundColor);
+ display: block;
+ width: fit-content;
+ font-size: 16px;
+ font-weight: $font-semibold;
+ }
+ }
+
+ .video-buttons {
+ min-width: 190px;
+ }
+}
+
+@media screen and (max-width: $small-view) {
+ .video {
+ flex-direction: column;
+ height: auto;
+ text-align: center;
+
+ .video-info-name {
+ margin: auto;
+ }
+
+ input[type=checkbox] {
+ display: none;
+ }
+
+ my-video-thumbnail {
+ margin-right: 0;
+ }
+
+ .video-buttons {
+ margin-top: 10px;
+ }
+ }
+}
--- /dev/null
+import { Component, OnInit, OnDestroy } from '@angular/core'
+import { Location } from '@angular/common'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { Router, ActivatedRoute } from '@angular/router'
+import { AbstractVideoList } from '@app/shared/video/abstract-video-list'
+import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
+import { Notifier, AuthService } from '@app/core'
+import { Video } from '@shared/models'
+import { VideoBlacklistService } from '@app/shared'
+import { immutableAssign } from '@app/shared/misc/utils'
+import { ScreenService } from '@app/shared/misc/screen.service'
+
+@Component({
+ selector: 'my-video-auto-blacklist-list',
+ templateUrl: './video-auto-blacklist-list.component.html',
+ styleUrls: [ './video-auto-blacklist-list.component.scss' ]
+})
+export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy {
+ titlePage: string
+ currentRoute = '/admin/moderation/video-auto-blacklist/list'
+ checkedVideos: { [ id: number ]: boolean } = {}
+ pagination: ComponentPagination = {
+ currentPage: 1,
+ itemsPerPage: 5,
+ totalItems: null
+ }
+
+ protected baseVideoWidth = -1
+ protected baseVideoHeight = 155
+
+ constructor (
+ protected router: Router,
+ protected route: ActivatedRoute,
+ protected i18n: I18n,
+ protected notifier: Notifier,
+ protected location: Location,
+ protected authService: AuthService,
+ protected screenService: ScreenService,
+ private videoBlacklistService: VideoBlacklistService,
+ ) {
+ super()
+
+ this.titlePage = this.i18n('Auto-blacklisted videos')
+ }
+
+ ngOnInit () {
+ super.ngOnInit()
+ }
+
+ ngOnDestroy () {
+ super.ngOnDestroy()
+ }
+
+ abortSelectionMode () {
+ this.checkedVideos = {}
+ }
+
+ isInSelectionMode () {
+ return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
+ }
+
+ getVideosObservable (page: number) {
+ const newPagination = immutableAssign(this.pagination, { currentPage: page })
+
+ return this.videoBlacklistService.getAutoBlacklistedAsVideoList(newPagination)
+ }
+
+ generateSyndicationList () {
+ throw new Error('Method not implemented.')
+ }
+
+ removeVideoFromBlacklist (entry: Video) {
+ this.videoBlacklistService.removeVideoFromBlacklist(entry.id).subscribe(
+ () => {
+ this.notifier.success(this.i18n('Video {{name}} removed from blacklist.', { name: entry.name }))
+ this.reloadVideos()
+ },
+
+ error => this.notifier.error(error.message)
+ )
+ }
+
+ removeSelectedVideosFromBlacklist () {
+ const toReleaseVideosIds = Object.keys(this.checkedVideos)
+ .filter(k => this.checkedVideos[ k ] === true)
+ .map(k => parseInt(k, 10))
+
+ this.videoBlacklistService.removeVideoFromBlacklist(toReleaseVideosIds).subscribe(
+ () => {
+ this.notifier.success(this.i18n('{{num}} videos removed from blacklist.', { num: toReleaseVideosIds.length }))
+
+ this.abortSelectionMode()
+ this.reloadVideos()
+ },
+
+ error => this.notifier.error(error.message)
+ )
+ }
+
+}
import { Component, OnInit } from '@angular/core'
import { SortMeta } from 'primeng/components/common/sortmeta'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
import { ConfirmService } from '../../../core'
import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
-import { VideoBlacklist } from '../../../../../../shared'
+import { VideoBlacklist, VideoBlacklistType } from '../../../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
import { Video } from '../../../shared/video/video.model'
rowsPerPage = 10
sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+ listBlacklistTypeFilter: VideoBlacklistType = undefined
videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
constructor (
private notifier: Notifier,
+ private serverService: ServerService,
private confirmService: ConfirmService,
private videoBlacklistService: VideoBlacklistService,
private markdownRenderer: MarkdownService,
) {
super()
+ // don't filter if auto-blacklist not enabled as this will be only list
+ if (this.serverService.getConfig().autoBlacklist.videos.ofUsers.enabled) {
+ this.listBlacklistTypeFilter = VideoBlacklistType.MANUAL
+ }
+
this.videoBlacklistActions = [
{
label: this.i18n('Unblacklist'),
}
protected loadData () {
- this.videoBlacklistService.listBlacklist(this.pagination, this.sort)
+ this.videoBlacklistService.listBlacklist(this.pagination, this.sort, this.listBlacklistTypeFilter)
.subscribe(
async resultList => {
this.totalRecords = resultList.total
private serverService: ServerService,
private notifier: Notifier
) {
+
this.labelNotifications = {
newVideoFromSubscription: this.i18n('New video from your subscriptions'),
newCommentOnMyVideo: this.i18n('New comment on your video'),
videoAbuseAsModerator: this.i18n('New video abuse'),
+ videoAutoBlacklistAsModerator: this.i18n('Video auto-blacklisted waiting review'),
blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'),
myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
myVideoImportFinished: this.i18n('Video import finished'),
this.rightNotifications = {
videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
+ videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
newUserRegistration: UserRight.MANAGE_USERS
}
videos: {
intervalDays: 0
}
+ },
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: false
+ }
+ }
}
}
private videoCategories: Array<VideoConstant<number>> = []
videoUrl?: string
commentUrl?: any[]
videoAbuseUrl?: string
+ videoAutoBlacklistUrl?: string
accountUrl?: string
videoImportIdentifier?: string
videoImportUrl?: string
this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
break
+ case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
+ this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
+ this.videoUrl = this.buildVideoUrl(this.video)
+ break
+
case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
break
</div>
</ng-container>
+ <ng-container i18n *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS">
+ <my-global-icon iconName="no"></my-global-icon>
+
+ <div class="message">
+ The recently added video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been <a (click)="markAsRead(notification)" [routerLink]="notification.videoAutoBlacklistUrl">auto-blacklisted</a>
+ </div>
+ </ng-container>
+
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
-import { catchError, map } from 'rxjs/operators'
+import { catchError, map, concatMap, toArray } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { SortMeta } from 'primeng/components/common/sortmeta'
-import { Observable } from 'rxjs'
-import { VideoBlacklist, ResultList } from '../../../../../shared'
+import { from as observableFrom, Observable } from 'rxjs'
+import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared'
+import { Video } from '../video/video.model'
import { environment } from '../../../environments/environment'
import { RestExtractor, RestPagination, RestService } from '../rest'
+import { ComponentPagination } from '../rest/component-pagination.model'
@Injectable()
export class VideoBlacklistService {
private restExtractor: RestExtractor
) {}
- listBlacklist (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoBlacklist>> {
+ listBlacklist (pagination: RestPagination, sort: SortMeta, type?: VideoBlacklistType): Observable<ResultList<VideoBlacklist>> {
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
+ if (type) {
+ params = params.set('type', type.toString())
+ }
+
return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
.pipe(
map(res => this.restExtractor.convertResultListDateToHuman(res)),
)
}
- removeVideoFromBlacklist (videoId: number) {
- return this.authHttp.delete(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist')
- .pipe(
- map(this.restExtractor.extractDataBool),
- catchError(res => this.restExtractor.handleError(res))
- )
+ getAutoBlacklistedAsVideoList (videoPagination: ComponentPagination): Observable<{ videos: Video[], totalVideos: number}> {
+ const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
+
+ // prioritize first created since waiting longest
+ const AUTO_BLACKLIST_SORT = 'createdAt'
+
+ let params = new HttpParams()
+ params = this.restService.addRestGetParams(params, pagination, AUTO_BLACKLIST_SORT)
+
+ params = params.set('type', VideoBlacklistType.AUTO_BEFORE_PUBLISHED.toString())
+
+ return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
+ .pipe(
+ map(res => {
+ const videos = res.data.map(videoBlacklist => new Video(videoBlacklist.video))
+ const totalVideos = res.total
+ return { videos, totalVideos }
+ }),
+ catchError(res => this.restExtractor.handleError(res))
+ )
+ }
+
+ removeVideoFromBlacklist (videoIdArgs: number | number[]) {
+ const videoIds = Array.isArray(videoIdArgs) ? videoIdArgs : [ videoIdArgs ]
+
+ return observableFrom(videoIds)
+ .pipe(
+ concatMap(id => this.authHttp.delete(VideoBlacklistService.BASE_VIDEOS_URL + id + '/blacklist')),
+ toArray(),
+ catchError(err => this.restExtractor.handleError(err))
+ )
}
blacklistVideo (videoId: number, reason: string, unfederate: boolean) {
torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
enabled: false
+auto_blacklist:
+ # New videos automatically blacklisted so moderators can review before publishing
+ videos:
+ of_users:
+ enabled: false
+
instance:
name: 'PeerTube'
short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'
torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
enabled: false
+auto_blacklist:
+ # New videos automatically blacklisted so moderators can review before publishing
+ videos:
+ of_users:
+ enabled: false
+
# Instance settings
instance:
name: 'PeerTube'
}
}
},
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
+ }
+ }
+ },
avatar: {
file: {
size: {
enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
}
}
+ },
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
+ }
+ }
}
}
}
newVideoFromSubscription: body.newVideoFromSubscription,
newCommentOnMyVideo: body.newCommentOnMyVideo,
videoAbuseAsModerator: body.videoAbuseAsModerator,
+ videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: body.blacklistOnMyVideo,
myVideoPublished: body.myVideoPublished,
myVideoImportFinished: body.myVideoImportFinished,
import * as express from 'express'
-import { UserRight, VideoBlacklist, VideoBlacklistCreate } from '../../../../shared'
+import { VideoBlacklist, UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
import {
setDefaultPagination,
videosBlacklistAddValidator,
videosBlacklistRemoveValidator,
- videosBlacklistUpdateValidator
+ videosBlacklistUpdateValidator,
+ videosBlacklistFiltersValidator
} from '../../../middlewares'
import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
import { sequelizeTypescript } from '../../../initializers'
blacklistSortValidator,
setBlacklistSort,
setDefaultPagination,
+ videosBlacklistFiltersValidator,
asyncMiddleware(listBlacklist)
)
const toCreate = {
videoId: videoInstance.id,
unfederated: body.unfederate === true,
- reason: body.reason
+ reason: body.reason,
+ type: VideoBlacklistType.MANUAL
}
const blacklist = await VideoBlacklistModel.create(toCreate)
}
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)
+ const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.type)
return res.json(getFormattedObjects<VideoBlacklist, VideoBlacklistModel>(resultList.data, resultList.total))
}
const videoBlacklist = res.locals.videoBlacklist
const video = res.locals.video
- await sequelizeTypescript.transaction(async t => {
+ const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
const unfederated = videoBlacklist.unfederated
+ const videoBlacklistType = videoBlacklist.type
+
await videoBlacklist.destroy({ transaction: t })
// Re federate the video
if (unfederated === true) {
await federateVideoIfNeeded(video, true, t)
}
+
+ return videoBlacklistType
})
Notifier.Instance.notifyOnVideoUnblacklist(video)
+ if (videoBlacklistType === VideoBlacklistType.AUTO_BEFORE_PUBLISHED) {
+ Notifier.Instance.notifyOnVideoPublishedAfterRemovedFromAutoBlacklist(video)
+
+ // Delete on object so new video notifications will send
+ delete video.VideoBlacklist
+ Notifier.Instance.notifyOnNewVideo(video)
+ }
+
logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
return res.type('json').status(204).end()
import { isArray } from '../../../helpers/custom-validators/misc'
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
import { VideoChannelModel } from '../../../models/video/video-channel'
+import { UserModel } from '../../../models/account/user'
import * as Bluebird from 'bluebird'
import * as parseTorrent from 'parse-torrent'
import { getSecureTorrentName } from '../../../helpers/utils'
import { readFile, move } from 'fs-extra'
+import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
const auditLogger = auditLoggerFactory('video-imports')
const videoImportsRouter = express.Router()
videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string
}
- const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
+ const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }, user)
await processThumbnail(req, video)
await processPreview(req, video)
}).end()
}
- const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
+ const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo, user)
const downloadThumbnail = !await processThumbnail(req, video)
const downloadPreview = !await processPreview(req, video)
return res.json(videoImport.toFormattedJSON()).end()
}
-function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo) {
+function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo, user: UserModel) {
const videoData = {
name: body.name || importData.name || 'Unknown name',
remote: false,
const videoCreated = await video.save(sequelizeOptions)
videoCreated.VideoChannel = videoChannel
+ await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
+
// Set tags to the video
if (tags) {
const tagInstances = await TagModel.findOrCreateTags(tags, t)
import { logger } from '../../../helpers/logger'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
+import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
import {
CONFIG,
MIMETYPES,
channelId: res.locals.videoChannel.id,
originallyPublishedAt: videoInfo.originallyPublishedAt
}
+
const video = new VideoModel(videoData)
video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
// Create the torrent file
await video.createTorrentAndSetInfoHash(videoFile)
- const videoCreated = await sequelizeTypescript.transaction(async t => {
+ const { videoCreated, videoWasAutoBlacklisted } = await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t }
const videoCreated = await video.save(sequelizeOptions)
}, { transaction: t })
}
- await federateVideoIfNeeded(video, true, t)
+ const videoWasAutoBlacklisted = await autoBlacklistVideoIfNeeded(video, res.locals.oauth.token.User, t)
+
+ if (!videoWasAutoBlacklisted) {
+ await federateVideoIfNeeded(video, true, t)
+ }
auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
- return videoCreated
+ return { videoCreated, videoWasAutoBlacklisted }
})
- Notifier.Instance.notifyOnNewVideo(videoCreated)
+ if (videoWasAutoBlacklisted) {
+ Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
+ } else {
+ Notifier.Instance.notifyOnNewVideo(videoCreated)
+ }
if (video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now
import { Response } from 'express'
import * as validator from 'validator'
+import { exists } from './misc'
import { CONSTRAINTS_FIELDS } from '../../initializers'
import { VideoBlacklistModel } from '../../models/video/video-blacklist'
+import { VideoBlacklistType } from '../../../shared/models/videos'
const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
return true
}
+function isVideoBlacklistTypeValid (value: any) {
+ return exists(value) && validator.isInt('' + value) && VideoBlacklistType[value] !== undefined
+}
+
// ---------------------------------------------------------------------------
export {
isVideoBlacklistReasonValid,
+ isVideoBlacklistTypeValid,
doesVideoBlacklistExist
}
+import { CONFIG } from '../initializers'
import { VideoModel } from '../models/video/video'
+import { UserRight } from '../../shared'
+import { UserModel } from '../models/account/user'
type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions',
- 'import.videos.http.enabled', 'import.videos.torrent.enabled',
+ 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'auto_blacklist.videos.of_users.enabled',
'trending.videos.interval_days',
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 345
+const LAST_MIGRATION_VERSION = 350
// ---------------------------------------------------------------------------
}
}
},
+ AUTO_BLACKLIST: {
+ VIDEOS: {
+ OF_USERS: {
+ get ENABLED () { return config.get<boolean>('auto_blacklist.videos.of_users.enabled') }
+ }
+ }
+ },
CACHE: {
PREVIEWS: {
get SIZE () { return config.get<number>('cache.previews.size') }
--- /dev/null
+import * as Sequelize from 'sequelize'
+import { VideoBlacklistType } from '../../../shared/models/videos'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize,
+ db: any
+}): Promise<void> {
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ defaultValue: null
+ }
+
+ await utils.queryInterface.addColumn('videoBlacklist', 'type', data)
+ }
+
+ {
+ const query = 'UPDATE "videoBlacklist" SET "type" = ' + VideoBlacklistType.MANUAL
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ defaultValue: null
+ }
+ await utils.queryInterface.changeColumn('videoBlacklist', 'type', data)
+ }
+
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ defaultValue: null,
+ allowNull: true
+ }
+ await utils.queryInterface.addColumn('userNotificationSetting', 'videoAutoBlacklistAsModerator', data)
+ }
+
+ {
+ const query = 'UPDATE "userNotificationSetting" SET "videoAutoBlacklistAsModerator" = 3'
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ defaultValue: null,
+ allowNull: false
+ }
+ await utils.queryInterface.changeColumn('userNotificationSetting', 'videoAutoBlacklistAsModerator', data)
+ }
+}
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
import { VideoCommentModel } from '../../models/video/video-comment'
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
- // If the video is not private and published, we federate it
+ // If the video is not private and is published, we federate it
if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
// Fetch more attributes that we will need to serialize in AP object
if (isArray(video.VideoCaptions) === false) {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
+ addVideoAutoBlacklistModeratorsNotification (to: string[], video: VideoModel) {
+ const VIDEO_AUTO_BLACKLIST_URL = CONFIG.WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
+ const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
+
+ const text = `Hi,\n\n` +
+ `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
+ `\n\n` +
+ `You can view it and take appropriate action on ${videoUrl}` +
+ `\n\n` +
+ `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
+ `\n\n` +
+ `Cheers,\n` +
+ `PeerTube.`
+
+ const emailPayload: EmailPayload = {
+ to,
+ subject: '[PeerTube] An auto-blacklisted video is awaiting review',
+ text
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
addNewUserRegistrationNotification (to: string[], user: UserModel) {
const text = `Hi,\n\n` +
`User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` +
return videoImportUpdated
})
- Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
+ if (videoImportUpdated.Video.VideoBlacklist) {
+ Notifier.Instance.notifyOnVideoAutoBlacklist(videoImportUpdated.Video)
+ } else {
+ Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
+ }
+
// Create transcoding jobs?
if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now
return { videoDatabase, videoPublished }
})
- // don't notify prior to scheduled video update
- if (videoPublished && !videoDatabase.ScheduleVideoUpdate) {
+ if (videoPublished) {
Notifier.Instance.notifyOnNewVideo(videoDatabase)
- Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase)
+ Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
}
await createHlsJobIfEnabled(payload)
return { videoDatabase, videoPublished }
})
- // don't notify prior to scheduled video update
- if (!videoDatabase.ScheduleVideoUpdate) {
- if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
- if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase)
- }
+ if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
+ if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }))
}
private constructor () {}
notifyOnNewVideo (video: VideoModel): void {
- // Only notify on public and published videos
- if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED) return
+ // Only notify on public and published videos which are not blacklisted
+ if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.VideoBlacklist) return
this.notifySubscribersOfNewVideo(video)
.catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
}
- notifyOnPendingVideoPublished (video: VideoModel): void {
- // Only notify on public videos that has been published while the user waited transcoding/scheduled update
- if (video.waitTranscoding === false && !video.ScheduleVideoUpdate) return
+ notifyOnVideoPublishedAfterTranscoding (video: VideoModel): void {
+ // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
+ if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
this.notifyOwnedVideoHasBeenPublished(video)
- .catch(err => logger.error('Cannot notify owner that its video %s has been published.', video.url, { err }))
+ .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
+ }
+
+ notifyOnVideoPublishedAfterScheduledUpdate (video: VideoModel): void {
+ // don't notify if video is still blacklisted or waiting for transcoding
+ if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
+
+ this.notifyOwnedVideoHasBeenPublished(video)
+ .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
+ }
+
+ notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: VideoModel): void {
+ // don't notify if video is still waiting for transcoding or scheduled update
+ if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
+
+ this.notifyOwnedVideoHasBeenPublished(video)
+ .catch(err => logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })) // tslint:disable-line:max-line-length
}
notifyOnNewComment (comment: VideoCommentModel): void {
.catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
}
+ notifyOnVideoAutoBlacklist (video: VideoModel): void {
+ this.notifyModeratorsOfVideoAutoBlacklist(video)
+ .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err }))
+ }
+
notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void {
this.notifyVideoOwnerOfBlacklist(videoBlacklist)
.catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
notifyOnVideoUnblacklist (video: VideoModel): void {
this.notifyVideoOwnerOfUnblacklist(video)
- .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err }))
+ .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
}
notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void {
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
}
+ private async notifyModeratorsOfVideoAutoBlacklist (video: VideoModel) {
+ const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
+ if (moderators.length === 0) return
+
+ logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url)
+
+ function settingGetter (user: UserModel) {
+ return user.NotificationSetting.videoAutoBlacklistAsModerator
+ }
+ async function notificationCreator (user: UserModel) {
+
+ const notification = await UserNotificationModel.create({
+ type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
+ userId: user.id,
+ videoId: video.id
+ })
+ notification.Video = video
+
+ return notification
+ }
+
+ function emailSender (emails: string[]) {
+ return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video)
+ }
+
+ return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
+ }
+
private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) {
const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
if (!user) return
for (const v of publishedVideos) {
Notifier.Instance.notifyOnNewVideo(v)
- Notifier.Instance.notifyOnPendingVideoPublished(v)
+ Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(v)
}
}
myVideoImportFinished: UserNotificationSettingValue.WEB,
myVideoPublished: UserNotificationSettingValue.WEB,
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newUserRegistration: UserNotificationSettingValue.WEB,
commentMention: UserNotificationSettingValue.WEB,
--- /dev/null
+import * as sequelize from 'sequelize'
+import { CONFIG } from '../initializers/constants'
+import { VideoBlacklistType, UserRight } from '../../shared/models'
+import { VideoBlacklistModel } from '../models/video/video-blacklist'
+import { UserModel } from '../models/account/user'
+import { VideoModel } from '../models/video/video'
+import { logger } from '../helpers/logger'
+
+async function autoBlacklistVideoIfNeeded (video: VideoModel, user: UserModel, transaction: sequelize.Transaction) {
+ if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED) return false
+
+ if (user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return false
+
+ const sequelizeOptions = { transaction }
+ const videoBlacklistToCreate = {
+ videoId: video.id,
+ unfederated: true,
+ reason: 'Auto-blacklisted. Moderator review required.',
+ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
+ }
+ await VideoBlacklistModel.create(videoBlacklistToCreate, sequelizeOptions)
+ logger.info('Video %s auto-blacklisted.', video.uuid)
+
+ return true
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ autoBlacklistVideoIfNeeded
+}
import * as express from 'express'
-import { body, param } from 'express-validator/check'
+import { body, param, query } from 'express-validator/check'
import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
import { doesVideoExist } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger'
import { areValidationErrors } from '../utils'
-import { doesVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist'
+import {
+ doesVideoBlacklistExist,
+ isVideoBlacklistReasonValid,
+ isVideoBlacklistTypeValid
+} from '../../../helpers/custom-validators/video-blacklist'
const videosBlacklistRemoveValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
}
]
+const videosBlacklistFiltersValidator = [
+ query('type')
+ .optional()
+ .custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking videos blacklist filters query', { parameters: req.query })
+
+ if (areValidationErrors(req, res)) return
+
+ return next()
+ }
+]
+
// ---------------------------------------------------------------------------
export {
videosBlacklistAddValidator,
videosBlacklistRemoveValidator,
- videosBlacklistUpdateValidator
+ videosBlacklistUpdateValidator,
+ videosBlacklistFiltersValidator
}
@Column
videoAbuseAsModerator: UserNotificationSettingValue
+ @AllowNull(false)
+ @Default(null)
+ @Is(
+ 'UserNotificationSettingVideoAutoBlacklistAsModerator',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator')
+ )
+ @Column
+ videoAutoBlacklistAsModerator: UserNotificationSettingValue
+
@AllowNull(false)
@Default(null)
@Is(
newCommentOnMyVideo: this.newCommentOnMyVideo,
newVideoFromSubscription: this.newVideoFromSubscription,
videoAbuseAsModerator: this.videoAbuseAsModerator,
+ videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: this.blacklistOnMyVideo,
myVideoPublished: this.myVideoPublished,
myVideoImportFinished: this.myVideoImportFinished,
model: VideoModel.scope(
[
VideoScopeNames.WITH_FILES,
- VideoScopeNames.WITH_ACCOUNT_DETAILS
+ VideoScopeNames.WITH_ACCOUNT_DETAILS,
+ VideoScopeNames.WITH_BLACKLISTED
]
)
}
-import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import {
+ AllowNull,
+ BelongsTo,
+ Column,
+ CreatedAt,
+ DataType,
+ Default,
+ ForeignKey,
+ Is, Model,
+ Table,
+ UpdatedAt,
+ IFindOptions
+} from 'sequelize-typescript'
import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
-import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
-import { VideoBlacklist } from '../../../shared/models/videos'
+import { VideoChannelModel, ScopeNames as VideoChannelScopeNames } from './video-channel'
+import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
+import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
import { CONSTRAINTS_FIELDS } from '../../initializers'
@Table({
@Column
unfederated: boolean
+ @AllowNull(false)
+ @Default(null)
+ @Is('VideoBlacklistType', value => throwIfNotValid(value, isVideoBlacklistTypeValid, 'type'))
+ @Column
+ type: VideoBlacklistType
+
@CreatedAt
createdAt: Date
})
Video: VideoModel
- static listForApi (start: number, count: number, sort: SortType) {
- const query = {
+ static listForApi (start: number, count: number, sort: SortType, type?: VideoBlacklistType) {
+ const query: IFindOptions<VideoBlacklistModel> = {
offset: start,
limit: count,
order: getSortOnModel(sort.sortModel, sort.sortValue),
include: [
{
model: VideoModel,
- required: true
+ required: true,
+ include: [
+ {
+ model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
+ required: true
+ }
+ ]
}
]
}
+ if (type) {
+ query.where = { type }
+ }
+
return VideoBlacklistModel.findAndCountAll(query)
.then(({ rows, count }) => {
return {
}
toFormattedJSON (): VideoBlacklist {
- const video = this.Video
-
return {
id: this.id,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
reason: this.reason,
unfederated: this.unfederated,
+ type: this.type,
- 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
- }
+ video: this.Video.toFormattedJSON()
}
}
}
enabled: false
}
}
+ },
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: false
+ }
+ }
}
}
newVideoFromSubscription: UserNotificationSettingValue.WEB,
newCommentOnMyVideo: UserNotificationSettingValue.WEB,
videoAbuseAsModerator: UserNotificationSettingValue.WEB,
+ videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
blacklistOnMyVideo: UserNotificationSettingValue.WEB,
myVideoImportFinished: UserNotificationSettingValue.WEB,
myVideoPublished: UserNotificationSettingValue.WEB,
flushAndRunMultipleServers,
flushTests,
getBlacklistedVideosList,
+ getBlacklistedVideosListWithTypeFilter,
getVideo,
getVideoWithToken,
killallServers,
checkBadSortPagination,
checkBadStartPagination
} from '../../../../shared/utils/requests/check-api-params'
-import { VideoDetails } from '../../../../shared/models/videos'
+import { VideoDetails, VideoBlacklistType } from '../../../../shared/models/videos'
import { expect } from 'chai'
describe('Test video blacklist API validators', function () {
it('Should fail with an incorrect sort', async function () {
await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken)
})
+
+ it('Should fail with an invalid type', async function () {
+ await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, 0, 400)
+ })
+
+ it('Should succeed with the correct parameters', async function () {
+ await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.MANUAL)
+ })
})
after(async function () {
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
import {
createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest,
- makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin
+ makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, uploadVideo,
+ runServer, ServerInfo, setAccessTokensToServers, userLogin, updateCustomSubConfig
} from '../../../../shared/utils'
import {
checkBadCountPagination,
expect(data.import.videos.http.enabled).to.be.true
expect(data.import.videos.torrent.enabled).to.be.true
+ expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
}
function checkUpdatedConfig (data: CustomConfig) {
expect(data.import.videos.http.enabled).to.be.false
expect(data.import.videos.torrent.enabled).to.be.false
+ expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
}
describe('Test config', function () {
enabled: false
}
}
+ },
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: true
+ }
+ }
}
}
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
updateVideo,
updateVideoChannel,
userLogin,
- wait
+ wait,
+ getCustomConfig,
+ updateCustomConfig
} from '../../../../shared/utils'
import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index'
import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
checkNewBlacklistOnMyVideo,
checkNewCommentOnMyVideo,
checkNewVideoAbuseForModerators,
+ checkVideoAutoBlacklistForModerators,
checkNewVideoFromSubscription,
checkUserRegistered,
checkVideoIsPublished,
import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
import * as uuidv4 from 'uuid/v4'
import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist'
+import { CustomConfig } from '../../../../shared/models/server'
const expect = chai.expect
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
})
it('Should send a new video notification after a video import', async function () {
- this.timeout(30000)
+ this.timeout(100000)
const name = 'video import ' + uuidv4()
})
})
+ describe('Video-related notifications when video auto-blacklist is enabled', function () {
+ let userBaseParams: CheckerBaseParams
+ let adminBaseParamsServer1: CheckerBaseParams
+ let adminBaseParamsServer2: CheckerBaseParams
+ let videoUUID: string
+ let videoName: string
+ let currentCustomConfig: CustomConfig
+
+ before(async () => {
+
+ adminBaseParamsServer1 = {
+ server: servers[0],
+ emails,
+ socketNotifications: adminNotifications,
+ token: servers[0].accessToken
+ }
+
+ adminBaseParamsServer2 = {
+ server: servers[1],
+ emails,
+ socketNotifications: adminNotificationsServer2,
+ token: servers[1].accessToken
+ }
+
+ userBaseParams = {
+ server: servers[0],
+ emails,
+ socketNotifications: userNotifications,
+ token: userAccessToken
+ }
+
+ const resCustomConfig = await getCustomConfig(servers[0].url, servers[0].accessToken)
+ currentCustomConfig = resCustomConfig.body
+ const autoBlacklistTestsCustomConfig = immutableAssign(currentCustomConfig, {
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: true
+ }
+ }
+ }
+ })
+ // enable transcoding otherwise own publish notification after transcoding not expected
+ autoBlacklistTestsCustomConfig.transcoding.enabled = true
+ await updateCustomConfig(servers[0].url, servers[0].accessToken, autoBlacklistTestsCustomConfig)
+
+ await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
+ await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
+
+ })
+
+ it('Should send notification to moderators on new video with auto-blacklist', async function () {
+ this.timeout(20000)
+
+ videoName = 'video with auto-blacklist ' + uuidv4()
+ const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName })
+ videoUUID = resVideo.body.video.uuid
+
+ await waitJobs(servers)
+ await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, videoUUID, videoName, 'presence')
+ })
+
+ it('Should not send video publish notification if auto-blacklisted', async function () {
+ await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'absence')
+ })
+
+ it('Should not send a local user subscription notification if auto-blacklisted', async function () {
+ await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'absence')
+ })
+
+ it('Should not send a remote user subscription notification if auto-blacklisted', async function () {
+ await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'absence')
+ })
+
+ it('Should send video published and unblacklist after video unblacklisted', async function () {
+ this.timeout(20000)
+
+ await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoUUID)
+
+ await waitJobs(servers)
+
+ // FIXME: Can't test as two notifications sent to same user and util only checks last one
+ // One notification might be better anyways
+ // await checkNewBlacklistOnMyVideo(userBaseParams, videoUUID, videoName, 'unblacklist')
+ // await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'presence')
+ })
+
+ it('Should send a local user subscription notification after removed from blacklist', async function () {
+ await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'presence')
+ })
+
+ it('Should send a remote user subscription notification after removed from blacklist', async function () {
+ await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'presence')
+ })
+
+ it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () {
+ this.timeout(20000)
+
+ let updateAt = new Date(new Date().getTime() + 100000)
+
+ const name = 'video with auto-blacklist and future schedule ' + uuidv4()
+
+ const data = {
+ name,
+ privacy: VideoPrivacy.PRIVATE,
+ scheduleUpdate: {
+ updateAt: updateAt.toISOString(),
+ privacy: VideoPrivacy.PUBLIC
+ }
+ }
+
+ const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
+ const uuid = resVideo.body.video.uuid
+
+ await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid)
+
+ await waitJobs(servers)
+ await checkNewBlacklistOnMyVideo(userBaseParams, uuid, name, 'unblacklist')
+
+ // FIXME: Can't test absence as two notifications sent to same user and util only checks last one
+ // One notification might be better anyways
+ // await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
+
+ await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
+ await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
+ })
+
+ it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () {
+ this.timeout(20000)
+
+ // In 2 seconds
+ let updateAt = new Date(new Date().getTime() + 2000)
+
+ const name = 'video with schedule done and still auto-blacklisted ' + uuidv4()
+
+ const data = {
+ name,
+ privacy: VideoPrivacy.PRIVATE,
+ scheduleUpdate: {
+ updateAt: updateAt.toISOString(),
+ privacy: VideoPrivacy.PUBLIC
+ }
+ }
+
+ const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
+ const uuid = resVideo.body.video.uuid
+
+ await wait(6000)
+ await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
+ await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
+ await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
+ })
+
+ it('Should not send a notification to moderators on new video without auto-blacklist', async function () {
+ this.timeout(20000)
+
+ const name = 'video without auto-blacklist ' + uuidv4()
+
+ // admin with blacklist right will not be auto-blacklisted
+ const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name })
+ const uuid = resVideo.body.video.uuid
+
+ await waitJobs(servers)
+ await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, uuid, name, 'absence')
+ })
+
+ after(async () => {
+ await updateCustomConfig(servers[0].url, servers[0].accessToken, currentCustomConfig)
+
+ await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
+ await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
+ })
+ })
+
describe('Mark as read', function () {
it('Should mark as read some notifications', async function () {
const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3)
})
it('Should not have notifications', async function () {
- this.timeout(10000)
+ this.timeout(20000)
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
newVideoFromSubscription: UserNotificationSettingValue.NONE
})
it('Should only have web notifications', async function () {
- this.timeout(10000)
+ this.timeout(20000)
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
newVideoFromSubscription: UserNotificationSettingValue.WEB
})
it('Should only have mail notifications', async function () {
- this.timeout(10000)
+ this.timeout(20000)
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
newVideoFromSubscription: UserNotificationSettingValue.EMAIL
})
it('Should have email and web notifications', async function () {
- this.timeout(10000)
+ this.timeout(20000)
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
addVideoToBlacklist,
flushAndRunMultipleServers,
getBlacklistedVideosList,
+ getBlacklistedVideosListWithTypeFilter,
getMyVideos,
getSortedBlacklistedVideosList,
getVideosList,
} from '../../../../shared/utils/index'
import { doubleFollow } from '../../../../shared/utils/server/follows'
import { waitJobs } from '../../../../shared/utils/server/jobs'
-import { VideoBlacklist } from '../../../../shared/models/videos'
+import { VideoBlacklist, VideoBlacklistType } from '../../../../shared/models/videos'
const expect = chai.expect
})
})
- describe('When listing blacklisted videos', function () {
+ describe('When listing manually blacklisted videos', function () {
it('Should display all the blacklisted videos', async function () {
const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken)
}
})
+ it('Should display all the blacklisted videos when applying manual type filter', async function () {
+ const res = await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.MANUAL)
+
+ expect(res.body.total).to.equal(2)
+
+ const blacklistedVideos = res.body.data
+ expect(blacklistedVideos).to.be.an('array')
+ expect(blacklistedVideos.length).to.equal(2)
+ })
+
+ it('Should display nothing when applying automatic type filter', async function () {
+ const res = await getBlacklistedVideosListWithTypeFilter(servers[0].url, servers[0].accessToken, VideoBlacklistType.AUTO_BEFORE_PUBLISHED) // tslint:disable:max-line-length
+
+ expect(res.body.total).to.equal(0)
+
+ const blacklistedVideos = res.body.data
+ expect(blacklistedVideos).to.be.an('array')
+ expect(blacklistedVideos.length).to.equal(0)
+ })
+
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)
}
}
}
+
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: boolean
+ }
+ }
+ }
+
}
}
}
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: boolean
+ }
+ }
+ }
+
avatar: {
file: {
size: {
newVideoFromSubscription: UserNotificationSettingValue
newCommentOnMyVideo: UserNotificationSettingValue
videoAbuseAsModerator: UserNotificationSettingValue
+ videoAutoBlacklistAsModerator: UserNotificationSettingValue
blacklistOnMyVideo: UserNotificationSettingValue
myVideoPublished: UserNotificationSettingValue
myVideoImportFinished: UserNotificationSettingValue
NEW_USER_REGISTRATION = 9,
NEW_FOLLOW = 10,
- COMMENT_MENTION = 11
+ COMMENT_MENTION = 11,
+
+ VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12
}
export interface VideoInfo {
+import { Video } from '../video.model'
+
+export enum VideoBlacklistType {
+ MANUAL = 1,
+ AUTO_BEFORE_PUBLISHED = 2
+}
+
export interface VideoBlacklist {
id: number
createdAt: Date
updatedAt: Date
unfederated: boolean
reason?: string
+ type: VideoBlacklistType
- video: {
- id: number
- name: string
- uuid: string
- description: string
- duration: number
- views: number
- likes: number
- dislikes: number
- nsfw: boolean
- }
+ video: Video
}
enabled: false
}
}
+ },
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: false
+ }
+ }
}
}
})
}
-function getUserNotifications (
+async function getUserNotifications (
url: string,
token: string,
start: number,
checkVideo(notification.video, videoName, videoUUID)
checkActor(notification.video.channel)
} else {
- expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
+ expect(notification).to.satisfy((n: UserNotification) => {
+ return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
+ })
}
}
function emailFinder (email: object) {
- return email[ 'text' ].indexOf(videoUUID) !== -1
+ const text = email[ 'text' ]
+ return text.indexOf(videoUUID) !== -1 && text.indexOf('Your subscription') !== -1
}
await checkNotification(base, notificationChecker, emailFinder, type)
await checkNotification(base, notificationChecker, emailFinder, type)
}
+async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
+ const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
+
+ function notificationChecker (notification: UserNotification, type: CheckerType) {
+ if (type === 'presence') {
+ expect(notification).to.not.be.undefined
+ expect(notification.type).to.equal(notificationType)
+
+ expect(notification.video.id).to.be.a('number')
+ checkVideo(notification.video, videoName, videoUUID)
+ } else {
+ expect(notification).to.satisfy((n: UserNotification) => {
+ return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
+ })
+ }
+ }
+
+ function emailFinder (email: object) {
+ const text = email[ 'text' ]
+ return text.indexOf(videoUUID) !== -1 && email[ 'text' ].indexOf('video-auto-blacklist/list') !== -1
+ }
+
+ await checkNotification(base, notificationChecker, emailFinder, type)
+}
+
async function checkNewBlacklistOnMyVideo (
base: CheckerBaseParams,
videoUUID: string,
checkCommentMention,
updateMyNotificationSettings,
checkNewVideoAbuseForModerators,
+ checkVideoAutoBlacklistForModerators,
getUserNotifications,
markAsReadNotifications,
getLastNotification
.expect('Content-Type', /json/)
}
+function getBlacklistedVideosListWithTypeFilter (url: string, token: string, type: number, specialStatus = 200) {
+ const path = '/api/v1/videos/blacklist/'
+
+ return request(url)
+ .get(path)
+ .query({ sort: 'createdAt', type })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + token)
+ .expect(specialStatus)
+ .expect('Content-Type', /json/)
+}
+
function getSortedBlacklistedVideosList (url: string, token: string, sort: string, specialStatus = 200) {
const path = '/api/v1/videos/blacklist/'
addVideoToBlacklist,
removeVideoFromBlacklist,
getBlacklistedVideosList,
+ getBlacklistedVideosListWithTypeFilter,
getSortedBlacklistedVideosList,
updateVideoBlacklist
}
import * as request from 'supertest'
-function changeVideoOwnership (url: string, token: string, videoId: number | string, username) {
+function changeVideoOwnership (url: string, token: string, videoId: number | string, username, expectedStatus = 204) {
const path = '/api/v1/videos/' + videoId + '/give-ownership'
return request(url)
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token)
.send({ username })
- .expect(204)
+ .expect(expectedStatus)
}
function getVideoChangeOwnershipList (url: string, token: string) {