From 7ccddd7b5250bd25a917a6e77e58b87b9484a2a4 Mon Sep 17 00:00:00 2001 From: Josh Morel Date: Tue, 2 Apr 2019 05:26:47 -0400 Subject: add quarantine videos feature (#1637) * add quarantine videos feature * increase Notification settings test timeout to 20000ms. was completing 7000 locally but timing out after 10000 on travis * fix quarantine video test issues -propagate misspelling -remove skip from server/tests/client.ts * WIP use blacklist for moderator video approval instead of video.quarantine boolean * finish auto-blacklist feature --- server/lib/activitypub/videos.ts | 2 +- server/lib/emailer.ts | 23 ++++++++ server/lib/job-queue/handlers/video-import.ts | 7 ++- server/lib/job-queue/handlers/video-transcoding.ts | 12 ++--- server/lib/notifier.ts | 63 +++++++++++++++++++--- server/lib/schedulers/update-videos-scheduler.ts | 2 +- server/lib/user.ts | 1 + server/lib/video-blacklist.ts | 31 +++++++++++ 8 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 server/lib/video-blacklist.ts (limited to 'server/lib') diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 2c932371b..d935e3f90 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts @@ -45,7 +45,7 @@ import { VideoShareModel } from '../../models/video/video-share' 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) { diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 04e4b94b6..eec97c27e 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -250,6 +250,29 @@ class Emailer { 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` + diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index d96bfdf43..c5fc1061c 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts @@ -196,9 +196,14 @@ async function processFile (downloader: () => Promise, videoImport: Vide 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 diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index d9dad795e..581ec283e 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts @@ -85,10 +85,9 @@ async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodi 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) @@ -146,11 +145,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video 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 })) } diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 501680f6b..9fe93ec0d 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts @@ -23,19 +23,35 @@ class Notifier { 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 { @@ -51,6 +67,11 @@ class Notifier { .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 })) @@ -58,7 +79,7 @@ class Notifier { 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 { @@ -268,6 +289,34 @@ class Notifier { 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 diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index 2618a5857..2179a2f26 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts @@ -57,7 +57,7 @@ export class UpdateVideosScheduler extends AbstractScheduler { for (const v of publishedVideos) { Notifier.Instance.notifyOnNewVideo(v) - Notifier.Instance.notifyOnPendingVideoPublished(v) + Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(v) } } diff --git a/server/lib/user.ts b/server/lib/user.ts index 02a84f15b..5588b0f76 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -106,6 +106,7 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr 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, diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts new file mode 100644 index 000000000..dc4e0aed9 --- /dev/null +++ b/server/lib/video-blacklist.ts @@ -0,0 +1,31 @@ +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 +} -- cgit v1.2.3