89f91e0311c6db307f7ff4a1c294c207dfae431f
[oweals/peertube.git] / server / lib / notifier.ts
1 import { getServerActor } from '@server/models/application/application'
2 import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
3 import {
4   MUser,
5   MUserAccount,
6   MUserDefault,
7   MUserNotifSettingAccount,
8   MUserWithNotificationSetting,
9   UserNotificationModelForApi
10 } from '@server/typings/models/user'
11 import { MVideoImportVideo } from '@server/typings/models/video/video-import'
12 import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
13 import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos'
14 import { logger } from '../helpers/logger'
15 import { CONFIG } from '../initializers/config'
16 import { AccountBlocklistModel } from '../models/account/account-blocklist'
17 import { UserModel } from '../models/account/user'
18 import { UserNotificationModel } from '../models/account/user-notification'
19 import { MAccountServer, MActorFollowFull } from '../typings/models'
20 import {
21   MCommentOwnerVideo,
22   MVideoAbuseVideo,
23   MVideoAccountLight,
24   MVideoBlacklistLightVideo,
25   MVideoBlacklistVideo,
26   MVideoFullLight
27 } from '../typings/models/video'
28 import { isBlockedByServerOrAccount } from './blocklist'
29 import { Emailer } from './emailer'
30 import { PeerTubeSocket } from './peertube-socket'
31
32 class Notifier {
33
34   private static instance: Notifier
35
36   private constructor () {
37   }
38
39   notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void {
40     // Only notify on public and published videos which are not blacklisted
41     if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return
42
43     this.notifySubscribersOfNewVideo(video)
44         .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
45   }
46
47   notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void {
48     // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
49     if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
50
51     this.notifyOwnedVideoHasBeenPublished(video)
52         .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
53   }
54
55   notifyOnVideoPublishedAfterScheduledUpdate (video: MVideoFullLight): void {
56     // don't notify if video is still blacklisted or waiting for transcoding
57     if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
58
59     this.notifyOwnedVideoHasBeenPublished(video)
60         .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
61   }
62
63   notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: MVideoFullLight): void {
64     // don't notify if video is still waiting for transcoding or scheduled update
65     if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
66
67     this.notifyOwnedVideoHasBeenPublished(video)
68         .catch(err => {
69           logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })
70         })
71   }
72
73   notifyOnNewComment (comment: MCommentOwnerVideo): void {
74     this.notifyVideoOwnerOfNewComment(comment)
75         .catch(err => logger.error('Cannot notify video owner of new comment %s.', comment.url, { err }))
76
77     this.notifyOfCommentMention(comment)
78         .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
79   }
80
81   notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void {
82     this.notifyModeratorsOfNewVideoAbuse(parameters)
83         .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err }))
84   }
85
86   notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
87     this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist)
88         .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
89   }
90
91   notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
92     this.notifyVideoOwnerOfBlacklist(videoBlacklist)
93         .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
94   }
95
96   notifyOnVideoUnblacklist (video: MVideoFullLight): void {
97     this.notifyVideoOwnerOfUnblacklist(video)
98         .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
99   }
100
101   notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void {
102     this.notifyOwnerVideoImportIsFinished(videoImport, success)
103         .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
104   }
105
106   notifyOnNewUserRegistration (user: MUserDefault): void {
107     this.notifyModeratorsOfNewUserRegistration(user)
108         .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
109   }
110
111   notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
112     this.notifyUserOfNewActorFollow(actorFollow)
113         .catch(err => {
114           logger.error(
115             'Cannot notify owner of channel %s of a new follow by %s.',
116             actorFollow.ActorFollowing.VideoChannel.getDisplayName(),
117             actorFollow.ActorFollower.Account.getDisplayName(),
118             { err }
119           )
120         })
121   }
122
123   notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
124     this.notifyAdminsOfNewInstanceFollow(actorFollow)
125         .catch(err => {
126           logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })
127         })
128   }
129
130   notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void {
131     this.notifyAdminsOfAutoInstanceFollowing(actorFollow)
132         .catch(err => {
133           logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err })
134         })
135   }
136
137   private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
138     // List all followers that are users
139     const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
140
141     logger.info('Notifying %d users of new video %s.', users.length, video.url)
142
143     function settingGetter (user: MUserWithNotificationSetting) {
144       return user.NotificationSetting.newVideoFromSubscription
145     }
146
147     async function notificationCreator (user: MUserWithNotificationSetting) {
148       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
149         type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION,
150         userId: user.id,
151         videoId: video.id
152       })
153       notification.Video = video
154
155       return notification
156     }
157
158     function emailSender (emails: string[]) {
159       return Emailer.Instance.addNewVideoFromSubscriberNotification(emails, video)
160     }
161
162     return this.notify({ users, settingGetter, notificationCreator, emailSender })
163   }
164
165   private async notifyVideoOwnerOfNewComment (comment: MCommentOwnerVideo) {
166     if (comment.Video.isOwned() === false) return
167
168     const user = await UserModel.loadByVideoId(comment.videoId)
169
170     // Not our user or user comments its own video
171     if (!user || comment.Account.userId === user.id) return
172
173     if (await this.isBlockedByServerOrUser(comment.Account, user)) return
174
175     logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
176
177     function settingGetter (user: MUserWithNotificationSetting) {
178       return user.NotificationSetting.newCommentOnMyVideo
179     }
180
181     async function notificationCreator (user: MUserWithNotificationSetting) {
182       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
183         type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO,
184         userId: user.id,
185         commentId: comment.id
186       })
187       notification.Comment = comment
188
189       return notification
190     }
191
192     function emailSender (emails: string[]) {
193       return Emailer.Instance.addNewCommentOnMyVideoNotification(emails, comment)
194     }
195
196     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
197   }
198
199   private async notifyOfCommentMention (comment: MCommentOwnerVideo) {
200     const extractedUsernames = comment.extractMentions()
201     logger.debug(
202       'Extracted %d username from comment %s.', extractedUsernames.length, comment.url,
203       { usernames: extractedUsernames, text: comment.text }
204     )
205
206     let users = await UserModel.listByUsernames(extractedUsernames)
207
208     if (comment.Video.isOwned()) {
209       const userException = await UserModel.loadByVideoId(comment.videoId)
210       users = users.filter(u => u.id !== userException.id)
211     }
212
213     // Don't notify if I mentioned myself
214     users = users.filter(u => u.Account.id !== comment.accountId)
215
216     if (users.length === 0) return
217
218     const serverAccountId = (await getServerActor()).Account.id
219     const sourceAccounts = users.map(u => u.Account.id).concat([ serverAccountId ])
220
221     const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, comment.accountId)
222     const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, comment.Account.Actor.serverId)
223
224     logger.info('Notifying %d users of new comment %s.', users.length, comment.url)
225
226     function settingGetter (user: MUserNotifSettingAccount) {
227       const accountId = user.Account.id
228       if (
229         accountMutedHash[accountId] === true || instanceMutedHash[accountId] === true ||
230         accountMutedHash[serverAccountId] === true || instanceMutedHash[serverAccountId] === true
231       ) {
232         return UserNotificationSettingValue.NONE
233       }
234
235       return user.NotificationSetting.commentMention
236     }
237
238     async function notificationCreator (user: MUserNotifSettingAccount) {
239       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
240         type: UserNotificationType.COMMENT_MENTION,
241         userId: user.id,
242         commentId: comment.id
243       })
244       notification.Comment = comment
245
246       return notification
247     }
248
249     function emailSender (emails: string[]) {
250       return Emailer.Instance.addNewCommentMentionNotification(emails, comment)
251     }
252
253     return this.notify({ users, settingGetter, notificationCreator, emailSender })
254   }
255
256   private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFull) {
257     if (actorFollow.ActorFollowing.isOwned() === false) return
258
259     // Account follows one of our account?
260     let followType: 'account' | 'channel' = 'channel'
261     let user = await UserModel.loadByChannelActorId(actorFollow.ActorFollowing.id)
262
263     // Account follows one of our channel?
264     if (!user) {
265       user = await UserModel.loadByAccountActorId(actorFollow.ActorFollowing.id)
266       followType = 'account'
267     }
268
269     if (!user) return
270
271     const followerAccount = actorFollow.ActorFollower.Account
272     const followerAccountWithActor = Object.assign(followerAccount, { Actor: actorFollow.ActorFollower })
273
274     if (await this.isBlockedByServerOrUser(followerAccountWithActor, user)) return
275
276     logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName())
277
278     function settingGetter (user: MUserWithNotificationSetting) {
279       return user.NotificationSetting.newFollow
280     }
281
282     async function notificationCreator (user: MUserWithNotificationSetting) {
283       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
284         type: UserNotificationType.NEW_FOLLOW,
285         userId: user.id,
286         actorFollowId: actorFollow.id
287       })
288       notification.ActorFollow = actorFollow
289
290       return notification
291     }
292
293     function emailSender (emails: string[]) {
294       return Emailer.Instance.addNewFollowNotification(emails, actorFollow, followType)
295     }
296
297     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
298   }
299
300   private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) {
301     const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
302
303     const follower = Object.assign(actorFollow.ActorFollower.Account, { Actor: actorFollow.ActorFollower })
304     if (await this.isBlockedByServerOrUser(follower)) return
305
306     logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url)
307
308     function settingGetter (user: MUserWithNotificationSetting) {
309       return user.NotificationSetting.newInstanceFollower
310     }
311
312     async function notificationCreator (user: MUserWithNotificationSetting) {
313       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
314         type: UserNotificationType.NEW_INSTANCE_FOLLOWER,
315         userId: user.id,
316         actorFollowId: actorFollow.id
317       })
318       notification.ActorFollow = actorFollow
319
320       return notification
321     }
322
323     function emailSender (emails: string[]) {
324       return Emailer.Instance.addNewInstanceFollowerNotification(emails, actorFollow)
325     }
326
327     return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
328   }
329
330   private async notifyAdminsOfAutoInstanceFollowing (actorFollow: MActorFollowFull) {
331     const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
332
333     logger.info('Notifying %d administrators of auto instance following: %s.', admins.length, actorFollow.ActorFollowing.url)
334
335     function settingGetter (user: MUserWithNotificationSetting) {
336       return user.NotificationSetting.autoInstanceFollowing
337     }
338
339     async function notificationCreator (user: MUserWithNotificationSetting) {
340       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
341         type: UserNotificationType.AUTO_INSTANCE_FOLLOWING,
342         userId: user.id,
343         actorFollowId: actorFollow.id
344       })
345       notification.ActorFollow = actorFollow
346
347       return notification
348     }
349
350     function emailSender (emails: string[]) {
351       return Emailer.Instance.addAutoInstanceFollowingNotification(emails, actorFollow)
352     }
353
354     return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
355   }
356
357   private async notifyModeratorsOfNewVideoAbuse (parameters: {
358     videoAbuse: VideoAbuse
359     videoAbuseInstance: MVideoAbuseVideo
360     reporter: string
361   }) {
362     const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
363     if (moderators.length === 0) return
364
365     logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url)
366
367     function settingGetter (user: MUserWithNotificationSetting) {
368       return user.NotificationSetting.videoAbuseAsModerator
369     }
370
371     async function notificationCreator (user: MUserWithNotificationSetting) {
372       const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
373         type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
374         userId: user.id,
375         videoAbuseId: parameters.videoAbuse.id
376       })
377       notification.VideoAbuse = parameters.videoAbuseInstance
378
379       return notification
380     }
381
382     function emailSender (emails: string[]) {
383       return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters)
384     }
385
386     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
387   }
388
389   private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) {
390     const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
391     if (moderators.length === 0) return
392
393     logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, videoBlacklist.Video.url)
394
395     function settingGetter (user: MUserWithNotificationSetting) {
396       return user.NotificationSetting.videoAutoBlacklistAsModerator
397     }
398
399     async function notificationCreator (user: MUserWithNotificationSetting) {
400       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
401         type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
402         userId: user.id,
403         videoBlacklistId: videoBlacklist.id
404       })
405       notification.VideoBlacklist = videoBlacklist
406
407       return notification
408     }
409
410     function emailSender (emails: string[]) {
411       return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, videoBlacklist)
412     }
413
414     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
415   }
416
417   private async notifyVideoOwnerOfBlacklist (videoBlacklist: MVideoBlacklistVideo) {
418     const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
419     if (!user) return
420
421     logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url)
422
423     function settingGetter (user: MUserWithNotificationSetting) {
424       return user.NotificationSetting.blacklistOnMyVideo
425     }
426
427     async function notificationCreator (user: MUserWithNotificationSetting) {
428       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
429         type: UserNotificationType.BLACKLIST_ON_MY_VIDEO,
430         userId: user.id,
431         videoBlacklistId: videoBlacklist.id
432       })
433       notification.VideoBlacklist = videoBlacklist
434
435       return notification
436     }
437
438     function emailSender (emails: string[]) {
439       return Emailer.Instance.addVideoBlacklistNotification(emails, videoBlacklist)
440     }
441
442     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
443   }
444
445   private async notifyVideoOwnerOfUnblacklist (video: MVideoFullLight) {
446     const user = await UserModel.loadByVideoId(video.id)
447     if (!user) return
448
449     logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url)
450
451     function settingGetter (user: MUserWithNotificationSetting) {
452       return user.NotificationSetting.blacklistOnMyVideo
453     }
454
455     async function notificationCreator (user: MUserWithNotificationSetting) {
456       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
457         type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO,
458         userId: user.id,
459         videoId: video.id
460       })
461       notification.Video = video
462
463       return notification
464     }
465
466     function emailSender (emails: string[]) {
467       return Emailer.Instance.addVideoUnblacklistNotification(emails, video)
468     }
469
470     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
471   }
472
473   private async notifyOwnedVideoHasBeenPublished (video: MVideoFullLight) {
474     const user = await UserModel.loadByVideoId(video.id)
475     if (!user) return
476
477     logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url)
478
479     function settingGetter (user: MUserWithNotificationSetting) {
480       return user.NotificationSetting.myVideoPublished
481     }
482
483     async function notificationCreator (user: MUserWithNotificationSetting) {
484       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
485         type: UserNotificationType.MY_VIDEO_PUBLISHED,
486         userId: user.id,
487         videoId: video.id
488       })
489       notification.Video = video
490
491       return notification
492     }
493
494     function emailSender (emails: string[]) {
495       return Emailer.Instance.myVideoPublishedNotification(emails, video)
496     }
497
498     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
499   }
500
501   private async notifyOwnerVideoImportIsFinished (videoImport: MVideoImportVideo, success: boolean) {
502     const user = await UserModel.loadByVideoImportId(videoImport.id)
503     if (!user) return
504
505     logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier())
506
507     function settingGetter (user: MUserWithNotificationSetting) {
508       return user.NotificationSetting.myVideoImportFinished
509     }
510
511     async function notificationCreator (user: MUserWithNotificationSetting) {
512       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
513         type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR,
514         userId: user.id,
515         videoImportId: videoImport.id
516       })
517       notification.VideoImport = videoImport
518
519       return notification
520     }
521
522     function emailSender (emails: string[]) {
523       return success
524         ? Emailer.Instance.myVideoImportSuccessNotification(emails, videoImport)
525         : Emailer.Instance.myVideoImportErrorNotification(emails, videoImport)
526     }
527
528     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
529   }
530
531   private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserDefault) {
532     const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS)
533     if (moderators.length === 0) return
534
535     logger.info(
536       'Notifying %s moderators of new user registration of %s.',
537       moderators.length, registeredUser.username
538     )
539
540     function settingGetter (user: MUserWithNotificationSetting) {
541       return user.NotificationSetting.newUserRegistration
542     }
543
544     async function notificationCreator (user: MUserWithNotificationSetting) {
545       const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
546         type: UserNotificationType.NEW_USER_REGISTRATION,
547         userId: user.id,
548         accountId: registeredUser.Account.id
549       })
550       notification.Account = registeredUser.Account
551
552       return notification
553     }
554
555     function emailSender (emails: string[]) {
556       return Emailer.Instance.addNewUserRegistrationNotification(emails, registeredUser)
557     }
558
559     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
560   }
561
562   private async notify<T extends MUserWithNotificationSetting> (options: {
563     users: T[]
564     notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
565     emailSender: (emails: string[]) => void
566     settingGetter: (user: T) => UserNotificationSettingValue
567   }) {
568     const emails: string[] = []
569
570     for (const user of options.users) {
571       if (this.isWebNotificationEnabled(options.settingGetter(user))) {
572         const notification = await options.notificationCreator(user)
573
574         PeerTubeSocket.Instance.sendNotification(user.id, notification)
575       }
576
577       if (this.isEmailEnabled(user, options.settingGetter(user))) {
578         emails.push(user.email)
579       }
580     }
581
582     if (emails.length !== 0) {
583       options.emailSender(emails)
584     }
585   }
586
587   private isEmailEnabled (user: MUser, value: UserNotificationSettingValue) {
588     if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false
589
590     return value & UserNotificationSettingValue.EMAIL
591   }
592
593   private isWebNotificationEnabled (value: UserNotificationSettingValue) {
594     return value & UserNotificationSettingValue.WEB
595   }
596
597   private isBlockedByServerOrUser (targetAccount: MAccountServer, user?: MUserAccount) {
598     return isBlockedByServerOrAccount(targetAccount, user?.Account)
599   }
600
601   static get Instance () {
602     return this.instance || (this.instance = new this())
603   }
604 }
605
606 // ---------------------------------------------------------------------------
607
608 export {
609   Notifier
610 }