Add new follow, mention and user registered notifs
This commit is contained in:
parent
dc13348070
commit
f7cc67b455
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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' +
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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' ])
|
||||
})
|
||||
})
|
|
@ -1 +1,2 @@
|
|||
import './core-utils'
|
||||
import './comment-model'
|
||||
|
|
|
@ -12,4 +12,7 @@ export interface UserNotificationSetting {
|
|||
blacklistOnMyVideo: UserNotificationSettingValue
|
||||
myVideoPublished: UserNotificationSettingValue
|
||||
myVideoImportFinished: UserNotificationSettingValue
|
||||
newUserRegistration: UserNotificationSettingValue
|
||||
newFollow: UserNotificationSettingValue
|
||||
commentMention: UserNotificationSettingValue
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]: []
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue