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