import { myBlocklistRouter } from './my-blocklist'
import { myVideosHistoryRouter } from './my-history'
import { myNotificationsRouter } from './my-notifications'
+import { Notifier } from '../../../lib/notifier'
const auditLogger = auditLoggerFactory('users')
await sendVerifyUserEmail(user)
}
+ Notifier.Instance.notifyOnNewUserRegistration(user)
+
return res.type('json').status(204).end()
}
markAsReadUserNotificationsValidator,
updateNotificationSettingsValidator
} from '../../../middlewares/validators/user-notifications'
-import { UserNotificationSetting, UserNotificationSettingValue } from '../../../../shared/models/users'
+import { UserNotificationSetting } from '../../../../shared/models/users'
import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting'
const myNotificationsRouter = express.Router()
async function updateNotificationSettings (req: express.Request, res: express.Response) {
const user: UserModel = res.locals.oauth.token.User
- const body: UserNotificationSetting = req.body
+ const body = req.body
const query = {
where: {
}
}
- await UserNotificationSettingModel.update({
+ const values: UserNotificationSetting = {
newVideoFromSubscription: body.newVideoFromSubscription,
newCommentOnMyVideo: body.newCommentOnMyVideo,
videoAbuseAsModerator: body.videoAbuseAsModerator,
blacklistOnMyVideo: body.blacklistOnMyVideo,
myVideoPublished: body.myVideoPublished,
- myVideoImportFinished: body.myVideoImportFinished
- }, query)
+ myVideoImportFinished: body.myVideoImportFinished,
+ newFollow: body.newFollow,
+ newUserRegistration: body.newUserRegistration,
+ commentMention: body.commentMention,
+ }
+
+ await UserNotificationSettingModel.update(values, query)
return res.status(204).end()
}
validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
}
-const actorNameRegExp = new RegExp('^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_\.]+$')
+const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.]'
+const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`)
function isActorPreferredUsernameValid (preferredUsername: string) {
return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
}
export {
normalizeActor,
+ actorNameAlphabet,
areValidActorHandles,
isActorEndpointsObjectValid,
isActorPublicKeyObjectValid,
--- /dev/null
+// Thanks to https://regex101.com
+function regexpCapture (str: string, regex: RegExp, maxIterations = 100) {
+ let m: RegExpExecArray
+ let i = 0
+ let result: RegExpExecArray[] = []
+
+ // tslint:disable:no-conditional-assignment
+ while ((m = regex.exec(str)) !== null && i < maxIterations) {
+ // This is necessary to avoid infinite loops with zero-width matches
+ if (m.index === regex.lastIndex) {
+ regex.lastIndex++
+ }
+
+ result.push(m)
+ i++
+ }
+
+ return result
+}
+
+export {
+ regexpCapture
+}
"blacklistOnMyVideo" INTEGER NOT NULL DEFAULT NULL,
"myVideoPublished" INTEGER NOT NULL DEFAULT NULL,
"myVideoImportFinished" INTEGER NOT NULL DEFAULT NULL,
+"newUserRegistration" INTEGER NOT NULL DEFAULT NULL,
+"newFollow" INTEGER NOT NULL DEFAULT NULL,
+"commentMention" INTEGER NOT NULL DEFAULT NULL,
"userId" INTEGER REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
{
const query = 'INSERT INTO "userNotificationSetting" ' +
'("newVideoFromSubscription", "newCommentOnMyVideo", "videoAbuseAsModerator", "blacklistOnMyVideo", ' +
- '"myVideoPublished", "myVideoImportFinished", "userId", "createdAt", "updatedAt") ' +
- '(SELECT 2, 2, 4, 4, 2, 2, id, NOW(), NOW() FROM "user")'
+ '"myVideoPublished", "myVideoImportFinished", "newUserRegistration", "newFollow", "commentMention", ' +
+ '"userId", "createdAt", "updatedAt") ' +
+ '(SELECT 2, 2, 4, 4, 2, 2, 2, 2, 2, id, NOW(), NOW() FROM "user")'
await utils.sequelize.query(query)
}
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.')
if (follow.state !== 'accepted') {
follow.set('state', 'accepted')
await follow.save()
+
await addFetchOutboxJob(targetActor)
}
}
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
// ---------------------------------------------------------------------------
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
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)
}
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 {
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()
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` +
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()
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' +
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
// ---------------------------------------------------------------------------
-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.')
}
// 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,
// 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)
}
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 {
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 {
.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)
}
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
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
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) {
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<UserNotificationModel>,
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 => {
// ---------------------------------------------------------------------------
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 })
}
import { AccountModel } from './account'
import { getSort } from '../utils'
import { AccountBlock } from '../../../shared/models/blocklist'
+import { Op } from 'sequelize'
enum ScopeNames {
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
BlockedAccount: AccountModel
static isAccountMutedBy (accountId: number, targetAccountId: number) {
+ return AccountBlocklistModel.isAccountMutedByMulti([ accountId ], targetAccountId)
+ .then(result => result[accountId])
+ }
+
+ static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) {
const query = {
- attributes: [ 'id' ],
+ attributes: [ 'accountId', 'id' ],
where: {
- accountId,
+ accountId: {
+ [Op.any]: accountIds
+ },
targetAccountId
},
raw: true
}
return AccountBlocklistModel.unscoped()
- .findOne(query)
- .then(a => !!a)
+ .findAll(query)
+ .then(rows => {
+ const result: { [accountId: number]: boolean } = {}
+
+ for (const accountId of accountIds) {
+ result[accountId] = !!rows.find(r => r.accountId === accountId)
+ }
+
+ return result
+ })
}
static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
@Column
myVideoImportFinished: UserNotificationSettingValue
+ @AllowNull(false)
+ @Default(null)
+ @Is(
+ 'UserNotificationSettingNewUserRegistration',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration')
+ )
+ @Column
+ newUserRegistration: UserNotificationSettingValue
+
+ @AllowNull(false)
+ @Default(null)
+ @Is(
+ 'UserNotificationSettingNewFollow',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow')
+ )
+ @Column
+ newFollow: UserNotificationSettingValue
+
+ @AllowNull(false)
+ @Default(null)
+ @Is(
+ 'UserNotificationSettingCommentMention',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention')
+ )
+ @Column
+ commentMention: UserNotificationSettingValue
+
@ForeignKey(() => UserModel)
@Column
userId: number
videoAbuseAsModerator: this.videoAbuseAsModerator,
blacklistOnMyVideo: this.blacklistOnMyVideo,
myVideoPublished: this.myVideoPublished,
- myVideoImportFinished: this.myVideoImportFinished
+ myVideoImportFinished: this.myVideoImportFinished,
+ newUserRegistration: this.newUserRegistration,
+ commentMention: this.commentMention,
+ newFollow: this.newFollow
}
}
}
import { VideoAbuseModel } from '../video/video-abuse'
import { VideoBlacklistModel } from '../video/video-blacklist'
import { VideoImportModel } from '../video/video-import'
+import { ActorModel } from '../activitypub/actor'
+import { ActorFollowModel } from '../activitypub/actor-follow'
enum ScopeNames {
WITH_ALL = 'WITH_ALL'
}
}
-function buildChannelInclude () {
+function buildChannelInclude (required: boolean) {
return {
- required: true,
+ required,
attributes: [ 'id', 'name' ],
model: () => VideoChannelModel.unscoped()
}
}
-function buildAccountInclude () {
+function buildAccountInclude (required: boolean) {
return {
- required: true,
+ required,
attributes: [ 'id', 'name' ],
model: () => AccountModel.unscoped()
}
[ScopeNames.WITH_ALL]: {
include: [
Object.assign(buildVideoInclude(false), {
- include: [ buildChannelInclude() ]
+ include: [ buildChannelInclude(true) ]
}),
{
attributes: [ 'id', 'originCommentId' ],
model: () => VideoCommentModel.unscoped(),
required: false,
include: [
- buildAccountInclude(),
+ buildAccountInclude(true),
buildVideoInclude(true)
]
},
model: () => VideoImportModel.unscoped(),
required: false,
include: [ buildVideoInclude(false) ]
+ },
+ {
+ attributes: [ 'id', 'name' ],
+ model: () => AccountModel.unscoped(),
+ required: false,
+ include: [
+ {
+ attributes: [ 'id', 'preferredUsername' ],
+ model: () => ActorModel.unscoped(),
+ required: true
+ }
+ ]
+ },
+ {
+ attributes: [ 'id' ],
+ model: () => ActorFollowModel.unscoped(),
+ required: false,
+ include: [
+ {
+ attributes: [ 'preferredUsername' ],
+ model: () => ActorModel.unscoped(),
+ required: true,
+ as: 'ActorFollower',
+ include: [ buildAccountInclude(true) ]
+ },
+ {
+ attributes: [ 'preferredUsername' ],
+ model: () => ActorModel.unscoped(),
+ required: true,
+ as: 'ActorFollowing',
+ include: [
+ buildChannelInclude(false),
+ buildAccountInclude(false)
+ ]
+ }
+ ]
}
]
}
})
VideoImport: VideoImportModel
+ @ForeignKey(() => AccountModel)
+ @Column
+ accountId: number
+
+ @BelongsTo(() => AccountModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+ Account: AccountModel
+
+ @ForeignKey(() => ActorFollowModel)
+ @Column
+ actorFollowId: number
+
+ @BelongsTo(() => ActorFollowModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+ ActorFollow: ActorFollowModel
+
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
const query: IFindOptions<UserNotificationModel> = {
offset: start,
video: this.formatVideo(this.VideoBlacklist.Video)
} : undefined
+ const account = this.Account ? {
+ id: this.Account.id,
+ displayName: this.Account.getDisplayName(),
+ name: this.Account.Actor.preferredUsername
+ } : undefined
+
+ const actorFollow = this.ActorFollow ? {
+ id: this.ActorFollow.id,
+ follower: {
+ displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
+ name: this.ActorFollow.ActorFollower.preferredUsername
+ },
+ following: {
+ type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
+ displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
+ name: this.ActorFollow.ActorFollowing.preferredUsername
+ }
+ } : undefined
+
return {
id: this.id,
type: this.type,
comment,
videoAbuse,
videoBlacklist,
+ account,
+ actorFollow,
createdAt: this.createdAt.toISOString(),
updatedAt: this.updatedAt.toISOString()
}
return UserModel.unscoped().findAll(query)
}
+ static listByUsernames (usernames: string[]) {
+ const query = {
+ where: {
+ username: usernames
+ }
+ }
+
+ return UserModel.findAll(query)
+ }
+
static loadById (id: number) {
return UserModel.findById(id)
}
return UserModel.findOne(query)
}
+ static loadByChannelActorId (videoChannelActorId: number) {
+ const query = {
+ include: [
+ {
+ required: true,
+ attributes: [ 'id' ],
+ model: AccountModel.unscoped(),
+ include: [
+ {
+ required: true,
+ attributes: [ 'id' ],
+ model: VideoChannelModel.unscoped(),
+ where: {
+ actorId: videoChannelActorId
+ }
+ }
+ ]
+ }
+ ]
+ }
+
+ return UserModel.findOne(query)
+ }
+
+ static loadByAccountActorId (accountActorId: number) {
+ const query = {
+ include: [
+ {
+ required: true,
+ attributes: [ 'id' ],
+ model: AccountModel.unscoped(),
+ where: {
+ actorId: accountActorId
+ }
+ }
+ ]
+ }
+
+ return UserModel.findOne(query)
+ }
+
static getOriginalVideoFileTotalFromUser (user: UserModel) {
// Don't use sequelize because we need to use a sub query
const query = UserModel.generateUserQuotaBaseSQL()
import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
import { VideoComment } from '../../../shared/models/videos/video-comment.model'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { CONSTRAINTS_FIELDS } from '../../initializers'
+import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { sendDeleteVideoComment } from '../../lib/activitypub/send'
import { AccountModel } from '../account/account'
import { ActorModel } from '../activitypub/actor'
import { VideoChannelModel } from './video-channel'
import { getServerActor } from '../../helpers/utils'
import { UserModel } from '../account/user'
+import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
+import { regexpCapture } from '../../helpers/regexp'
+import { uniq } from 'lodash'
enum ScopeNames {
WITH_ACCOUNT = 'WITH_ACCOUNT',
id: {
[ Sequelize.Op.in ]: Sequelize.literal('(' +
'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
- 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' +
- 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' +
- 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' +
+ `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
+ 'UNION ' +
+ 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' +
+ 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
+ ') ' +
'SELECT id FROM children' +
')'),
[ Sequelize.Op.ne ]: comment.id
return this.Account.isOwned()
}
+ extractMentions () {
+ if (!this.text) return []
+
+ const localMention = `@(${actorNameAlphabet}+)`
+ const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}`
+
+ const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g')
+ const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
+ const firstMentionRegex = new RegExp('^(?:(?:' + remoteMention + ')|(?:' + localMention + ')) ', 'g')
+ const endMentionRegex = new RegExp(' (?:(?:' + remoteMention + ')|(?:' + localMention + '))$', 'g')
+
+ return uniq(
+ [].concat(
+ regexpCapture(this.text, remoteMentionsRegex)
+ .map(([ , username ]) => username),
+
+ regexpCapture(this.text, localMentionsRegex)
+ .map(([ , username ]) => username),
+
+ regexpCapture(this.text, firstMentionRegex)
+ .map(([ , username1, username2 ]) => username1 || username2),
+
+ regexpCapture(this.text, endMentionRegex)
+ .map(([ , username1, username2 ]) => username1 || username2)
+ )
+ )
+ }
+
toFormattedJSON () {
return {
id: this.id,
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION,
blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION,
- myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION
+ myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION,
+ commentMention: UserNotificationSettingValue.WEB_NOTIFICATION,
+ newFollow: UserNotificationSettingValue.WEB_NOTIFICATION,
+ newUserRegistration: UserNotificationSettingValue.WEB_NOTIFICATION
}
it('Should fail with missing fields', async function () {
flushTests,
getMyUserInformation,
immutableAssign,
+ registerUser,
removeVideoFromBlacklist,
reportVideoAbuse,
+ updateMyUser,
updateVideo,
+ updateVideoChannel,
userLogin,
wait
} from '../../../../shared/utils'
import { waitJobs } from '../../../../shared/utils/server/jobs'
import { getUserNotificationSocket } from '../../../../shared/utils/socket/socket-io'
import {
+ checkCommentMention,
CheckerBaseParams,
+ checkMyVideoImportIsFinished,
+ checkNewActorFollow,
checkNewBlacklistOnMyVideo,
checkNewCommentOnMyVideo,
checkNewVideoAbuseForModerators,
checkNewVideoFromSubscription,
+ checkUserRegistered,
+ checkVideoIsPublished,
getLastNotification,
getUserNotifications,
markAsReadNotifications,
- updateMyNotificationSettings,
- checkVideoIsPublished, checkMyVideoImportIsFinished
+ updateMyNotificationSettings
} from '../../../../shared/utils/users/user-notifications'
import {
User,
UserNotificationType
} from '../../../../shared/models/users'
import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
-import { addUserSubscription } from '../../../../shared/utils/users/user-subscriptions'
+import { addUserSubscription, removeUserSubscription } from '../../../../shared/utils/users/user-subscriptions'
import { VideoPrivacy } from '../../../../shared/models/videos'
-import { getYoutubeVideoUrl, importVideo, getBadVideoUrl } from '../../../../shared/utils/videos/video-imports'
+import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports'
import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
import * as uuidv4 from 'uuid/v4'
import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist'
let channelId: number
const allNotificationSettings: UserNotificationSetting = {
- myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
- myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
- newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
+ newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
- blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
+ blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
+ myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
+ myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
+ commentMention: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
+ newFollow: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
+ newUserRegistration: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
}
before(async function () {
})
})
+ describe('Mention notifications', function () {
+ let baseParams: CheckerBaseParams
+
+ before(async () => {
+ baseParams = {
+ server: servers[0],
+ emails,
+ socketNotifications: userNotifications,
+ token: userAccessToken
+ }
+
+ await updateMyUser({
+ url: servers[0].url,
+ accessToken: servers[0].accessToken,
+ displayName: 'super root name'
+ })
+
+ await updateMyUser({
+ url: servers[1].url,
+ accessToken: servers[1].accessToken,
+ displayName: 'super root 2 name'
+ })
+ })
+
+ it('Should not send a new mention comment notification if I mention the video owner', async function () {
+ this.timeout(10000)
+
+ const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
+ const uuid = resVideo.body.video.uuid
+
+ const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello')
+ const commentId = resComment.body.comment.id
+
+ await wait(500)
+ await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence')
+ })
+
+ it('Should not send a new mention comment notification if I mention myself', async function () {
+ this.timeout(10000)
+
+ const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
+ const uuid = resVideo.body.video.uuid
+
+ const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, uuid, '@user_1 hello')
+ const commentId = resComment.body.comment.id
+
+ await wait(500)
+ await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence')
+ })
+
+ it('Should not send a new mention notification if the account is muted', async function () {
+ this.timeout(10000)
+
+ await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root')
+
+ const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
+ const uuid = resVideo.body.video.uuid
+
+ const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello')
+ const commentId = resComment.body.comment.id
+
+ await wait(500)
+ await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence')
+
+ await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root')
+ })
+
+ it('Should send a new mention notification after local comments', async function () {
+ this.timeout(10000)
+
+ const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
+ const uuid = resVideo.body.video.uuid
+
+ const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, '@user_1 hello 1')
+ const threadId = resThread.body.comment.id
+
+ await wait(500)
+ await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root name', 'presence')
+
+ const resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, 'hello 2 @user_1')
+ const commentId = resComment.body.comment.id
+
+ await wait(500)
+ await checkCommentMention(baseParams, uuid, commentId, threadId, 'super root name', 'presence')
+ })
+
+ it('Should send a new mention notification after remote comments', async function () {
+ this.timeout(20000)
+
+ const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
+ const uuid = resVideo.body.video.uuid
+
+ await waitJobs(servers)
+ const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'hello @user_1@localhost:9001 1')
+ const threadId = resThread.body.comment.id
+
+ await waitJobs(servers)
+ await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root 2 name', 'presence')
+
+ const text = '@user_1@localhost:9001 hello 2 @root@localhost:9001'
+ const resComment = await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, text)
+ const commentId = resComment.body.comment.id
+
+ await waitJobs(servers)
+ await checkCommentMention(baseParams, uuid, commentId, threadId, 'super root 2 name', 'presence')
+ })
+ })
+
describe('Video abuse for moderators notification' , function () {
let baseParams: CheckerBaseParams
})
})
+ describe('New registration', function () {
+ let baseParams: CheckerBaseParams
+
+ before(() => {
+ baseParams = {
+ server: servers[0],
+ emails,
+ socketNotifications: adminNotifications,
+ token: servers[0].accessToken
+ }
+ })
+
+ it('Should send a notification only to moderators when a user registers on the instance', async function () {
+ await registerUser(servers[0].url, 'user_45', 'password')
+
+ await waitJobs(servers)
+
+ await checkUserRegistered(baseParams, 'user_45', 'presence')
+
+ const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
+ await checkUserRegistered(immutableAssign(baseParams, userOverride), 'user_45', 'absence')
+ })
+ })
+
+ describe('New actor follow', function () {
+ let baseParams: CheckerBaseParams
+ let myChannelName = 'super channel name'
+ let myUserName = 'super user name'
+
+ before(async () => {
+ baseParams = {
+ server: servers[0],
+ emails,
+ socketNotifications: userNotifications,
+ token: userAccessToken
+ }
+
+ await updateMyUser({
+ url: servers[0].url,
+ accessToken: servers[0].accessToken,
+ displayName: 'super root name'
+ })
+
+ await updateMyUser({
+ url: servers[0].url,
+ accessToken: userAccessToken,
+ displayName: myUserName
+ })
+
+ await updateMyUser({
+ url: servers[1].url,
+ accessToken: servers[1].accessToken,
+ displayName: 'super root 2 name'
+ })
+
+ await updateVideoChannel(servers[0].url, userAccessToken, 'user_1_channel', { displayName: myChannelName })
+ })
+
+ it('Should notify when a local channel is following one of our channel', async function () {
+ await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
+
+ await waitJobs(servers)
+
+ await checkNewActorFollow(baseParams, 'channel', 'root', 'super root name', myChannelName, 'presence')
+
+ await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
+ })
+
+ it('Should notify when a remote channel is following one of our channel', async function () {
+ await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
+
+ await waitJobs(servers)
+
+ await checkNewActorFollow(baseParams, 'channel', 'root', 'super root 2 name', myChannelName, 'presence')
+
+ await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
+ })
+
+ it('Should notify when a local account is following one of our channel', async function () {
+ await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001')
+
+ await waitJobs(servers)
+
+ await checkNewActorFollow(baseParams, 'account', 'root', 'super root name', myUserName, 'presence')
+ })
+
+ it('Should notify when a remote account is following one of our channel', async function () {
+ await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001')
+
+ await waitJobs(servers)
+
+ await checkNewActorFollow(baseParams, 'account', 'root', 'super root 2 name', myUserName, 'presence')
+ })
+ })
+
describe('Mark as read', function () {
it('Should mark as read some notifications', async function () {
const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3)
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { VideoCommentModel } from '../../models/video/video-comment'
+
+const expect = chai.expect
+
+class CommentMock {
+ text: string
+
+ extractMentions = VideoCommentModel.prototype.extractMentions
+}
+
+describe('Comment model', function () {
+ it('Should correctly extract mentions', async function () {
+ const comment = new CommentMock()
+
+ comment.text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' +
+ 'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end'
+ const result = comment.extractMentions().sort()
+
+ expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ])
+ })
+})
import './core-utils'
+import './comment-model'
blacklistOnMyVideo: UserNotificationSettingValue
myVideoPublished: UserNotificationSettingValue
myVideoImportFinished: UserNotificationSettingValue
+ newUserRegistration: UserNotificationSettingValue
+ newFollow: UserNotificationSettingValue
+ commentMention: UserNotificationSettingValue
}
UNBLACKLIST_ON_MY_VIDEO = 5,
MY_VIDEO_PUBLISHED = 6,
MY_VIDEO_IMPORT_SUCCESS = 7,
- MY_VIDEO_IMPORT_ERROR = 8
+ MY_VIDEO_IMPORT_ERROR = 8,
+ NEW_USER_REGISTRATION = 9,
+ NEW_FOLLOW = 10,
+ COMMENT_MENTION = 11
}
export interface VideoInfo {
video: VideoInfo
}
+ account?: {
+ id: number
+ displayName: string
+ name: string
+ }
+
+ actorFollow?: {
+ id: number
+ follower: {
+ name: string
+ displayName: string
+ }
+ following: {
+ type: 'account' | 'channel'
+ name: string
+ displayName: string
+ }
+ }
+
createdAt: string
updatedAt: string
}
ALL,
MANAGE_USERS,
+
MANAGE_SERVER_FOLLOW,
+
MANAGE_SERVER_REDUNDANCY,
+
MANAGE_VIDEO_ABUSES,
+
MANAGE_JOBS,
+
MANAGE_CONFIGURATION,
MANAGE_ACCOUNTS_BLOCKLIST,
UserRight.UPDATE_ANY_VIDEO,
UserRight.SEE_ALL_VIDEOS,
UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
- UserRight.MANAGE_SERVERS_BLOCKLIST
+ UserRight.MANAGE_SERVERS_BLOCKLIST,
+ UserRight.MANAGE_USERS
],
[UserRole.USER]: []
})
if (checkType === 'presence') {
- expect(socketNotification, 'The socket notification is absent. ' + inspect(base.socketNotifications)).to.not.be.undefined
+ const obj = inspect(base.socketNotifications, { depth: 5 })
+ expect(socketNotification, 'The socket notification is absent. ' + obj).to.not.be.undefined
} else {
- expect(socketNotification, 'The socket notification is present. ' + inspect(socketNotification)).to.be.undefined
+ const obj = inspect(socketNotification, { depth: 5 })
+ expect(socketNotification, 'The socket notification is present. ' + obj).to.be.undefined
}
}
expect(video.id).to.be.a('number')
}
-function checkActor (channel: any) {
- expect(channel.id).to.be.a('number')
- expect(channel.displayName).to.be.a('string')
- expect(channel.displayName).to.not.be.empty
+function checkActor (actor: any) {
+ expect(actor.displayName).to.be.a('string')
+ expect(actor.displayName).to.not.be.empty
}
function checkComment (comment: any, commentId: number, threadId: number) {
await checkNotification(base, notificationChecker, emailFinder, type)
}
+async function checkUserRegistered (base: CheckerBaseParams, username: string, type: CheckerType) {
+ const notificationType = UserNotificationType.NEW_USER_REGISTRATION
+
+ function notificationChecker (notification: UserNotification, type: CheckerType) {
+ if (type === 'presence') {
+ expect(notification).to.not.be.undefined
+ expect(notification.type).to.equal(notificationType)
+
+ checkActor(notification.account)
+ expect(notification.account.name).to.equal(username)
+ } else {
+ expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
+ }
+ }
+
+ function emailFinder (email: object) {
+ const text: string = email[ 'text' ]
+
+ return text.includes(' registered ') && text.includes(username)
+ }
+
+ await checkNotification(base, notificationChecker, emailFinder, type)
+}
+
+async function checkNewActorFollow (
+ base: CheckerBaseParams,
+ followType: 'channel' | 'account',
+ followerName: string,
+ followerDisplayName: string,
+ followingDisplayName: string,
+ type: CheckerType
+) {
+ const notificationType = UserNotificationType.NEW_FOLLOW
+
+ function notificationChecker (notification: UserNotification, type: CheckerType) {
+ if (type === 'presence') {
+ expect(notification).to.not.be.undefined
+ expect(notification.type).to.equal(notificationType)
+
+ checkActor(notification.actorFollow.follower)
+ expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
+ expect(notification.actorFollow.follower.name).to.equal(followerName)
+
+ checkActor(notification.actorFollow.following)
+ expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName)
+ expect(notification.actorFollow.following.type).to.equal(followType)
+ } else {
+ expect(notification).to.satisfy(n => {
+ return n.type !== notificationType ||
+ (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
+ })
+ }
+ }
+
+ function emailFinder (email: object) {
+ const text: string = email[ 'text' ]
+
+ return text.includes('Your ' + followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
+ }
+
+ await checkNotification(base, notificationChecker, emailFinder, type)
+}
+
+async function checkCommentMention (
+ base: CheckerBaseParams,
+ uuid: string,
+ commentId: number,
+ threadId: number,
+ byAccountDisplayName: string,
+ type: CheckerType
+) {
+ const notificationType = UserNotificationType.COMMENT_MENTION
+
+ function notificationChecker (notification: UserNotification, type: CheckerType) {
+ if (type === 'presence') {
+ expect(notification).to.not.be.undefined
+ expect(notification.type).to.equal(notificationType)
+
+ checkComment(notification.comment, commentId, threadId)
+ checkActor(notification.comment.account)
+ expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
+
+ checkVideo(notification.comment.video, undefined, uuid)
+ } else {
+ expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
+ }
+ }
+
+ function emailFinder (email: object) {
+ const text: string = email[ 'text' ]
+
+ return text.includes(' mentioned ') && text.includes(uuid) && text.includes(byAccountDisplayName)
+ }
+
+ await checkNotification(base, notificationChecker, emailFinder, type)
+}
+
let lastEmailCount = 0
async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) {
const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
CheckerType,
checkNotification,
checkMyVideoImportIsFinished,
+ checkUserRegistered,
checkVideoIsPublished,
checkNewVideoFromSubscription,
+ checkNewActorFollow,
checkNewCommentOnMyVideo,
checkNewBlacklistOnMyVideo,
+ checkCommentMention,
updateMyNotificationSettings,
checkNewVideoAbuseForModerators,
getUserNotifications,