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
+ }
+}