From d26836cd95e981d636006652927773c7943e77ce Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 30 Jul 2021 16:51:27 +0200 Subject: [PATCH] Refactor notifier --- server/lib/activitypub/url.ts | 12 +- server/lib/emailer.ts | 440 +--------- server/lib/job-queue/handlers/video-import.ts | 4 +- server/lib/notifier.ts | 796 ------------------ server/lib/notifier/index.ts | 1 + server/lib/notifier/notifier.ts | 259 ++++++ .../abuse/abstract-new-abuse-message.ts | 67 ++ .../abuse/abuse-state-change-for-reporter.ts | 74 ++ server/lib/notifier/shared/abuse/index.ts | 4 + .../shared/abuse/new-abuse-for-moderators.ts | 119 +++ .../abuse/new-abuse-message-for-moderators.ts | 32 + .../abuse/new-abuse-message-for-reporter.ts | 36 + server/lib/notifier/shared/blacklist/index.ts | 3 + .../new-auto-blacklist-for-moderators.ts | 60 ++ .../blacklist/new-blacklist-for-owner.ts | 58 ++ .../shared/blacklist/unblacklist-for-owner.ts | 55 ++ .../shared/comment/comment-mention.ts | 111 +++ server/lib/notifier/shared/comment/index.ts | 2 + .../comment/new-comment-for-video-owner.ts | 76 ++ .../shared/common/abstract-notification.ts | 23 + server/lib/notifier/shared/common/index.ts | 1 + .../shared/follow/auto-follow-for-instance.ts | 51 ++ .../shared/follow/follow-for-instance.ts | 68 ++ .../notifier/shared/follow/follow-for-user.ts | 82 ++ server/lib/notifier/shared/follow/index.ts | 3 + server/lib/notifier/shared/index.ts | 7 + server/lib/notifier/shared/instance/index.ts | 3 + .../new-peertube-version-for-admins.ts | 54 ++ .../instance/new-plugin-version-for-admins.ts | 58 ++ .../instance/registration-for-moderators.ts | 49 ++ .../abstract-owned-video-publication.ts | 57 ++ .../import-finished-for-owner.ts | 97 +++ .../shared/video-publication/index.ts | 5 + .../new-video-for-subscribers.ts | 61 ++ ...wned-publication-after-auto-unblacklist.ts | 11 + ...owned-publication-after-schedule-update.ts | 10 + .../owned-publication-after-transcoding.ts | 9 + 37 files changed, 1627 insertions(+), 1231 deletions(-) delete mode 100644 server/lib/notifier.ts create mode 100644 server/lib/notifier/index.ts create mode 100644 server/lib/notifier/notifier.ts create mode 100644 server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts create mode 100644 server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts create mode 100644 server/lib/notifier/shared/abuse/index.ts create mode 100644 server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts create mode 100644 server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts create mode 100644 server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts create mode 100644 server/lib/notifier/shared/blacklist/index.ts create mode 100644 server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts create mode 100644 server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts create mode 100644 server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts create mode 100644 server/lib/notifier/shared/comment/comment-mention.ts create mode 100644 server/lib/notifier/shared/comment/index.ts create mode 100644 server/lib/notifier/shared/comment/new-comment-for-video-owner.ts create mode 100644 server/lib/notifier/shared/common/abstract-notification.ts create mode 100644 server/lib/notifier/shared/common/index.ts create mode 100644 server/lib/notifier/shared/follow/auto-follow-for-instance.ts create mode 100644 server/lib/notifier/shared/follow/follow-for-instance.ts create mode 100644 server/lib/notifier/shared/follow/follow-for-user.ts create mode 100644 server/lib/notifier/shared/follow/index.ts create mode 100644 server/lib/notifier/shared/index.ts create mode 100644 server/lib/notifier/shared/instance/index.ts create mode 100644 server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts create mode 100644 server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts create mode 100644 server/lib/notifier/shared/instance/registration-for-moderators.ts create mode 100644 server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts create mode 100644 server/lib/notifier/shared/video-publication/import-finished-for-owner.ts create mode 100644 server/lib/notifier/shared/video-publication/index.ts create mode 100644 server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts create mode 100644 server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts create mode 100644 server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts create mode 100644 server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 7816b0be0..338398f2b 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts @@ -1,5 +1,6 @@ import { WEBSERVER } from '../../initializers/constants' import { + MAbuseFull, MAbuseId, MActor, MActorFollowActors, @@ -112,6 +113,14 @@ function getUndoActivityPubUrl (originalUrl: string) { return originalUrl + '/undo' } +// --------------------------------------------------------------------------- + +function getAbuseTargetUrl (abuse: MAbuseFull) { + return abuse.VideoAbuse?.Video?.url || + abuse.VideoCommentAbuse?.VideoComment?.url || + abuse.FlaggedAccount.Actor.url +} + export { getLocalVideoActivityPubUrl, getLocalVideoPlaylistActivityPubUrl, @@ -135,5 +144,6 @@ export { getLocalVideoSharesActivityPubUrl, getLocalVideoCommentsActivityPubUrl, getLocalVideoLikesActivityPubUrl, - getLocalVideoDislikesActivityPubUrl + getLocalVideoDislikesActivityPubUrl, + getAbuseTargetUrl } diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 458214f88..6bb61484b 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -1,20 +1,15 @@ import { readFileSync } from 'fs-extra' -import { merge } from 'lodash' +import { isArray, merge } from 'lodash' import { createTransport, Transporter } from 'nodemailer' import { join } from 'path' -import { VideoChannelModel } from '@server/models/video/video-channel' -import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' -import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' -import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' +import { EmailPayload } from '@shared/models' import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' import { isTestInstance, root } from '../helpers/core-utils' import { bunyanLogger, logger } from '../helpers/logger' import { CONFIG, isEmailEnabled } from '../initializers/config' import { WEBSERVER } from '../initializers/constants' -import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models' -import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' +import { MUser } from '../types/models' import { JobQueue } from './job-queue' -import { toSafeHtml } from '../helpers/markdown' const Email = require('email-templates') @@ -59,429 +54,6 @@ class Emailer { } } - addNewVideoFromSubscriberNotification (to: string[], video: MVideoAccountLight) { - const channelName = video.VideoChannel.getDisplayName() - const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() - - const emailPayload: EmailPayload = { - to, - subject: channelName + ' just published a new video', - text: `Your subscription ${channelName} just published a new video: "${video.name}".`, - locals: { - title: 'New content ', - action: { - text: 'View video', - url: videoUrl - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { - const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() - - const emailPayload: EmailPayload = { - template: 'follower-on-channel', - to, - subject: `New follower on your channel ${followingName}`, - locals: { - followerName: actorFollow.ActorFollower.Account.getDisplayName(), - followerUrl: actorFollow.ActorFollower.url, - followingName, - followingUrl: actorFollow.ActorFollowing.url, - followType - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { - const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' - - const emailPayload: EmailPayload = { - to, - subject: 'New instance follower', - text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`, - locals: { - title: 'New instance follower', - action: { - text: 'Review followers', - url: WEBSERVER.URL + '/admin/follows/followers-list' - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { - const instanceUrl = actorFollow.ActorFollowing.url - const emailPayload: EmailPayload = { - to, - subject: 'Auto instance following', - text: `Your instance automatically followed a new instance: ${instanceUrl}.` - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - myVideoPublishedNotification (to: string[], video: MVideo) { - const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() - - const emailPayload: EmailPayload = { - to, - subject: `Your video ${video.name} has been published`, - text: `Your video "${video.name}" has been published.`, - locals: { - title: 'You video is live', - action: { - text: 'View video', - url: videoUrl - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { - const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() - - const emailPayload: EmailPayload = { - to, - subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`, - text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`, - locals: { - title: 'Import complete', - action: { - text: 'View video', - url: videoUrl - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { - const importUrl = WEBSERVER.URL + '/my-library/video-imports' - - const text = - `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` + - '\n\n' + - `See your videos import dashboard for more information: ${importUrl}.` - - const emailPayload: EmailPayload = { - to, - subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`, - text, - locals: { - title: 'Import failed', - action: { - text: 'Review imports', - url: importUrl - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) { - const video = comment.Video - const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() - const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() - const commentHtml = toSafeHtml(comment.text) - - const emailPayload: EmailPayload = { - template: 'video-comment-new', - to, - subject: 'New comment on your video ' + video.name, - locals: { - accountName: comment.Account.getDisplayName(), - accountUrl: comment.Account.Actor.url, - comment, - commentHtml, - video, - videoUrl, - action: { - text: 'View comment', - url: commentUrl - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) { - const accountName = comment.Account.getDisplayName() - const video = comment.Video - const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() - const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() - const commentHtml = toSafeHtml(comment.text) - - const emailPayload: EmailPayload = { - template: 'video-comment-mention', - to, - subject: 'Mention on video ' + video.name, - locals: { - comment, - commentHtml, - video, - videoUrl, - accountName, - action: { - text: 'View comment', - url: commentUrl - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addAbuseModeratorsNotification (to: string[], parameters: { - abuse: UserAbuse - abuseInstance: MAbuseFull - reporter: string - }) { - const { abuse, abuseInstance, reporter } = parameters - - const action = { - text: 'View report #' + abuse.id, - url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id - } - - let emailPayload: EmailPayload - - if (abuseInstance.VideoAbuse) { - const video = abuseInstance.VideoAbuse.Video - const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() - - emailPayload = { - template: 'video-abuse-new', - to, - subject: `New video abuse report from ${reporter}`, - locals: { - videoUrl, - isLocal: video.remote === false, - videoCreatedAt: new Date(video.createdAt).toLocaleString(), - videoPublishedAt: new Date(video.publishedAt).toLocaleString(), - videoName: video.name, - reason: abuse.reason, - videoChannel: abuse.video.channel, - reporter, - action - } - } - } else if (abuseInstance.VideoCommentAbuse) { - const comment = abuseInstance.VideoCommentAbuse.VideoComment - const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId() - - emailPayload = { - template: 'video-comment-abuse-new', - to, - subject: `New comment abuse report from ${reporter}`, - locals: { - commentUrl, - videoName: comment.Video.name, - isLocal: comment.isOwned(), - commentCreatedAt: new Date(comment.createdAt).toLocaleString(), - reason: abuse.reason, - flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(), - reporter, - action - } - } - } else { - const account = abuseInstance.FlaggedAccount - const accountUrl = account.getClientUrl() - - emailPayload = { - template: 'account-abuse-new', - to, - subject: `New account abuse report from ${reporter}`, - locals: { - accountUrl, - accountDisplayName: account.getDisplayName(), - isLocal: account.isOwned(), - reason: abuse.reason, - reporter, - action - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addAbuseStateChangeNotification (to: string[], abuse: MAbuseFull) { - const text = abuse.state === AbuseState.ACCEPTED - ? 'Report #' + abuse.id + ' has been accepted' - : 'Report #' + abuse.id + ' has been rejected' - - const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id - - const action = { - text, - url: abuseUrl - } - - const emailPayload: EmailPayload = { - template: 'abuse-state-change', - to, - subject: text, - locals: { - action, - abuseId: abuse.id, - abuseUrl, - isAccepted: abuse.state === AbuseState.ACCEPTED - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addAbuseNewMessageNotification ( - to: string[], - options: { - target: 'moderator' | 'reporter' - abuse: MAbuseFull - message: MAbuseMessage - accountMessage: MAccountDefault - }) { - const { abuse, target, message, accountMessage } = options - - const text = 'New message on report #' + abuse.id - const abuseUrl = target === 'moderator' - ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id - : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id - - const action = { - text, - url: abuseUrl - } - - const emailPayload: EmailPayload = { - template: 'abuse-new-message', - to, - subject: text, - locals: { - abuseId: abuse.id, - abuseUrl: action.url, - messageAccountName: accountMessage.getDisplayName(), - messageText: message.message, - action - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { - const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' - const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() - const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() - - const emailPayload: EmailPayload = { - template: 'video-auto-blacklist-new', - to, - subject: 'A new video is pending moderation', - locals: { - channel, - videoUrl, - videoName: videoBlacklist.Video.name, - action: { - text: 'Review autoblacklist', - url: videoAutoBlacklistUrl - } - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addNewUserRegistrationNotification (to: string[], user: MUser) { - const emailPayload: EmailPayload = { - template: 'user-registered', - to, - subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${user.username}`, - locals: { - user - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addVideoBlacklistNotification (to: string[], videoBlacklist: MVideoBlacklistVideo) { - const videoName = videoBlacklist.Video.name - const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() - - const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' - const blockedString = `Your video ${videoName} (${videoUrl} on ${CONFIG.INSTANCE.NAME} has been blacklisted${reasonString}.` - - const emailPayload: EmailPayload = { - to, - subject: `Video ${videoName} blacklisted`, - text: blockedString, - locals: { - title: 'Your video was blacklisted' - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addVideoUnblacklistNotification (to: string[], video: MVideo) { - const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() - - const emailPayload: EmailPayload = { - to, - subject: `Video ${video.name} unblacklisted`, - text: `Your video "${video.name}" (${videoUrl}) on ${CONFIG.INSTANCE.NAME} has been unblacklisted.`, - locals: { - title: 'Your video was unblacklisted' - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addNewPeerTubeVersionNotification (to: string[], latestVersion: string) { - const emailPayload: EmailPayload = { - to, - template: 'peertube-version-new', - subject: `A new PeerTube version is available: ${latestVersion}`, - locals: { - latestVersion - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - - addNewPlugionVersionNotification (to: string[], plugin: MPlugin) { - const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type - - const emailPayload: EmailPayload = { - to, - template: 'plugin-version-new', - subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`, - locals: { - pluginName: plugin.name, - latestVersion: plugin.latestVersion, - pluginUrl - } - } - - return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) - } - addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { const emailPayload: EmailPayload = { template: 'password-reset', @@ -578,7 +150,11 @@ class Emailer { subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX }) - for (const to of options.to) { + const toEmails = isArray(options.to) + ? options.to + : [ options.to ] + + for (const to of toEmails) { const baseOptions: SendEmailDefaultOptions = { template: 'common', message: { diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 6e425d09c..5fd2039b1 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts @@ -235,7 +235,7 @@ async function processFile (downloader: () => Promise, videoImport: MVid }) }) - Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) + Notifier.Instance.notifyOnFinishedVideoImport({ videoImport: videoImportUpdated, success: true }) if (video.isBlacklisted()) { const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video }) @@ -263,7 +263,7 @@ async function processFile (downloader: () => Promise, videoImport: MVid } await videoImport.save() - Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false) + Notifier.Instance.notifyOnFinishedVideoImport({ videoImport, success: false }) throw err } diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts deleted file mode 100644 index 1f9ff16df..000000000 --- a/server/lib/notifier.ts +++ /dev/null @@ -1,796 +0,0 @@ -import { AccountModel } from '@server/models/account/account' -import { getServerActor } from '@server/models/application/application' -import { ServerBlocklistModel } from '@server/models/server/server-blocklist' -import { - MUser, - MUserAccount, - MUserDefault, - MUserNotifSettingAccount, - MUserWithNotificationSetting, - UserNotificationModelForApi -} from '@server/types/models/user' -import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' -import { MVideoImportVideo } from '@server/types/models/video/video-import' -import { UserAbuse } from '@shared/models' -import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' -import { VideoPrivacy, VideoState } from '../../shared/models/videos' -import { logger } from '../helpers/logger' -import { CONFIG } from '../initializers/config' -import { AccountBlocklistModel } from '../models/account/account-blocklist' -import { UserModel } from '../models/user/user' -import { UserNotificationModel } from '../models/user/user-notification' -import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models' -import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' -import { isBlockedByServerOrAccount } from './blocklist' -import { Emailer } from './emailer' -import { PeerTubeSocket } from './peertube-socket' - -class Notifier { - - private static instance: Notifier - - private constructor () { - } - - notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void { - // Only notify on public and published videos which are not blacklisted - if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return - - this.notifySubscribersOfNewVideo(video) - .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) - } - - notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): 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 after transcoding.', video.url, { err })) - } - - notifyOnVideoPublishedAfterScheduledUpdate (video: MVideoFullLight): 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: MVideoFullLight): 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 }) - }) - } - - notifyOnNewComment (comment: MCommentOwnerVideo): void { - this.notifyVideoOwnerOfNewComment(comment) - .catch(err => logger.error('Cannot notify video owner of new comment %s.', comment.url, { err })) - - this.notifyOfCommentMention(comment) - .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) - } - - notifyOnNewAbuse (parameters: { abuse: UserAbuse, abuseInstance: MAbuseFull, reporter: string }): void { - this.notifyModeratorsOfNewAbuse(parameters) - .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err })) - } - - notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { - this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist) - .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err })) - } - - notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { - this.notifyVideoOwnerOfBlacklist(videoBlacklist) - .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) - } - - notifyOnVideoUnblacklist (video: MVideoFullLight): void { - this.notifyVideoOwnerOfUnblacklist(video) - .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err })) - } - - notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void { - this.notifyOwnerVideoImportIsFinished(videoImport, success) - .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) - } - - notifyOnNewUserRegistration (user: MUserDefault): void { - this.notifyModeratorsOfNewUserRegistration(user) - .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) - } - - notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { - this.notifyUserOfNewActorFollow(actorFollow) - .catch(err => { - logger.error( - 'Cannot notify owner of channel %s of a new follow by %s.', - actorFollow.ActorFollowing.VideoChannel.getDisplayName(), - actorFollow.ActorFollower.Account.getDisplayName(), - { err } - ) - }) - } - - notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void { - this.notifyAdminsOfNewInstanceFollow(actorFollow) - .catch(err => { - logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err }) - }) - } - - notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void { - this.notifyAdminsOfAutoInstanceFollowing(actorFollow) - .catch(err => { - logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err }) - }) - } - - notifyOnAbuseStateChange (abuse: MAbuseFull): void { - this.notifyReporterOfAbuseStateChange(abuse) - .catch(err => { - logger.error('Cannot notify reporter of abuse %d state change.', abuse.id, { err }) - }) - } - - notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void { - this.notifyOfNewAbuseMessage(abuse, message) - .catch(err => { - logger.error('Cannot notify on new abuse %d message.', abuse.id, { err }) - }) - } - - notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { - this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion) - .catch(err => { - logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err }) - }) - } - - notifyOfNewPluginVersion (plugin: MPlugin) { - this.notifyAdminsOfNewPluginVersion(plugin) - .catch(err => { - logger.error('Cannot notify on new plugin version %s.', plugin.name, { err }) - }) - } - - private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { - // List all followers that are users - const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) - - logger.info('Notifying %d users of new video %s.', users.length, video.url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.newVideoFromSubscription - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION, - userId: user.id, - videoId: video.id - }) - notification.Video = video - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addNewVideoFromSubscriberNotification(emails, video) - } - - return this.notify({ users, settingGetter, notificationCreator, emailSender }) - } - - private async notifyVideoOwnerOfNewComment (comment: MCommentOwnerVideo) { - if (comment.Video.isOwned() === false) return - - const user = await UserModel.loadByVideoId(comment.videoId) - - // Not our user or user comments its own video - if (!user || comment.Account.userId === user.id) return - - if (await this.isBlockedByServerOrUser(comment.Account, user)) return - - logger.info('Notifying user %s of new comment %s.', user.username, comment.url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.newCommentOnMyVideo - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, - userId: user.id, - commentId: comment.id - }) - notification.Comment = comment - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addNewCommentOnMyVideoNotification(emails, comment) - } - - return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) - } - - private async notifyOfCommentMention (comment: MCommentOwnerVideo) { - const extractedUsernames = comment.extractMentions() - logger.debug( - 'Extracted %d username from comment %s.', extractedUsernames.length, comment.url, - { usernames: extractedUsernames, text: comment.text } - ) - - let users = await UserModel.listByUsernames(extractedUsernames) - - if (comment.Video.isOwned()) { - const userException = await UserModel.loadByVideoId(comment.videoId) - users = users.filter(u => u.id !== userException.id) - } - - // Don't notify if I mentioned myself - users = users.filter(u => u.Account.id !== comment.accountId) - - if (users.length === 0) return - - const serverAccountId = (await getServerActor()).Account.id - const sourceAccounts = users.map(u => u.Account.id).concat([ serverAccountId ]) - - const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, comment.accountId) - const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, comment.Account.Actor.serverId) - - logger.info('Notifying %d users of new comment %s.', users.length, comment.url) - - function settingGetter (user: MUserNotifSettingAccount) { - const accountId = user.Account.id - if ( - accountMutedHash[accountId] === true || instanceMutedHash[accountId] === true || - accountMutedHash[serverAccountId] === true || instanceMutedHash[serverAccountId] === true - ) { - return UserNotificationSettingValue.NONE - } - - return user.NotificationSetting.commentMention - } - - async function notificationCreator (user: MUserNotifSettingAccount) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.COMMENT_MENTION, - userId: user.id, - commentId: comment.id - }) - notification.Comment = comment - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addNewCommentMentionNotification(emails, comment) - } - - return this.notify({ users, settingGetter, notificationCreator, emailSender }) - } - - private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFull) { - if (actorFollow.ActorFollowing.isOwned() === false) return - - // Account follows one of our account? - let followType: 'account' | 'channel' = 'channel' - let user = await UserModel.loadByChannelActorId(actorFollow.ActorFollowing.id) - - // Account follows one of our channel? - if (!user) { - user = await UserModel.loadByAccountActorId(actorFollow.ActorFollowing.id) - followType = 'account' - } - - if (!user) return - - const followerAccount = actorFollow.ActorFollower.Account - const followerAccountWithActor = Object.assign(followerAccount, { Actor: actorFollow.ActorFollower }) - - if (await this.isBlockedByServerOrUser(followerAccountWithActor, user)) return - - logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.newFollow - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_FOLLOW, - userId: user.id, - actorFollowId: actorFollow.id - }) - notification.ActorFollow = actorFollow - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addNewFollowNotification(emails, actorFollow, followType) - } - - return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) - } - - private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) { - const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) - - const follower = Object.assign(actorFollow.ActorFollower.Account, { Actor: actorFollow.ActorFollower }) - if (await this.isBlockedByServerOrUser(follower)) return - - logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.newInstanceFollower - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_INSTANCE_FOLLOWER, - userId: user.id, - actorFollowId: actorFollow.id - }) - notification.ActorFollow = actorFollow - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addNewInstanceFollowerNotification(emails, actorFollow) - } - - return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) - } - - private async notifyAdminsOfAutoInstanceFollowing (actorFollow: MActorFollowFull) { - const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) - - logger.info('Notifying %d administrators of auto instance following: %s.', admins.length, actorFollow.ActorFollowing.url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.autoInstanceFollowing - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.AUTO_INSTANCE_FOLLOWING, - userId: user.id, - actorFollowId: actorFollow.id - }) - notification.ActorFollow = actorFollow - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addAutoInstanceFollowingNotification(emails, actorFollow) - } - - return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) - } - - private async notifyModeratorsOfNewAbuse (parameters: { - abuse: UserAbuse - abuseInstance: MAbuseFull - reporter: string - }) { - const { abuse, abuseInstance } = parameters - - const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) - if (moderators.length === 0) return - - const url = this.getAbuseUrl(abuseInstance) - - logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.abuseAsModerator - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_ABUSE_FOR_MODERATORS, - userId: user.id, - abuseId: abuse.id - }) - notification.Abuse = abuseInstance - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters) - } - - return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) - } - - private async notifyReporterOfAbuseStateChange (abuse: MAbuseFull) { - // Only notify our users - if (abuse.ReporterAccount.isOwned() !== true) return - - const url = this.getAbuseUrl(abuse) - - logger.info('Notifying reporter of abuse % of state change.', url) - - const reporter = await UserModel.loadByAccountActorId(abuse.ReporterAccount.actorId) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.abuseStateChange - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.ABUSE_STATE_CHANGE, - userId: user.id, - abuseId: abuse.id - }) - notification.Abuse = abuse - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addAbuseStateChangeNotification(emails, abuse) - } - - return this.notify({ users: [ reporter ], settingGetter, notificationCreator, emailSender }) - } - - private async notifyOfNewAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage) { - const url = this.getAbuseUrl(abuse) - logger.info('Notifying reporter and moderators of new abuse message on %s.', url) - - const accountMessage = await AccountModel.load(message.accountId) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.abuseNewMessage - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.ABUSE_NEW_MESSAGE, - userId: user.id, - abuseId: abuse.id - }) - notification.Abuse = abuse - - return notification - } - - function emailSenderReporter (emails: string[]) { - return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message, accountMessage }) - } - - function emailSenderModerators (emails: string[]) { - return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message, accountMessage }) - } - - async function buildReporterOptions () { - // Only notify our users - if (abuse.ReporterAccount.isOwned() !== true) return undefined - - const reporter = await UserModel.loadByAccountActorId(abuse.ReporterAccount.actorId) - // Don't notify my own message - if (reporter.Account.id === message.accountId) return undefined - - return { users: [ reporter ], settingGetter, notificationCreator, emailSender: emailSenderReporter } - } - - async function buildModeratorsOptions () { - let moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) - // Don't notify my own message - moderators = moderators.filter(m => m.Account.id !== message.accountId) - - if (moderators.length === 0) return undefined - - return { users: moderators, settingGetter, notificationCreator, emailSender: emailSenderModerators } - } - - const options = await Promise.all([ - buildReporterOptions(), - buildModeratorsOptions() - ]) - - return Promise.all( - options - .filter(opt => !!opt) - .map(opt => this.notify(opt)) - ) - } - - private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) { - 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, videoBlacklist.Video.url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.videoAutoBlacklistAsModerator - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS, - userId: user.id, - videoBlacklistId: videoBlacklist.id - }) - notification.VideoBlacklist = videoBlacklist - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, videoBlacklist) - } - - return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) - } - - private async notifyVideoOwnerOfBlacklist (videoBlacklist: MVideoBlacklistVideo) { - const user = await UserModel.loadByVideoId(videoBlacklist.videoId) - if (!user) return - - logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.blacklistOnMyVideo - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.BLACKLIST_ON_MY_VIDEO, - userId: user.id, - videoBlacklistId: videoBlacklist.id - }) - notification.VideoBlacklist = videoBlacklist - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addVideoBlacklistNotification(emails, videoBlacklist) - } - - return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) - } - - private async notifyVideoOwnerOfUnblacklist (video: MVideoFullLight) { - const user = await UserModel.loadByVideoId(video.id) - if (!user) return - - logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.blacklistOnMyVideo - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO, - userId: user.id, - videoId: video.id - }) - notification.Video = video - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addVideoUnblacklistNotification(emails, video) - } - - return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) - } - - private async notifyOwnedVideoHasBeenPublished (video: MVideoFullLight) { - const user = await UserModel.loadByVideoId(video.id) - if (!user) return - - logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.myVideoPublished - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.MY_VIDEO_PUBLISHED, - userId: user.id, - videoId: video.id - }) - notification.Video = video - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.myVideoPublishedNotification(emails, video) - } - - return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) - } - - private async notifyOwnerVideoImportIsFinished (videoImport: MVideoImportVideo, success: boolean) { - const user = await UserModel.loadByVideoImportId(videoImport.id) - if (!user) return - - logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier()) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.myVideoImportFinished - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR, - userId: user.id, - videoImportId: videoImport.id - }) - notification.VideoImport = videoImport - - return notification - } - - function emailSender (emails: string[]) { - return success - ? Emailer.Instance.myVideoImportSuccessNotification(emails, videoImport) - : Emailer.Instance.myVideoImportErrorNotification(emails, videoImport) - } - - return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) - } - - private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserDefault) { - const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS) - if (moderators.length === 0) return - - logger.info( - 'Notifying %s moderators of new user registration of %s.', - moderators.length, registeredUser.username - ) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.newUserRegistration - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_USER_REGISTRATION, - userId: user.id, - accountId: registeredUser.Account.id - }) - notification.Account = registeredUser.Account - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addNewUserRegistrationNotification(emails, registeredUser) - } - - return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) - } - - private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { - // Use the debug right to know who is an administrator - const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) - if (admins.length === 0) return - - logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.newPeerTubeVersion - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_PEERTUBE_VERSION, - userId: user.id, - applicationId: application.id - }) - notification.Application = application - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion) - } - - return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) - } - - private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) { - // Use the debug right to know who is an administrator - const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) - if (admins.length === 0) return - - logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion) - - function settingGetter (user: MUserWithNotificationSetting) { - return user.NotificationSetting.newPluginVersion - } - - async function notificationCreator (user: MUserWithNotificationSetting) { - const notification = await UserNotificationModel.create({ - type: UserNotificationType.NEW_PLUGIN_VERSION, - userId: user.id, - pluginId: plugin.id - }) - notification.Plugin = plugin - - return notification - } - - function emailSender (emails: string[]) { - return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin) - } - - return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) - } - - private async notify (options: { - users: T[] - notificationCreator: (user: T) => Promise - emailSender: (emails: string[]) => void - settingGetter: (user: T) => UserNotificationSettingValue - }) { - const emails: string[] = [] - - for (const user of options.users) { - if (this.isWebNotificationEnabled(options.settingGetter(user))) { - const notification = await options.notificationCreator(user) - - PeerTubeSocket.Instance.sendNotification(user.id, notification) - } - - if (this.isEmailEnabled(user, options.settingGetter(user))) { - emails.push(user.email) - } - } - - if (emails.length !== 0) { - options.emailSender(emails) - } - } - - private isEmailEnabled (user: MUser, value: UserNotificationSettingValue) { - if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false - - return value & UserNotificationSettingValue.EMAIL - } - - private isWebNotificationEnabled (value: UserNotificationSettingValue) { - return value & UserNotificationSettingValue.WEB - } - - private isBlockedByServerOrUser (targetAccount: MAccountServer, user?: MUserAccount) { - return isBlockedByServerOrAccount(targetAccount, user?.Account) - } - - private getAbuseUrl (abuse: MAbuseFull) { - return abuse.VideoAbuse?.Video?.url || - abuse.VideoCommentAbuse?.VideoComment?.url || - abuse.FlaggedAccount.Actor.url - } - - static get Instance () { - return this.instance || (this.instance = new this()) - } -} - -// --------------------------------------------------------------------------- - -export { - Notifier -} diff --git a/server/lib/notifier/index.ts b/server/lib/notifier/index.ts new file mode 100644 index 000000000..5bc2f5f50 --- /dev/null +++ b/server/lib/notifier/index.ts @@ -0,0 +1 @@ +export * from './notifier' diff --git a/server/lib/notifier/notifier.ts b/server/lib/notifier/notifier.ts new file mode 100644 index 000000000..8b68d2e69 --- /dev/null +++ b/server/lib/notifier/notifier.ts @@ -0,0 +1,259 @@ +import { MUser, MUserDefault } from '@server/types/models/user' +import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' +import { UserNotificationSettingValue } from '../../../shared/models/users' +import { logger } from '../../helpers/logger' +import { CONFIG } from '../../initializers/config' +import { MAbuseFull, MAbuseMessage, MActorFollowFull, MApplication, MPlugin } from '../../types/models' +import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../../types/models/video' +import { JobQueue } from '../job-queue' +import { PeerTubeSocket } from '../peertube-socket' +import { + AbstractNotification, + AbuseStateChangeForReporter, + AutoFollowForInstance, + CommentMention, + FollowForInstance, + FollowForUser, + ImportFinishedForOwner, + ImportFinishedForOwnerPayload, + NewAbuseForModerators, + NewAbuseMessageForModerators, + NewAbuseMessageForReporter, + NewAbusePayload, + NewAutoBlacklistForModerators, + NewBlacklistForOwner, + NewCommentForVideoOwner, + NewPeerTubeVersionForAdmins, + NewPluginVersionForAdmins, + NewVideoForSubscribers, + OwnedPublicationAfterAutoUnblacklist, + OwnedPublicationAfterScheduleUpdate, + OwnedPublicationAfterTranscoding, + RegistrationForModerators, + UnblacklistForOwner +} from './shared' + +class Notifier { + + private readonly notificationModels = { + newVideo: [ NewVideoForSubscribers ], + publicationAfterTranscoding: [ OwnedPublicationAfterTranscoding ], + publicationAfterScheduleUpdate: [ OwnedPublicationAfterScheduleUpdate ], + publicationAfterAutoUnblacklist: [ OwnedPublicationAfterAutoUnblacklist ], + newComment: [ CommentMention, NewCommentForVideoOwner ], + newAbuse: [ NewAbuseForModerators ], + newBlacklist: [ NewBlacklistForOwner ], + unblacklist: [ UnblacklistForOwner ], + importFinished: [ ImportFinishedForOwner ], + userRegistration: [ RegistrationForModerators ], + userFollow: [ FollowForUser ], + instanceFollow: [ FollowForInstance ], + autoInstanceFollow: [ AutoFollowForInstance ], + newAutoBlacklist: [ NewAutoBlacklistForModerators ], + abuseStateChange: [ AbuseStateChangeForReporter ], + newAbuseMessage: [ NewAbuseMessageForReporter, NewAbuseMessageForModerators ], + newPeertubeVersion: [ NewPeerTubeVersionForAdmins ], + newPluginVersion: [ NewPluginVersionForAdmins ] + } + + private static instance: Notifier + + private constructor () { + } + + notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void { + const models = this.notificationModels.newVideo + + this.sendNotifications(models, video) + .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) + } + + notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void { + const models = this.notificationModels.publicationAfterTranscoding + + this.sendNotifications(models, video) + .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err })) + } + + notifyOnVideoPublishedAfterScheduledUpdate (video: MVideoFullLight): void { + const models = this.notificationModels.publicationAfterScheduleUpdate + + this.sendNotifications(models, video) + .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err })) + } + + notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: MVideoFullLight): void { + const models = this.notificationModels.publicationAfterAutoUnblacklist + + this.sendNotifications(models, video) + .catch(err => { + logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err }) + }) + } + + notifyOnNewComment (comment: MCommentOwnerVideo): void { + const models = this.notificationModels.newComment + + this.sendNotifications(models, comment) + .catch(err => logger.error('Cannot notify of new comment.', comment.url, { err })) + } + + notifyOnNewAbuse (payload: NewAbusePayload): void { + const models = this.notificationModels.newAbuse + + this.sendNotifications(models, payload) + .catch(err => logger.error('Cannot notify of new abuse %d.', payload.abuseInstance.id, { err })) + } + + notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { + const models = this.notificationModels.newAutoBlacklist + + this.sendNotifications(models, videoBlacklist) + .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err })) + } + + notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { + const models = this.notificationModels.newBlacklist + + this.sendNotifications(models, videoBlacklist) + .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) + } + + notifyOnVideoUnblacklist (video: MVideoFullLight): void { + const models = this.notificationModels.unblacklist + + this.sendNotifications(models, video) + .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err })) + } + + notifyOnFinishedVideoImport (payload: ImportFinishedForOwnerPayload): void { + const models = this.notificationModels.importFinished + + this.sendNotifications(models, payload) + .catch(err => { + logger.error('Cannot notify owner that its video import %s is finished.', payload.videoImport.getTargetIdentifier(), { err }) + }) + } + + notifyOnNewUserRegistration (user: MUserDefault): void { + const models = this.notificationModels.userRegistration + + this.sendNotifications(models, user) + .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) + } + + notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { + const models = this.notificationModels.userFollow + + this.sendNotifications(models, actorFollow) + .catch(err => { + logger.error( + 'Cannot notify owner of channel %s of a new follow by %s.', + actorFollow.ActorFollowing.VideoChannel.getDisplayName(), + actorFollow.ActorFollower.Account.getDisplayName(), + { err } + ) + }) + } + + notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void { + const models = this.notificationModels.instanceFollow + + this.sendNotifications(models, actorFollow) + .catch(err => logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })) + } + + notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void { + const models = this.notificationModels.autoInstanceFollow + + this.sendNotifications(models, actorFollow) + .catch(err => logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err })) + } + + notifyOnAbuseStateChange (abuse: MAbuseFull): void { + const models = this.notificationModels.abuseStateChange + + this.sendNotifications(models, abuse) + .catch(err => logger.error('Cannot notify of abuse %d state change.', abuse.id, { err })) + } + + notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void { + const models = this.notificationModels.newAbuseMessage + + this.sendNotifications(models, { abuse, message }) + .catch(err => logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })) + } + + notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { + const models = this.notificationModels.newPeertubeVersion + + this.sendNotifications(models, { application, latestVersion }) + .catch(err => logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err })) + } + + notifyOfNewPluginVersion (plugin: MPlugin) { + const models = this.notificationModels.newPluginVersion + + this.sendNotifications(models, plugin) + .catch(err => logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })) + } + + private async notify (object: AbstractNotification) { + await object.prepare() + + const users = object.getTargetUsers() + + if (users.length === 0) return + if (await object.isDisabled()) return + + object.log() + + const toEmails: string[] = [] + + for (const user of users) { + const setting = object.getSetting(user) + + if (this.isWebNotificationEnabled(setting)) { + const notification = await object.createNotification(user) + + PeerTubeSocket.Instance.sendNotification(user.id, notification) + } + + if (this.isEmailEnabled(user, setting)) { + toEmails.push(user.email) + } + } + + for (const to of toEmails) { + const payload = await object.createEmail(to) + JobQueue.Instance.createJob({ type: 'email', payload }) + } + } + + private isEmailEnabled (user: MUser, value: UserNotificationSettingValue) { + if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false + + return value & UserNotificationSettingValue.EMAIL + } + + private isWebNotificationEnabled (value: UserNotificationSettingValue) { + return value & UserNotificationSettingValue.WEB + } + + private async sendNotifications (models: (new (payload: T) => AbstractNotification)[], payload: T) { + for (const model of models) { + // eslint-disable-next-line new-cap + await this.notify(new model(payload)) + } + } + + static get Instance () { + return this.instance || (this.instance = new this()) + } +} + +// --------------------------------------------------------------------------- + +export { + Notifier +} diff --git a/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts new file mode 100644 index 000000000..1425c38ec --- /dev/null +++ b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts @@ -0,0 +1,67 @@ +import { WEBSERVER } from '@server/initializers/constants' +import { AccountModel } from '@server/models/account/account' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MAbuseFull, MAbuseMessage, MAccountDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export type NewAbuseMessagePayload = { + abuse: MAbuseFull + message: MAbuseMessage +} + +export abstract class AbstractNewAbuseMessage extends AbstractNotification { + protected messageAccount: MAccountDefault + + async loadMessageAccount () { + this.messageAccount = await AccountModel.load(this.message.accountId) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.abuseNewMessage + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.ABUSE_NEW_MESSAGE, + userId: user.id, + abuseId: this.abuse.id + }) + notification.Abuse = this.abuse + + return notification + } + + protected createEmailFor (to: string, target: 'moderator' | 'reporter') { + const text = 'New message on report #' + this.abuse.id + const abuseUrl = target === 'moderator' + ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + this.abuse.id + : WEBSERVER.URL + '/my-account/abuses?search=%23' + this.abuse.id + + const action = { + text, + url: abuseUrl + } + + return { + template: 'abuse-new-message', + to, + subject: text, + locals: { + abuseId: this.abuse.id, + abuseUrl: action.url, + messageAccountName: this.messageAccount.getDisplayName(), + messageText: this.message.message, + action + } + } + } + + protected get abuse () { + return this.payload.abuse + } + + protected get message () { + return this.payload.message + } +} diff --git a/server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts b/server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts new file mode 100644 index 000000000..968b5bca9 --- /dev/null +++ b/server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts @@ -0,0 +1,74 @@ +import { logger } from '@server/helpers/logger' +import { WEBSERVER } from '@server/initializers/constants' +import { getAbuseTargetUrl } from '@server/lib/activitypub/url' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MAbuseFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { AbuseState, UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class AbuseStateChangeForReporter extends AbstractNotification { + + private user: MUserDefault + + async prepare () { + const reporter = this.abuse.ReporterAccount + if (reporter.isOwned() !== true) return + + this.user = await UserModel.loadByAccountActorId(this.abuse.ReporterAccount.actorId) + } + + log () { + logger.info('Notifying reporter of abuse % of state change.', getAbuseTargetUrl(this.abuse)) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.abuseStateChange + } + + getTargetUsers () { + if (!this.user) return [] + + return [ this.user ] + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.ABUSE_STATE_CHANGE, + userId: user.id, + abuseId: this.abuse.id + }) + notification.Abuse = this.abuse + + return notification + } + + createEmail (to: string) { + const text = this.abuse.state === AbuseState.ACCEPTED + ? 'Report #' + this.abuse.id + ' has been accepted' + : 'Report #' + this.abuse.id + ' has been rejected' + + const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + this.abuse.id + + const action = { + text, + url: abuseUrl + } + + return { + template: 'abuse-state-change', + to, + subject: text, + locals: { + action, + abuseId: this.abuse.id, + abuseUrl, + isAccepted: this.abuse.state === AbuseState.ACCEPTED + } + } + } + + private get abuse () { + return this.payload + } +} diff --git a/server/lib/notifier/shared/abuse/index.ts b/server/lib/notifier/shared/abuse/index.ts new file mode 100644 index 000000000..7b54c5591 --- /dev/null +++ b/server/lib/notifier/shared/abuse/index.ts @@ -0,0 +1,4 @@ +export * from './abuse-state-change-for-reporter' +export * from './new-abuse-for-moderators' +export * from './new-abuse-message-for-reporter' +export * from './new-abuse-message-for-moderators' diff --git a/server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts b/server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts new file mode 100644 index 000000000..c3c7c5515 --- /dev/null +++ b/server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts @@ -0,0 +1,119 @@ +import { logger } from '@server/helpers/logger' +import { WEBSERVER } from '@server/initializers/constants' +import { getAbuseTargetUrl } from '@server/lib/activitypub/url' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MAbuseFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserAbuse, UserNotificationType, UserRight } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export type NewAbusePayload = { abuse: UserAbuse, abuseInstance: MAbuseFull, reporter: string } + +export class NewAbuseForModerators extends AbstractNotification { + private moderators: MUserDefault[] + + async prepare () { + this.moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) + } + + log () { + logger.info('Notifying %s user/moderators of new abuse %s.', this.moderators.length, getAbuseTargetUrl(this.payload.abuseInstance)) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.abuseAsModerator + } + + getTargetUsers () { + return this.moderators + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_ABUSE_FOR_MODERATORS, + userId: user.id, + abuseId: this.payload.abuseInstance.id + }) + notification.Abuse = this.payload.abuseInstance + + return notification + } + + createEmail (to: string) { + const abuseInstance = this.payload.abuseInstance + + if (abuseInstance.VideoAbuse) return this.createVideoAbuseEmail(to) + if (abuseInstance.VideoCommentAbuse) return this.createCommentAbuseEmail(to) + + return this.createAccountAbuseEmail(to) + } + + private createVideoAbuseEmail (to: string) { + const video = this.payload.abuseInstance.VideoAbuse.Video + const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() + + return { + template: 'video-abuse-new', + to, + subject: `New video abuse report from ${this.payload.reporter}`, + locals: { + videoUrl, + isLocal: video.remote === false, + videoCreatedAt: new Date(video.createdAt).toLocaleString(), + videoPublishedAt: new Date(video.publishedAt).toLocaleString(), + videoName: video.name, + reason: this.payload.abuse.reason, + videoChannel: this.payload.abuse.video.channel, + reporter: this.payload.reporter, + action: this.buildEmailAction() + } + } + } + + private createCommentAbuseEmail (to: string) { + const comment = this.payload.abuseInstance.VideoCommentAbuse.VideoComment + const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId() + + return { + template: 'video-comment-abuse-new', + to, + subject: `New comment abuse report from ${this.payload.reporter}`, + locals: { + commentUrl, + videoName: comment.Video.name, + isLocal: comment.isOwned(), + commentCreatedAt: new Date(comment.createdAt).toLocaleString(), + reason: this.payload.abuse.reason, + flaggedAccount: this.payload.abuseInstance.FlaggedAccount.getDisplayName(), + reporter: this.payload.reporter, + action: this.buildEmailAction() + } + } + } + + private createAccountAbuseEmail (to: string) { + const account = this.payload.abuseInstance.FlaggedAccount + const accountUrl = account.getClientUrl() + + return { + template: 'account-abuse-new', + to, + subject: `New account abuse report from ${this.payload.reporter}`, + locals: { + accountUrl, + accountDisplayName: account.getDisplayName(), + isLocal: account.isOwned(), + reason: this.payload.abuse.reason, + reporter: this.payload.reporter, + action: this.buildEmailAction() + } + } + } + + private buildEmailAction () { + return { + text: 'View report #' + this.payload.abuseInstance.id, + url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + this.payload.abuseInstance.id + } + } +} diff --git a/server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts b/server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts new file mode 100644 index 000000000..9d0629690 --- /dev/null +++ b/server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts @@ -0,0 +1,32 @@ +import { logger } from '@server/helpers/logger' +import { getAbuseTargetUrl } from '@server/lib/activitypub/url' +import { UserModel } from '@server/models/user/user' +import { MUserDefault } from '@server/types/models' +import { UserRight } from '@shared/models' +import { AbstractNewAbuseMessage } from './abstract-new-abuse-message' + +export class NewAbuseMessageForModerators extends AbstractNewAbuseMessage { + private moderators: MUserDefault[] + + async prepare () { + this.moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) + + // Don't notify my own message + this.moderators = this.moderators.filter(m => m.Account.id !== this.message.accountId) + if (this.moderators.length === 0) return + + await this.loadMessageAccount() + } + + log () { + logger.info('Notifying moderators of new abuse message on %s.', getAbuseTargetUrl(this.abuse)) + } + + getTargetUsers () { + return this.moderators + } + + createEmail (to: string) { + return this.createEmailFor(to, 'moderator') + } +} diff --git a/server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts b/server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts new file mode 100644 index 000000000..c5bbb5447 --- /dev/null +++ b/server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts @@ -0,0 +1,36 @@ +import { logger } from '@server/helpers/logger' +import { getAbuseTargetUrl } from '@server/lib/activitypub/url' +import { UserModel } from '@server/models/user/user' +import { MUserDefault } from '@server/types/models' +import { AbstractNewAbuseMessage } from './abstract-new-abuse-message' + +export class NewAbuseMessageForReporter extends AbstractNewAbuseMessage { + private reporter: MUserDefault + + async prepare () { + // Only notify our users + if (this.abuse.ReporterAccount.isOwned() !== true) return + + await this.loadMessageAccount() + + const reporter = await UserModel.loadByAccountActorId(this.abuse.ReporterAccount.actorId) + // Don't notify my own message + if (reporter.Account.id === this.message.accountId) return + + this.reporter = reporter + } + + log () { + logger.info('Notifying reporter of new abuse message on %s.', getAbuseTargetUrl(this.abuse)) + } + + getTargetUsers () { + if (!this.reporter) return [] + + return [ this.reporter ] + } + + createEmail (to: string) { + return this.createEmailFor(to, 'reporter') + } +} diff --git a/server/lib/notifier/shared/blacklist/index.ts b/server/lib/notifier/shared/blacklist/index.ts new file mode 100644 index 000000000..2f98d88ae --- /dev/null +++ b/server/lib/notifier/shared/blacklist/index.ts @@ -0,0 +1,3 @@ +export * from './new-auto-blacklist-for-moderators' +export * from './new-blacklist-for-owner' +export * from './unblacklist-for-owner' diff --git a/server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts b/server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts new file mode 100644 index 000000000..a92a49a0c --- /dev/null +++ b/server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts @@ -0,0 +1,60 @@ +import { logger } from '@server/helpers/logger' +import { WEBSERVER } from '@server/initializers/constants' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { VideoChannelModel } from '@server/models/video/video-channel' +import { MUserDefault, MUserWithNotificationSetting, MVideoBlacklistLightVideo, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType, UserRight } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class NewAutoBlacklistForModerators extends AbstractNotification { + private moderators: MUserDefault[] + + async prepare () { + this.moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST) + } + + log () { + logger.info('Notifying %s moderators of video auto-blacklist %s.', this.moderators.length, this.payload.Video.url) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.videoAutoBlacklistAsModerator + } + + getTargetUsers () { + return this.moderators + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS, + userId: user.id, + videoBlacklistId: this.payload.id + }) + notification.VideoBlacklist = this.payload + + return notification + } + + async createEmail (to: string) { + const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' + const videoUrl = WEBSERVER.URL + this.payload.Video.getWatchStaticPath() + const channel = await VideoChannelModel.loadAndPopulateAccount(this.payload.Video.channelId) + + return { + template: 'video-auto-blacklist-new', + to, + subject: 'A new video is pending moderation', + locals: { + channel: channel.toFormattedSummaryJSON(), + videoUrl, + videoName: this.payload.Video.name, + action: { + text: 'Review autoblacklist', + url: videoAutoBlacklistUrl + } + } + } + } +} diff --git a/server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts b/server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts new file mode 100644 index 000000000..45bc30eb2 --- /dev/null +++ b/server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts @@ -0,0 +1,58 @@ +import { logger } from '@server/helpers/logger' +import { CONFIG } from '@server/initializers/config' +import { WEBSERVER } from '@server/initializers/constants' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MUserDefault, MUserWithNotificationSetting, MVideoBlacklistVideo, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class NewBlacklistForOwner extends AbstractNotification { + private user: MUserDefault + + async prepare () { + this.user = await UserModel.loadByVideoId(this.payload.videoId) + } + + log () { + logger.info('Notifying user %s that its video %s has been blacklisted.', this.user.username, this.payload.Video.url) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.blacklistOnMyVideo + } + + getTargetUsers () { + if (!this.user) return [] + + return [ this.user ] + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.BLACKLIST_ON_MY_VIDEO, + userId: user.id, + videoBlacklistId: this.payload.id + }) + notification.VideoBlacklist = this.payload + + return notification + } + + createEmail (to: string) { + const videoName = this.payload.Video.name + const videoUrl = WEBSERVER.URL + this.payload.Video.getWatchStaticPath() + + const reasonString = this.payload.reason ? ` for the following reason: ${this.payload.reason}` : '' + const blockedString = `Your video ${videoName} (${videoUrl} on ${CONFIG.INSTANCE.NAME} has been blacklisted${reasonString}.` + + return { + to, + subject: `Video ${videoName} blacklisted`, + text: blockedString, + locals: { + title: 'Your video was blacklisted' + } + } + } +} diff --git a/server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts b/server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts new file mode 100644 index 000000000..21f5a1c2d --- /dev/null +++ b/server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts @@ -0,0 +1,55 @@ +import { logger } from '@server/helpers/logger' +import { CONFIG } from '@server/initializers/config' +import { WEBSERVER } from '@server/initializers/constants' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class UnblacklistForOwner extends AbstractNotification { + private user: MUserDefault + + async prepare () { + this.user = await UserModel.loadByVideoId(this.payload.id) + } + + log () { + logger.info('Notifying user %s that its video %s has been unblacklisted.', this.user.username, this.payload.url) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.blacklistOnMyVideo + } + + getTargetUsers () { + if (!this.user) return [] + + return [ this.user ] + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO, + userId: user.id, + videoId: this.payload.id + }) + notification.Video = this.payload + + return notification + } + + createEmail (to: string) { + const video = this.payload + const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() + + return { + to, + subject: `Video ${video.name} unblacklisted`, + text: `Your video "${video.name}" (${videoUrl}) on ${CONFIG.INSTANCE.NAME} has been unblacklisted.`, + locals: { + title: 'Your video was unblacklisted' + } + } + } +} diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts new file mode 100644 index 000000000..4f84d8dea --- /dev/null +++ b/server/lib/notifier/shared/comment/comment-mention.ts @@ -0,0 +1,111 @@ +import { logger } from '@server/helpers/logger' +import { toSafeHtml } from '@server/helpers/markdown' +import { WEBSERVER } from '@server/initializers/constants' +import { AccountBlocklistModel } from '@server/models/account/account-blocklist' +import { getServerActor } from '@server/models/application/application' +import { ServerBlocklistModel } from '@server/models/server/server-blocklist' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { + MCommentOwnerVideo, + MUserDefault, + MUserNotifSettingAccount, + MUserWithNotificationSetting, + UserNotificationModelForApi +} from '@server/types/models' +import { UserNotificationSettingValue, UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common' + +export class CommentMention extends AbstractNotification { + private users: MUserDefault[] + + private serverAccountId: number + + private accountMutedHash: { [ id: number ]: boolean } + private instanceMutedHash: { [ id: number ]: boolean } + + async prepare () { + const extractedUsernames = this.payload.extractMentions() + logger.debug( + 'Extracted %d username from comment %s.', extractedUsernames.length, this.payload.url, + { usernames: extractedUsernames, text: this.payload.text } + ) + + this.users = await UserModel.listByUsernames(extractedUsernames) + + if (this.payload.Video.isOwned()) { + const userException = await UserModel.loadByVideoId(this.payload.videoId) + this.users = this.users.filter(u => u.id !== userException.id) + } + + // Don't notify if I mentioned myself + this.users = this.users.filter(u => u.Account.id !== this.payload.accountId) + + if (this.users.length === 0) return + + this.serverAccountId = (await getServerActor()).Account.id + + const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ]) + + this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, this.payload.accountId) + this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, this.payload.Account.Actor.serverId) + } + + log () { + logger.info('Notifying %d users of new comment %s.', this.users.length, this.payload.url) + } + + getSetting (user: MUserNotifSettingAccount) { + const accountId = user.Account.id + if ( + this.accountMutedHash[accountId] === true || this.instanceMutedHash[accountId] === true || + this.accountMutedHash[this.serverAccountId] === true || this.instanceMutedHash[this.serverAccountId] === true + ) { + return UserNotificationSettingValue.NONE + } + + return user.NotificationSetting.commentMention + } + + getTargetUsers () { + return this.users + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.COMMENT_MENTION, + userId: user.id, + commentId: this.payload.id + }) + notification.Comment = this.payload + + return notification + } + + createEmail (to: string) { + const comment = this.payload + + const accountName = comment.Account.getDisplayName() + const video = comment.Video + const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() + const commentHtml = toSafeHtml(comment.text) + + return { + template: 'video-comment-mention', + to, + subject: 'Mention on video ' + video.name, + locals: { + comment, + commentHtml, + video, + videoUrl, + accountName, + action: { + text: 'View comment', + url: commentUrl + } + } + } + } +} diff --git a/server/lib/notifier/shared/comment/index.ts b/server/lib/notifier/shared/comment/index.ts new file mode 100644 index 000000000..ae01a9646 --- /dev/null +++ b/server/lib/notifier/shared/comment/index.ts @@ -0,0 +1,2 @@ +export * from './comment-mention' +export * from './new-comment-for-video-owner' diff --git a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts new file mode 100644 index 000000000..b76fc15bf --- /dev/null +++ b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts @@ -0,0 +1,76 @@ +import { logger } from '@server/helpers/logger' +import { toSafeHtml } from '@server/helpers/markdown' +import { WEBSERVER } from '@server/initializers/constants' +import { isBlockedByServerOrAccount } from '@server/lib/blocklist' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MCommentOwnerVideo, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class NewCommentForVideoOwner extends AbstractNotification { + private user: MUserDefault + + async prepare () { + this.user = await UserModel.loadByVideoId(this.payload.videoId) + } + + log () { + logger.info('Notifying owner of a video %s of new comment %s.', this.user.username, this.payload.url) + } + + isDisabled () { + if (this.payload.Video.isOwned() === false) return true + + // Not our user or user comments its own video + if (!this.user || this.payload.Account.userId === this.user.id) return true + + return isBlockedByServerOrAccount(this.payload.Account, this.user.Account) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.newCommentOnMyVideo + } + + getTargetUsers () { + if (!this.user) return [] + + return [ this.user ] + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, + userId: user.id, + commentId: this.payload.id + }) + notification.Comment = this.payload + + return notification + } + + createEmail (to: string) { + const video = this.payload.Video + const videoUrl = WEBSERVER.URL + this.payload.Video.getWatchStaticPath() + const commentUrl = WEBSERVER.URL + this.payload.getCommentStaticPath() + const commentHtml = toSafeHtml(this.payload.text) + + return { + template: 'video-comment-new', + to, + subject: 'New comment on your video ' + video.name, + locals: { + accountName: this.payload.Account.getDisplayName(), + accountUrl: this.payload.Account.Actor.url, + comment: this.payload, + commentHtml, + video, + videoUrl, + action: { + text: 'View comment', + url: commentUrl + } + } + } + } +} diff --git a/server/lib/notifier/shared/common/abstract-notification.ts b/server/lib/notifier/shared/common/abstract-notification.ts new file mode 100644 index 000000000..53e2e02d5 --- /dev/null +++ b/server/lib/notifier/shared/common/abstract-notification.ts @@ -0,0 +1,23 @@ +import { MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { EmailPayload, UserNotificationSettingValue } from '@shared/models' + +export abstract class AbstractNotification { + + constructor (protected readonly payload: T) { + + } + + abstract prepare (): Promise + abstract log (): void + + abstract getSetting (user: U): UserNotificationSettingValue + abstract getTargetUsers (): U[] + + abstract createNotification (user: U): Promise + abstract createEmail (to: string): EmailPayload | Promise + + isDisabled (): boolean | Promise { + return false + } + +} diff --git a/server/lib/notifier/shared/common/index.ts b/server/lib/notifier/shared/common/index.ts new file mode 100644 index 000000000..0b2570278 --- /dev/null +++ b/server/lib/notifier/shared/common/index.ts @@ -0,0 +1 @@ +export * from './abstract-notification' diff --git a/server/lib/notifier/shared/follow/auto-follow-for-instance.ts b/server/lib/notifier/shared/follow/auto-follow-for-instance.ts new file mode 100644 index 000000000..16cc62984 --- /dev/null +++ b/server/lib/notifier/shared/follow/auto-follow-for-instance.ts @@ -0,0 +1,51 @@ +import { logger } from '@server/helpers/logger' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType, UserRight } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class AutoFollowForInstance extends AbstractNotification { + private admins: MUserDefault[] + + async prepare () { + this.admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) + } + + log () { + logger.info('Notifying %d administrators of auto instance following: %s.', this.admins.length, this.actorFollow.ActorFollowing.url) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.autoInstanceFollowing + } + + getTargetUsers () { + return this.admins + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.AUTO_INSTANCE_FOLLOWING, + userId: user.id, + actorFollowId: this.actorFollow.id + }) + notification.ActorFollow = this.actorFollow + + return notification + } + + async createEmail (to: string) { + const instanceUrl = this.actorFollow.ActorFollowing.url + + return { + to, + subject: 'Auto instance following', + text: `Your instance automatically followed a new instance: ${instanceUrl}.` + } + } + + private get actorFollow () { + return this.payload + } +} diff --git a/server/lib/notifier/shared/follow/follow-for-instance.ts b/server/lib/notifier/shared/follow/follow-for-instance.ts new file mode 100644 index 000000000..9ab269cf1 --- /dev/null +++ b/server/lib/notifier/shared/follow/follow-for-instance.ts @@ -0,0 +1,68 @@ +import { logger } from '@server/helpers/logger' +import { WEBSERVER } from '@server/initializers/constants' +import { isBlockedByServerOrAccount } from '@server/lib/blocklist' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType, UserRight } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class FollowForInstance extends AbstractNotification { + private admins: MUserDefault[] + + async prepare () { + this.admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) + } + + isDisabled () { + const follower = Object.assign(this.actorFollow.ActorFollower.Account, { Actor: this.actorFollow.ActorFollower }) + + return isBlockedByServerOrAccount(follower) + } + + log () { + logger.info('Notifying %d administrators of new instance follower: %s.', this.admins.length, this.actorFollow.ActorFollower.url) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.newInstanceFollower + } + + getTargetUsers () { + return this.admins + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_INSTANCE_FOLLOWER, + userId: user.id, + actorFollowId: this.actorFollow.id + }) + notification.ActorFollow = this.actorFollow + + return notification + } + + async createEmail (to: string) { + const awaitingApproval = this.actorFollow.state === 'pending' + ? ' awaiting manual approval.' + : '' + + return { + to, + subject: 'New instance follower', + text: `Your instance has a new follower: ${this.actorFollow.ActorFollower.url}${awaitingApproval}.`, + locals: { + title: 'New instance follower', + action: { + text: 'Review followers', + url: WEBSERVER.URL + '/admin/follows/followers-list' + } + } + } + } + + private get actorFollow () { + return this.payload + } +} diff --git a/server/lib/notifier/shared/follow/follow-for-user.ts b/server/lib/notifier/shared/follow/follow-for-user.ts new file mode 100644 index 000000000..2d0f675a8 --- /dev/null +++ b/server/lib/notifier/shared/follow/follow-for-user.ts @@ -0,0 +1,82 @@ +import { logger } from '@server/helpers/logger' +import { isBlockedByServerOrAccount } from '@server/lib/blocklist' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class FollowForUser extends AbstractNotification { + private followType: 'account' | 'channel' + private user: MUserDefault + + async prepare () { + // Account follows one of our account? + this.followType = 'channel' + this.user = await UserModel.loadByChannelActorId(this.actorFollow.ActorFollowing.id) + + // Account follows one of our channel? + if (!this.user) { + this.user = await UserModel.loadByAccountActorId(this.actorFollow.ActorFollowing.id) + this.followType = 'account' + } + } + + async isDisabled () { + if (this.payload.ActorFollowing.isOwned() === false) return true + + const followerAccount = this.actorFollow.ActorFollower.Account + const followerAccountWithActor = Object.assign(followerAccount, { Actor: this.actorFollow.ActorFollower }) + + return isBlockedByServerOrAccount(followerAccountWithActor, this.user.Account) + } + + log () { + logger.info('Notifying user %s of new follower: %s.', this.user.username, this.actorFollow.ActorFollower.Account.getDisplayName()) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.newFollow + } + + getTargetUsers () { + if (!this.user) return [] + + return [ this.user ] + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_FOLLOW, + userId: user.id, + actorFollowId: this.actorFollow.id + }) + notification.ActorFollow = this.actorFollow + + return notification + } + + async createEmail (to: string) { + const following = this.actorFollow.ActorFollowing + const follower = this.actorFollow.ActorFollower + + const followingName = (following.VideoChannel || following.Account).getDisplayName() + + return { + template: 'follower-on-channel', + to, + subject: `New follower on your channel ${followingName}`, + locals: { + followerName: follower.Account.getDisplayName(), + followerUrl: follower.url, + followingName, + followingUrl: following.url, + followType: this.followType + } + } + } + + private get actorFollow () { + return this.payload + } +} diff --git a/server/lib/notifier/shared/follow/index.ts b/server/lib/notifier/shared/follow/index.ts new file mode 100644 index 000000000..27f5289d9 --- /dev/null +++ b/server/lib/notifier/shared/follow/index.ts @@ -0,0 +1,3 @@ +export * from './auto-follow-for-instance' +export * from './follow-for-instance' +export * from './follow-for-user' diff --git a/server/lib/notifier/shared/index.ts b/server/lib/notifier/shared/index.ts new file mode 100644 index 000000000..cc3ce8c7c --- /dev/null +++ b/server/lib/notifier/shared/index.ts @@ -0,0 +1,7 @@ +export * from './abuse' +export * from './blacklist' +export * from './comment' +export * from './common' +export * from './follow' +export * from './instance' +export * from './video-publication' diff --git a/server/lib/notifier/shared/instance/index.ts b/server/lib/notifier/shared/instance/index.ts new file mode 100644 index 000000000..c3bb22aec --- /dev/null +++ b/server/lib/notifier/shared/instance/index.ts @@ -0,0 +1,3 @@ +export * from './new-peertube-version-for-admins' +export * from './new-plugin-version-for-admins' +export * from './registration-for-moderators' diff --git a/server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts b/server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts new file mode 100644 index 000000000..ab5bfb1ac --- /dev/null +++ b/server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts @@ -0,0 +1,54 @@ +import { logger } from '@server/helpers/logger' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MApplication, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType, UserRight } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export type NewPeerTubeVersionForAdminsPayload = { + application: MApplication + latestVersion: string +} + +export class NewPeerTubeVersionForAdmins extends AbstractNotification { + private admins: MUserDefault[] + + async prepare () { + // Use the debug right to know who is an administrator + this.admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) + } + + log () { + logger.info('Notifying %s admins of new PeerTube version %s.', this.admins.length, this.payload.latestVersion) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.newPeerTubeVersion + } + + getTargetUsers () { + return this.admins + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_PEERTUBE_VERSION, + userId: user.id, + applicationId: this.payload.application.id + }) + notification.Application = this.payload.application + + return notification + } + + async createEmail (to: string) { + return { + to, + template: 'peertube-version-new', + subject: `A new PeerTube version is available: ${this.payload.latestVersion}`, + locals: { + latestVersion: this.payload.latestVersion + } + } + } +} diff --git a/server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts b/server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts new file mode 100644 index 000000000..e5e456a70 --- /dev/null +++ b/server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts @@ -0,0 +1,58 @@ +import { logger } from '@server/helpers/logger' +import { WEBSERVER } from '@server/initializers/constants' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MPlugin, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType, UserRight } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class NewPluginVersionForAdmins extends AbstractNotification { + private admins: MUserDefault[] + + async prepare () { + // Use the debug right to know who is an administrator + this.admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) + } + + log () { + logger.info('Notifying %s admins of new PeerTube version %s.', this.admins.length, this.payload.latestVersion) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.newPluginVersion + } + + getTargetUsers () { + return this.admins + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_PLUGIN_VERSION, + userId: user.id, + pluginId: this.plugin.id + }) + notification.Plugin = this.plugin + + return notification + } + + async createEmail (to: string) { + const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + this.plugin.type + + return { + to, + template: 'plugin-version-new', + subject: `A new plugin/theme version is available: ${this.plugin.name}@${this.plugin.latestVersion}`, + locals: { + pluginName: this.plugin.name, + latestVersion: this.plugin.latestVersion, + pluginUrl + } + } + } + + private get plugin () { + return this.payload + } +} diff --git a/server/lib/notifier/shared/instance/registration-for-moderators.ts b/server/lib/notifier/shared/instance/registration-for-moderators.ts new file mode 100644 index 000000000..4deb5a2cc --- /dev/null +++ b/server/lib/notifier/shared/instance/registration-for-moderators.ts @@ -0,0 +1,49 @@ +import { logger } from '@server/helpers/logger' +import { CONFIG } from '@server/initializers/config' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType, UserRight } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class RegistrationForModerators extends AbstractNotification { + private moderators: MUserDefault[] + + async prepare () { + this.moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS) + } + + log () { + logger.info('Notifying %s moderators of new user registration of %s.', this.moderators.length, this.payload.username) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.newUserRegistration + } + + getTargetUsers () { + return this.moderators + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_USER_REGISTRATION, + userId: user.id, + accountId: this.payload.Account.id + }) + notification.Account = this.payload.Account + + return notification + } + + async createEmail (to: string) { + return { + template: 'user-registered', + to, + subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`, + locals: { + user: this.payload + } + } + } +} diff --git a/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts b/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts new file mode 100644 index 000000000..fd06e080d --- /dev/null +++ b/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts @@ -0,0 +1,57 @@ +import { logger } from '@server/helpers/logger' +import { WEBSERVER } from '@server/initializers/constants' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export abstract class AbstractOwnedVideoPublication extends AbstractNotification { + protected user: MUserDefault + + async prepare () { + this.user = await UserModel.loadByVideoId(this.payload.id) + } + + log () { + logger.info('Notifying user %s of the publication of its video %s.', this.user.username, this.payload.url) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.myVideoPublished + } + + getTargetUsers () { + if (!this.user) return [] + + return [ this.user ] + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.MY_VIDEO_PUBLISHED, + userId: user.id, + videoId: this.payload.id + }) + notification.Video = this.payload + + return notification + } + + createEmail (to: string) { + const videoUrl = WEBSERVER.URL + this.payload.getWatchStaticPath() + + return { + to, + subject: `Your video ${this.payload.name} has been published`, + text: `Your video "${this.payload.name}" has been published.`, + locals: { + title: 'You video is live', + action: { + text: 'View video', + url: videoUrl + } + } + } + } +} diff --git a/server/lib/notifier/shared/video-publication/import-finished-for-owner.ts b/server/lib/notifier/shared/video-publication/import-finished-for-owner.ts new file mode 100644 index 000000000..9f374b6f9 --- /dev/null +++ b/server/lib/notifier/shared/video-publication/import-finished-for-owner.ts @@ -0,0 +1,97 @@ +import { logger } from '@server/helpers/logger' +import { WEBSERVER } from '@server/initializers/constants' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MUserDefault, MUserWithNotificationSetting, MVideoImportVideo, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export type ImportFinishedForOwnerPayload = { + videoImport: MVideoImportVideo + success: boolean +} + +export class ImportFinishedForOwner extends AbstractNotification { + private user: MUserDefault + + async prepare () { + this.user = await UserModel.loadByVideoImportId(this.videoImport.id) + } + + log () { + logger.info('Notifying user %s its video import %s is finished.', this.user.username, this.videoImport.getTargetIdentifier()) + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.myVideoImportFinished + } + + getTargetUsers () { + if (!this.user) return [] + + return [ this.user ] + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: this.payload.success + ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS + : UserNotificationType.MY_VIDEO_IMPORT_ERROR, + + userId: user.id, + videoImportId: this.videoImport.id + }) + notification.VideoImport = this.videoImport + + return notification + } + + createEmail (to: string) { + if (this.payload.success) return this.createSuccessEmail(to) + + return this.createFailEmail(to) + } + + private createSuccessEmail (to: string) { + const videoUrl = WEBSERVER.URL + this.videoImport.Video.getWatchStaticPath() + + return { + to, + subject: `Your video import ${this.videoImport.getTargetIdentifier()} is complete`, + text: `Your video "${this.videoImport.getTargetIdentifier()}" just finished importing.`, + locals: { + title: 'Import complete', + action: { + text: 'View video', + url: videoUrl + } + } + } + } + + private createFailEmail (to: string) { + const importUrl = WEBSERVER.URL + '/my-library/video-imports' + + const text = + `Your video import "${this.videoImport.getTargetIdentifier()}" encountered an error.` + + '\n\n' + + `See your videos import dashboard for more information: ${importUrl}.` + + return { + to, + subject: `Your video import "${this.videoImport.getTargetIdentifier()}" encountered an error`, + text, + locals: { + title: 'Import failed', + action: { + text: 'Review imports', + url: importUrl + } + } + } + } + + private get videoImport () { + return this.payload.videoImport + } +} diff --git a/server/lib/notifier/shared/video-publication/index.ts b/server/lib/notifier/shared/video-publication/index.ts new file mode 100644 index 000000000..940774504 --- /dev/null +++ b/server/lib/notifier/shared/video-publication/index.ts @@ -0,0 +1,5 @@ +export * from './new-video-for-subscribers' +export * from './import-finished-for-owner' +export * from './owned-publication-after-auto-unblacklist' +export * from './owned-publication-after-schedule-update' +export * from './owned-publication-after-transcoding' diff --git a/server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts b/server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts new file mode 100644 index 000000000..4253a0930 --- /dev/null +++ b/server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts @@ -0,0 +1,61 @@ +import { logger } from '@server/helpers/logger' +import { WEBSERVER } from '@server/initializers/constants' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { MUserWithNotificationSetting, MVideoAccountLight, UserNotificationModelForApi } from '@server/types/models' +import { UserNotificationType, VideoPrivacy, VideoState } from '@shared/models' +import { AbstractNotification } from '../common/abstract-notification' + +export class NewVideoForSubscribers extends AbstractNotification { + private users: MUserWithNotificationSetting[] + + async prepare () { + // List all followers that are users + this.users = await UserModel.listUserSubscribersOf(this.payload.VideoChannel.actorId) + } + + log () { + logger.info('Notifying %d users of new video %s.', this.users.length, this.payload.url) + } + + isDisabled () { + return this.payload.privacy !== VideoPrivacy.PUBLIC || this.payload.state !== VideoState.PUBLISHED || this.payload.isBlacklisted() + } + + getSetting (user: MUserWithNotificationSetting) { + return user.NotificationSetting.newVideoFromSubscription + } + + getTargetUsers () { + return this.users + } + + async createNotification (user: MUserWithNotificationSetting) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION, + userId: user.id, + videoId: this.payload.id + }) + notification.Video = this.payload + + return notification + } + + createEmail (to: string) { + const channelName = this.payload.VideoChannel.getDisplayName() + const videoUrl = WEBSERVER.URL + this.payload.getWatchStaticPath() + + return { + to, + subject: channelName + ' just published a new video', + text: `Your subscription ${channelName} just published a new video: "${this.payload.name}".`, + locals: { + title: 'New content ', + action: { + text: 'View video', + url: videoUrl + } + } + } + } +} diff --git a/server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts b/server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts new file mode 100644 index 000000000..27d89a5c7 --- /dev/null +++ b/server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts @@ -0,0 +1,11 @@ + +import { VideoState } from '@shared/models' +import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication' + +export class OwnedPublicationAfterAutoUnblacklist extends AbstractOwnedVideoPublication { + + isDisabled () { + // Don't notify if video is still waiting for transcoding or scheduled update + return !!this.payload.ScheduleVideoUpdate || (this.payload.waitTranscoding && this.payload.state !== VideoState.PUBLISHED) + } +} diff --git a/server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts b/server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts new file mode 100644 index 000000000..2e253b358 --- /dev/null +++ b/server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts @@ -0,0 +1,10 @@ +import { VideoState } from '@shared/models' +import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication' + +export class OwnedPublicationAfterScheduleUpdate extends AbstractOwnedVideoPublication { + + isDisabled () { + // Don't notify if video is still blacklisted or waiting for transcoding + return !!this.payload.VideoBlacklist || (this.payload.waitTranscoding && this.payload.state !== VideoState.PUBLISHED) + } +} diff --git a/server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts b/server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts new file mode 100644 index 000000000..4fab1090f --- /dev/null +++ b/server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts @@ -0,0 +1,9 @@ +import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication' + +export class OwnedPublicationAfterTranscoding extends AbstractOwnedVideoPublication { + + isDisabled () { + // Don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update + return !this.payload.waitTranscoding || !!this.payload.VideoBlacklist || !!this.payload.ScheduleVideoUpdate + } +}