Add new follow, mention and user registered notifs

This commit is contained in:
Chocobozzz 2019-01-04 08:56:20 +01:00 committed by Chocobozzz
parent dc13348070
commit f7cc67b455
25 changed files with 899 additions and 57 deletions

View File

@ -40,6 +40,7 @@ import { deleteUserToken } from '../../../lib/oauth-model'
import { myBlocklistRouter } from './my-blocklist'
import { myVideosHistoryRouter } from './my-history'
import { myNotificationsRouter } from './my-notifications'
import { Notifier } from '../../../lib/notifier'
const auditLogger = auditLoggerFactory('users')
@ -213,6 +214,8 @@ async function registerUser (req: express.Request, res: express.Response) {
await sendVerifyUserEmail(user)
}
Notifier.Instance.notifyOnNewUserRegistration(user)
return res.type('json').status(204).end()
}

View File

@ -18,7 +18,7 @@ import {
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()
@ -53,7 +53,7 @@ export {
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: {
@ -61,14 +61,19 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
}
}
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()
}

View File

@ -27,7 +27,8 @@ function isActorPublicKeyValid (publicKey: string) {
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)
}
@ -127,6 +128,7 @@ function areValidActorHandles (handles: string[]) {
export {
normalizeActor,
actorNameAlphabet,
areValidActorHandles,
isActorEndpointsObjectValid,
isActorPublicKeyObjectValid,

23
server/helpers/regexp.ts Normal file
View File

@ -0,0 +1,23 @@
// 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
}

View File

@ -15,6 +15,9 @@ CREATE TABLE IF NOT EXISTS "userNotificationSetting" ("id" SERIAL,
"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,
@ -26,8 +29,9 @@ PRIMARY KEY ("id"))
{
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)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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' +

View File

@ -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)
}

View File

@ -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<UserNotificationModel>,

View File

@ -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 })
}

View File

@ -2,6 +2,7 @@ import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, Updated
import { AccountModel } from './account'
import { getSort } from '../utils'
import { AccountBlock } from '../../../shared/models/blocklist'
import { Op } from 'sequelize'
enum ScopeNames {
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
@ -73,18 +74,33 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
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) {

View File

@ -83,6 +83,33 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
@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
@ -114,7 +141,10 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
videoAbuseAsModerator: this.videoAbuseAsModerator,
blacklistOnMyVideo: this.blacklistOnMyVideo,
myVideoPublished: this.myVideoPublished,
myVideoImportFinished: this.myVideoImportFinished
myVideoImportFinished: this.myVideoImportFinished,
newUserRegistration: this.newUserRegistration,
commentMention: this.commentMention,
newFollow: this.newFollow
}
}
}

View File

@ -25,6 +25,8 @@ import { AccountModel } from './account'
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'
@ -38,17 +40,17 @@ function buildVideoInclude (required: boolean) {
}
}
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()
}
@ -58,14 +60,14 @@ function buildAccountInclude () {
[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)
]
},
@ -86,6 +88,42 @@ function buildAccountInclude () {
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)
]
}
]
}
]
}
@ -193,6 +231,30 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
})
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,
@ -264,6 +326,25 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
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,
@ -273,6 +354,8 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
comment,
videoAbuse,
videoBlacklist,
account,
actorFollow,
createdAt: this.createdAt.toISOString(),
updatedAt: this.updatedAt.toISOString()
}

View File

@ -330,6 +330,16 @@ export class UserModel extends Model<UserModel> {
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)
}
@ -424,6 +434,47 @@ export class UserModel extends Model<UserModel> {
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()

View File

@ -18,7 +18,7 @@ import { ActivityTagObject } from '../../../shared/models/activitypub/objects/co
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'
@ -29,6 +29,9 @@ import { VideoModel } from './video'
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',
@ -370,9 +373,11 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
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
@ -460,6 +465,34 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
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,

View File

@ -139,7 +139,10 @@ describe('Test user notifications API validators', function () {
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 () {

View File

@ -10,9 +10,12 @@ import {
flushTests,
getMyUserInformation,
immutableAssign,
registerUser,
removeVideoFromBlacklist,
reportVideoAbuse,
updateMyUser,
updateVideo,
updateVideoChannel,
userLogin,
wait
} from '../../../../shared/utils'
@ -21,16 +24,20 @@ import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
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,
@ -40,9 +47,9 @@ import {
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'
@ -81,12 +88,15 @@ describe('Test users notifications', function () {
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 () {
@ -424,6 +434,114 @@ describe('Test users notifications', 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
@ -645,6 +763,101 @@ describe('Test users notifications', function () {
})
})
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)

View File

@ -0,0 +1,25 @@
/* 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' ])
})
})

View File

@ -1 +1,2 @@
import './core-utils'
import './comment-model'

View File

@ -12,4 +12,7 @@ export interface UserNotificationSetting {
blacklistOnMyVideo: UserNotificationSettingValue
myVideoPublished: UserNotificationSettingValue
myVideoImportFinished: UserNotificationSettingValue
newUserRegistration: UserNotificationSettingValue
newFollow: UserNotificationSettingValue
commentMention: UserNotificationSettingValue
}

View File

@ -6,7 +6,10 @@ export enum UserNotificationType {
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 {
@ -55,6 +58,25 @@ export interface UserNotification {
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
}

View File

@ -2,10 +2,15 @@ export enum UserRight {
ALL,
MANAGE_USERS,
MANAGE_SERVER_FOLLOW,
MANAGE_SERVER_REDUNDANCY,
MANAGE_VIDEO_ABUSES,
MANAGE_JOBS,
MANAGE_CONFIGURATION,
MANAGE_ACCOUNTS_BLOCKLIST,

View File

@ -29,7 +29,8 @@ const userRoleRights: { [ id: number ]: UserRight[] } = {
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]: []

View File

@ -98,9 +98,11 @@ async function checkNotification (
})
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
}
}
@ -131,10 +133,9 @@ function checkVideo (video: any, videoName?: string, videoUUID?: string) {
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) {
@ -220,6 +221,103 @@ async function checkMyVideoImportIsFinished (
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
@ -312,10 +410,13 @@ export {
CheckerType,
checkNotification,
checkMyVideoImportIsFinished,
checkUserRegistered,
checkVideoIsPublished,
checkNewVideoFromSubscription,
checkNewActorFollow,
checkNewCommentOnMyVideo,
checkNewBlacklistOnMyVideo,
checkCommentMention,
updateMyNotificationSettings,
checkNewVideoAbuseForModerators,
getUserNotifications,