From f7cc67b455a12ccae9b0ea16876d166720364357 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 4 Jan 2019 08:56:20 +0100 Subject: Add new follow, mention and user registered notifs --- server/lib/activitypub/process/process-accept.ts | 2 + server/lib/activitypub/process/process-follow.ts | 11 +- server/lib/emailer.ts | 63 ++++++++- .../lib/job-queue/handlers/activitypub-follow.ts | 9 +- server/lib/notifier.ts | 154 ++++++++++++++++++++- server/lib/user.ts | 13 +- 6 files changed, 235 insertions(+), 17 deletions(-) (limited to 'server/lib') diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts index 89bda9c32..605705ad3 100644 --- a/server/lib/activitypub/process/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts @@ -2,6 +2,7 @@ import { ActivityAccept } from '../../../../shared/models/activitypub' import { ActorModel } from '../../../models/activitypub/actor' import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { addFetchOutboxJob } from '../actor' +import { Notifier } from '../../notifier' async function processAcceptActivity (activity: ActivityAccept, targetActor: ActorModel, inboxActor?: ActorModel) { if (inboxActor === undefined) throw new Error('Need to accept on explicit inbox.') @@ -24,6 +25,7 @@ async function processAccept (actor: ActorModel, targetActor: ActorModel) { if (follow.state !== 'accepted') { follow.set('state', 'accepted') await follow.save() + await addFetchOutboxJob(targetActor) } } diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index 24c9085f7..a67892440 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts @@ -5,6 +5,7 @@ import { sequelizeTypescript } from '../../../initializers' import { ActorModel } from '../../../models/activitypub/actor' import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { sendAccept } from '../send' +import { Notifier } from '../../notifier' async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { const activityObject = activity.object @@ -21,13 +22,13 @@ export { // --------------------------------------------------------------------------- async function processFollow (actor: ActorModel, targetActorURL: string) { - await sequelizeTypescript.transaction(async t => { + const { actorFollow, created } = await sequelizeTypescript.transaction(async t => { const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) if (!targetActor) throw new Error('Unknown actor') if (targetActor.isOwned() === false) throw new Error('This is not a local actor.') - const [ actorFollow ] = await ActorFollowModel.findOrCreate({ + const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({ where: { actorId: actor.id, targetActorId: targetActor.id @@ -52,8 +53,12 @@ async function processFollow (actor: ActorModel, targetActorURL: string) { actorFollow.ActorFollowing = targetActor // Target sends to actor he accepted the follow request - return sendAccept(actorFollow) + await sendAccept(actorFollow) + + return { actorFollow, created } }) + if (created) Notifier.Instance.notifyOfNewFollow(actorFollow) + logger.info('Actor %s is followed by actor %s.', targetActorURL, actor.url) } diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 6dc8f2adf..3429498e7 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -11,6 +11,7 @@ import { VideoCommentModel } from '../models/video/video-comment' import { VideoAbuseModel } from '../models/video/video-abuse' import { VideoBlacklistModel } from '../models/video/video-blacklist' import { VideoImportModel } from '../models/video/video-import' +import { ActorFollowModel } from '../models/activitypub/actor-follow' class Emailer { @@ -103,6 +104,25 @@ class Emailer { return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) } + addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') { + const followerName = actorFollow.ActorFollower.Account.getDisplayName() + const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() + + const text = `Hi dear user,\n\n` + + `Your ${followType} ${followingName} has a new subscriber: ${followerName}` + + `\n\n` + + `Cheers,\n` + + `PeerTube.` + + const emailPayload: EmailPayload = { + to, + subject: 'New follower on your channel ' + followingName, + text + } + + return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) + } + myVideoPublishedNotification (to: string[], video: VideoModel) { const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() @@ -185,7 +205,29 @@ class Emailer { return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) } - async addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) { + addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) { + const accountName = comment.Account.getDisplayName() + const video = comment.Video + const commentUrl = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() + + const text = `Hi dear user,\n\n` + + `${accountName} mentioned you on video ${video.name}` + + `\n\n` + + `You can view the comment on ${commentUrl} ` + + `\n\n` + + `Cheers,\n` + + `PeerTube.` + + const emailPayload: EmailPayload = { + to, + subject: 'Mention on video ' + video.name, + text + } + + return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) + } + + addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) { const videoUrl = CONFIG.WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() const text = `Hi,\n\n` + @@ -202,7 +244,22 @@ class Emailer { return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) } - async addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) { + addNewUserRegistrationNotification (to: string[], user: UserModel) { + const text = `Hi,\n\n` + + `User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` + + `Cheers,\n` + + `PeerTube.` + + const emailPayload: EmailPayload = { + to, + subject: '[PeerTube] New user registration on ' + CONFIG.WEBSERVER.HOST, + text + } + + return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) + } + + addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) { const videoName = videoBlacklist.Video.name const videoUrl = CONFIG.WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() @@ -224,7 +281,7 @@ class Emailer { return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) } - async addVideoUnblacklistNotification (to: string[], video: VideoModel) { + addVideoUnblacklistNotification (to: string[], video: VideoModel) { const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() const text = 'Hi,\n\n' + diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts index 36d0f237b..b4d381062 100644 --- a/server/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/lib/job-queue/handlers/activitypub-follow.ts @@ -8,6 +8,7 @@ import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActorModel } from '../../../models/activitypub/actor' +import { Notifier } from '../../notifier' export type ActivitypubFollowPayload = { followerActorId: number @@ -42,7 +43,7 @@ export { // --------------------------------------------------------------------------- -function follow (fromActor: ActorModel, targetActor: ActorModel) { +async function follow (fromActor: ActorModel, targetActor: ActorModel) { if (fromActor.id === targetActor.id) { throw new Error('Follower is the same than target actor.') } @@ -50,7 +51,7 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) { // Same server, direct accept const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending' - return sequelizeTypescript.transaction(async t => { + const actorFollow = await sequelizeTypescript.transaction(async t => { const [ actorFollow ] = await ActorFollowModel.findOrCreate({ where: { actorId: fromActor.id, @@ -68,5 +69,9 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) { // Send a notification to remote server if our follow is not already accepted if (actorFollow.state !== 'accepted') await sendFollow(actorFollow) + + return actorFollow }) + + if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewFollow(actorFollow) } diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 11b0937e9..2c51d7101 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts @@ -13,6 +13,8 @@ import { VideoBlacklistModel } from '../models/video/video-blacklist' import * as Bluebird from 'bluebird' import { VideoImportModel } from '../models/video/video-import' import { AccountBlocklistModel } from '../models/account/account-blocklist' +import { ActorFollowModel } from '../models/activitypub/actor-follow' +import { AccountModel } from '../models/account/account' class Notifier { @@ -38,7 +40,10 @@ class Notifier { notifyOnNewComment (comment: VideoCommentModel): void { this.notifyVideoOwnerOfNewComment(comment) - .catch(err => logger.error('Cannot notify of new comment %s.', comment.url, { err })) + .catch(err => logger.error('Cannot notify video owner of new comment %s.', comment.url, { err })) + + this.notifyOfCommentMention(comment) + .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) } notifyOnNewVideoAbuse (videoAbuse: VideoAbuseModel): void { @@ -61,6 +66,23 @@ class Notifier { .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) } + notifyOnNewUserRegistration (user: UserModel): void { + this.notifyModeratorsOfNewUserRegistration(user) + .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) + } + + notifyOfNewFollow (actorFollow: ActorFollowModel): void { + this.notifyUserOfNewActorFollow(actorFollow) + .catch(err => { + logger.error( + 'Cannot notify owner of channel %s of a new follow by %s.', + actorFollow.ActorFollowing.VideoChannel.getDisplayName(), + actorFollow.ActorFollower.Account.getDisplayName(), + err + ) + }) + } + private async notifySubscribersOfNewVideo (video: VideoModel) { // List all followers that are users const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) @@ -90,6 +112,8 @@ class Notifier { } private async notifyVideoOwnerOfNewComment (comment: VideoCommentModel) { + if (comment.Video.isOwned() === false) return + const user = await UserModel.loadByVideoId(comment.videoId) // Not our user or user comments its own video @@ -122,11 +146,100 @@ class Notifier { return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) } - private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) { - const users = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) + private async notifyOfCommentMention (comment: VideoCommentModel) { + const usernames = comment.extractMentions() + let users = await UserModel.listByUsernames(usernames) + + if (comment.Video.isOwned()) { + const userException = await UserModel.loadByVideoId(comment.videoId) + users = users.filter(u => u.id !== userException.id) + } + + // Don't notify if I mentioned myself + users = users.filter(u => u.Account.id !== comment.accountId) + if (users.length === 0) return - logger.info('Notifying %s user/moderators of new video abuse %s.', users.length, videoAbuse.Video.url) + const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(users.map(u => u.Account.id), comment.accountId) + + logger.info('Notifying %d users of new comment %s.', users.length, comment.url) + + function settingGetter (user: UserModel) { + if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE + + return user.NotificationSetting.commentMention + } + + async function notificationCreator (user: UserModel) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.COMMENT_MENTION, + userId: user.id, + commentId: comment.id + }) + notification.Comment = comment + + return notification + } + + function emailSender (emails: string[]) { + return Emailer.Instance.addNewCommentMentionNotification(emails, comment) + } + + return this.notify({ users, settingGetter, notificationCreator, emailSender }) + } + + private async notifyUserOfNewActorFollow (actorFollow: ActorFollowModel) { + if (actorFollow.ActorFollowing.isOwned() === false) return + + // Account follows one of our account? + let followType: 'account' | 'channel' = 'channel' + let user = await UserModel.loadByChannelActorId(actorFollow.ActorFollowing.id) + + // Account follows one of our channel? + if (!user) { + user = await UserModel.loadByAccountActorId(actorFollow.ActorFollowing.id) + followType = 'account' + } + + if (!user) return + + if (!actorFollow.ActorFollower.Account || !actorFollow.ActorFollower.Account.name) { + actorFollow.ActorFollower.Account = await actorFollow.ActorFollower.$get('Account') as AccountModel + } + const followerAccount = actorFollow.ActorFollower.Account + + const accountMuted = await AccountBlocklistModel.isAccountMutedBy(user.Account.id, followerAccount.id) + if (accountMuted) return + + logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) + + function settingGetter (user: UserModel) { + return user.NotificationSetting.newFollow + } + + async function notificationCreator (user: UserModel) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_FOLLOW, + userId: user.id, + actorFollowId: actorFollow.id + }) + notification.ActorFollow = actorFollow + + return notification + } + + function emailSender (emails: string[]) { + return Emailer.Instance.addNewFollowNotification(emails, actorFollow, followType) + } + + return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) + } + + private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) { + const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) + if (moderators.length === 0) return + + logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) function settingGetter (user: UserModel) { return user.NotificationSetting.videoAbuseAsModerator @@ -147,7 +260,7 @@ class Notifier { return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse) } - return this.notify({ users, settingGetter, notificationCreator, emailSender }) + return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) } private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) { @@ -264,6 +377,37 @@ class Notifier { return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) } + private async notifyModeratorsOfNewUserRegistration (registeredUser: UserModel) { + const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS) + if (moderators.length === 0) return + + logger.info( + 'Notifying %s moderators of new user registration of %s.', + moderators.length, registeredUser.Account.Actor.preferredUsername + ) + + function settingGetter (user: UserModel) { + return user.NotificationSetting.newUserRegistration + } + + async function notificationCreator (user: UserModel) { + const notification = await UserNotificationModel.create({ + type: UserNotificationType.NEW_USER_REGISTRATION, + userId: user.id, + accountId: registeredUser.Account.id + }) + notification.Account = registeredUser.Account + + return notification + } + + function emailSender (emails: string[]) { + return Emailer.Instance.addNewUserRegistrationNotification(emails, registeredUser) + } + + return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) + } + private async notify (options: { users: UserModel[], notificationCreator: (user: UserModel) => Promise, diff --git a/server/lib/user.ts b/server/lib/user.ts index 481571828..9e24e85a0 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -10,7 +10,7 @@ import { VideoChannelModel } from '../models/video/video-channel' import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' import { ActorModel } from '../models/activitypub/actor' import { UserNotificationSettingModel } from '../models/account/user-notification-setting' -import { UserNotificationSettingValue } from '../../shared/models/users' +import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) { const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { @@ -96,13 +96,18 @@ export { // --------------------------------------------------------------------------- function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) { - return UserNotificationSettingModel.create({ + const values: UserNotificationSetting & { userId: number } = { userId: user.id, newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION, newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION, myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION, myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION, videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, - blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL - }, { transaction: t }) + blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, + newUserRegistration: UserNotificationSettingValue.WEB_NOTIFICATION, + commentMention: UserNotificationSettingValue.WEB_NOTIFICATION, + newFollow: UserNotificationSettingValue.WEB_NOTIFICATION + } + + return UserNotificationSettingModel.create(values, { transaction: t }) } -- cgit v1.2.3