From d26836cd95e981d636006652927773c7943e77ce Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 30 Jul 2021 16:51:27 +0200 Subject: Refactor notifier --- server/lib/notifier/index.ts | 1 + server/lib/notifier/notifier.ts | 259 +++++++++++++++++++++ .../shared/abuse/abstract-new-abuse-message.ts | 67 ++++++ .../abuse/abuse-state-change-for-reporter.ts | 74 ++++++ server/lib/notifier/shared/abuse/index.ts | 4 + .../shared/abuse/new-abuse-for-moderators.ts | 119 ++++++++++ .../abuse/new-abuse-message-for-moderators.ts | 32 +++ .../shared/abuse/new-abuse-message-for-reporter.ts | 36 +++ server/lib/notifier/shared/blacklist/index.ts | 3 + .../blacklist/new-auto-blacklist-for-moderators.ts | 60 +++++ .../shared/blacklist/new-blacklist-for-owner.ts | 58 +++++ .../shared/blacklist/unblacklist-for-owner.ts | 55 +++++ .../lib/notifier/shared/comment/comment-mention.ts | 111 +++++++++ server/lib/notifier/shared/comment/index.ts | 2 + .../shared/comment/new-comment-for-video-owner.ts | 76 ++++++ .../shared/common/abstract-notification.ts | 23 ++ server/lib/notifier/shared/common/index.ts | 1 + .../shared/follow/auto-follow-for-instance.ts | 51 ++++ .../notifier/shared/follow/follow-for-instance.ts | 68 ++++++ .../lib/notifier/shared/follow/follow-for-user.ts | 82 +++++++ server/lib/notifier/shared/follow/index.ts | 3 + server/lib/notifier/shared/index.ts | 7 + server/lib/notifier/shared/instance/index.ts | 3 + .../instance/new-peertube-version-for-admins.ts | 54 +++++ .../instance/new-plugin-version-for-admins.ts | 58 +++++ .../shared/instance/registration-for-moderators.ts | 49 ++++ .../abstract-owned-video-publication.ts | 57 +++++ .../video-publication/import-finished-for-owner.ts | 97 ++++++++ .../lib/notifier/shared/video-publication/index.ts | 5 + .../video-publication/new-video-for-subscribers.ts | 61 +++++ .../owned-publication-after-auto-unblacklist.ts | 11 + .../owned-publication-after-schedule-update.ts | 10 + .../owned-publication-after-transcoding.ts | 9 + 33 files changed, 1606 insertions(+) create mode 100644 server/lib/notifier/index.ts create mode 100644 server/lib/notifier/notifier.ts create mode 100644 server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts create mode 100644 server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts create mode 100644 server/lib/notifier/shared/abuse/index.ts create mode 100644 server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts create mode 100644 server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts create mode 100644 server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts create mode 100644 server/lib/notifier/shared/blacklist/index.ts create mode 100644 server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts create mode 100644 server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts create mode 100644 server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts create mode 100644 server/lib/notifier/shared/comment/comment-mention.ts create mode 100644 server/lib/notifier/shared/comment/index.ts create mode 100644 server/lib/notifier/shared/comment/new-comment-for-video-owner.ts create mode 100644 server/lib/notifier/shared/common/abstract-notification.ts create mode 100644 server/lib/notifier/shared/common/index.ts create mode 100644 server/lib/notifier/shared/follow/auto-follow-for-instance.ts create mode 100644 server/lib/notifier/shared/follow/follow-for-instance.ts create mode 100644 server/lib/notifier/shared/follow/follow-for-user.ts create mode 100644 server/lib/notifier/shared/follow/index.ts create mode 100644 server/lib/notifier/shared/index.ts create mode 100644 server/lib/notifier/shared/instance/index.ts create mode 100644 server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts create mode 100644 server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts create mode 100644 server/lib/notifier/shared/instance/registration-for-moderators.ts create mode 100644 server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts create mode 100644 server/lib/notifier/shared/video-publication/import-finished-for-owner.ts create mode 100644 server/lib/notifier/shared/video-publication/index.ts create mode 100644 server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts create mode 100644 server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts create mode 100644 server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts create mode 100644 server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts (limited to 'server/lib/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 + } +} -- cgit v1.2.3