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 { myBlocklistRouter } from './my-blocklist'
|
||||||
import { myVideosHistoryRouter } from './my-history'
|
import { myVideosHistoryRouter } from './my-history'
|
||||||
import { myNotificationsRouter } from './my-notifications'
|
import { myNotificationsRouter } from './my-notifications'
|
||||||
|
import { Notifier } from '../../../lib/notifier'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('users')
|
const auditLogger = auditLoggerFactory('users')
|
||||||
|
|
||||||
|
@ -213,6 +214,8 @@ async function registerUser (req: express.Request, res: express.Response) {
|
||||||
await sendVerifyUserEmail(user)
|
await sendVerifyUserEmail(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Notifier.Instance.notifyOnNewUserRegistration(user)
|
||||||
|
|
||||||
return res.type('json').status(204).end()
|
return res.type('json').status(204).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
markAsReadUserNotificationsValidator,
|
markAsReadUserNotificationsValidator,
|
||||||
updateNotificationSettingsValidator
|
updateNotificationSettingsValidator
|
||||||
} from '../../../middlewares/validators/user-notifications'
|
} 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'
|
import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting'
|
||||||
|
|
||||||
const myNotificationsRouter = express.Router()
|
const myNotificationsRouter = express.Router()
|
||||||
|
@ -53,7 +53,7 @@ export {
|
||||||
|
|
||||||
async function updateNotificationSettings (req: express.Request, res: express.Response) {
|
async function updateNotificationSettings (req: express.Request, res: express.Response) {
|
||||||
const user: UserModel = res.locals.oauth.token.User
|
const user: UserModel = res.locals.oauth.token.User
|
||||||
const body: UserNotificationSetting = req.body
|
const body = req.body
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
|
@ -61,14 +61,19 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserNotificationSettingModel.update({
|
const values: UserNotificationSetting = {
|
||||||
newVideoFromSubscription: body.newVideoFromSubscription,
|
newVideoFromSubscription: body.newVideoFromSubscription,
|
||||||
newCommentOnMyVideo: body.newCommentOnMyVideo,
|
newCommentOnMyVideo: body.newCommentOnMyVideo,
|
||||||
videoAbuseAsModerator: body.videoAbuseAsModerator,
|
videoAbuseAsModerator: body.videoAbuseAsModerator,
|
||||||
blacklistOnMyVideo: body.blacklistOnMyVideo,
|
blacklistOnMyVideo: body.blacklistOnMyVideo,
|
||||||
myVideoPublished: body.myVideoPublished,
|
myVideoPublished: body.myVideoPublished,
|
||||||
myVideoImportFinished: body.myVideoImportFinished
|
myVideoImportFinished: body.myVideoImportFinished,
|
||||||
}, query)
|
newFollow: body.newFollow,
|
||||||
|
newUserRegistration: body.newUserRegistration,
|
||||||
|
commentMention: body.commentMention,
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserNotificationSettingModel.update(values, query)
|
||||||
|
|
||||||
return res.status(204).end()
|
return res.status(204).end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,8 @@ function isActorPublicKeyValid (publicKey: string) {
|
||||||
validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
|
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) {
|
function isActorPreferredUsernameValid (preferredUsername: string) {
|
||||||
return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
|
return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
|
||||||
}
|
}
|
||||||
|
@ -127,6 +128,7 @@ function areValidActorHandles (handles: string[]) {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
normalizeActor,
|
normalizeActor,
|
||||||
|
actorNameAlphabet,
|
||||||
areValidActorHandles,
|
areValidActorHandles,
|
||||||
isActorEndpointsObjectValid,
|
isActorEndpointsObjectValid,
|
||||||
isActorPublicKeyObjectValid,
|
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,
|
"blacklistOnMyVideo" INTEGER NOT NULL DEFAULT NULL,
|
||||||
"myVideoPublished" INTEGER NOT NULL DEFAULT NULL,
|
"myVideoPublished" INTEGER NOT NULL DEFAULT NULL,
|
||||||
"myVideoImportFinished" 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,
|
"userId" INTEGER REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
"updatedAt" 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" ' +
|
const query = 'INSERT INTO "userNotificationSetting" ' +
|
||||||
'("newVideoFromSubscription", "newCommentOnMyVideo", "videoAbuseAsModerator", "blacklistOnMyVideo", ' +
|
'("newVideoFromSubscription", "newCommentOnMyVideo", "videoAbuseAsModerator", "blacklistOnMyVideo", ' +
|
||||||
'"myVideoPublished", "myVideoImportFinished", "userId", "createdAt", "updatedAt") ' +
|
'"myVideoPublished", "myVideoImportFinished", "newUserRegistration", "newFollow", "commentMention", ' +
|
||||||
'(SELECT 2, 2, 4, 4, 2, 2, id, NOW(), NOW() FROM "user")'
|
'"userId", "createdAt", "updatedAt") ' +
|
||||||
|
'(SELECT 2, 2, 4, 4, 2, 2, 2, 2, 2, id, NOW(), NOW() FROM "user")'
|
||||||
|
|
||||||
await utils.sequelize.query(query)
|
await utils.sequelize.query(query)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { ActivityAccept } from '../../../../shared/models/activitypub'
|
||||||
import { ActorModel } from '../../../models/activitypub/actor'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||||
import { addFetchOutboxJob } from '../actor'
|
import { addFetchOutboxJob } from '../actor'
|
||||||
|
import { Notifier } from '../../notifier'
|
||||||
|
|
||||||
async function processAcceptActivity (activity: ActivityAccept, targetActor: ActorModel, inboxActor?: ActorModel) {
|
async function processAcceptActivity (activity: ActivityAccept, targetActor: ActorModel, inboxActor?: ActorModel) {
|
||||||
if (inboxActor === undefined) throw new Error('Need to accept on explicit inbox.')
|
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') {
|
if (follow.state !== 'accepted') {
|
||||||
follow.set('state', 'accepted')
|
follow.set('state', 'accepted')
|
||||||
await follow.save()
|
await follow.save()
|
||||||
|
|
||||||
await addFetchOutboxJob(targetActor)
|
await addFetchOutboxJob(targetActor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { sequelizeTypescript } from '../../../initializers'
|
||||||
import { ActorModel } from '../../../models/activitypub/actor'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||||
import { sendAccept } from '../send'
|
import { sendAccept } from '../send'
|
||||||
|
import { Notifier } from '../../notifier'
|
||||||
|
|
||||||
async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) {
|
async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) {
|
||||||
const activityObject = activity.object
|
const activityObject = activity.object
|
||||||
|
@ -21,13 +22,13 @@ export {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function processFollow (actor: ActorModel, targetActorURL: string) {
|
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)
|
const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
|
||||||
|
|
||||||
if (!targetActor) throw new Error('Unknown actor')
|
if (!targetActor) throw new Error('Unknown actor')
|
||||||
if (targetActor.isOwned() === false) throw new Error('This is not a local 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: {
|
where: {
|
||||||
actorId: actor.id,
|
actorId: actor.id,
|
||||||
targetActorId: targetActor.id
|
targetActorId: targetActor.id
|
||||||
|
@ -52,8 +53,12 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
|
||||||
actorFollow.ActorFollowing = targetActor
|
actorFollow.ActorFollowing = targetActor
|
||||||
|
|
||||||
// Target sends to actor he accepted the follow request
|
// 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)
|
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 { VideoAbuseModel } from '../models/video/video-abuse'
|
||||||
import { VideoBlacklistModel } from '../models/video/video-blacklist'
|
import { VideoBlacklistModel } from '../models/video/video-blacklist'
|
||||||
import { VideoImportModel } from '../models/video/video-import'
|
import { VideoImportModel } from '../models/video/video-import'
|
||||||
|
import { ActorFollowModel } from '../models/activitypub/actor-follow'
|
||||||
|
|
||||||
class Emailer {
|
class Emailer {
|
||||||
|
|
||||||
|
@ -103,6 +104,25 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
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) {
|
myVideoPublishedNotification (to: string[], video: VideoModel) {
|
||||||
const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
|
const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
|
||||||
|
|
||||||
|
@ -185,7 +205,29 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
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 videoUrl = CONFIG.WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
|
||||||
|
|
||||||
const text = `Hi,\n\n` +
|
const text = `Hi,\n\n` +
|
||||||
|
@ -202,7 +244,22 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
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 videoName = videoBlacklist.Video.name
|
||||||
const videoUrl = CONFIG.WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
|
const videoUrl = CONFIG.WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
|
||||||
|
|
||||||
|
@ -224,7 +281,7 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
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 videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
|
||||||
|
|
||||||
const text = 'Hi,\n\n' +
|
const text = 'Hi,\n\n' +
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor'
|
||||||
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
||||||
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||||
import { ActorModel } from '../../../models/activitypub/actor'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
|
import { Notifier } from '../../notifier'
|
||||||
|
|
||||||
export type ActivitypubFollowPayload = {
|
export type ActivitypubFollowPayload = {
|
||||||
followerActorId: number
|
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) {
|
if (fromActor.id === targetActor.id) {
|
||||||
throw new Error('Follower is the same than target actor.')
|
throw new Error('Follower is the same than target actor.')
|
||||||
}
|
}
|
||||||
|
@ -50,7 +51,7 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) {
|
||||||
// Same server, direct accept
|
// Same server, direct accept
|
||||||
const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending'
|
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({
|
const [ actorFollow ] = await ActorFollowModel.findOrCreate({
|
||||||
where: {
|
where: {
|
||||||
actorId: fromActor.id,
|
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
|
// Send a notification to remote server if our follow is not already accepted
|
||||||
if (actorFollow.state !== 'accepted') await sendFollow(actorFollow)
|
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 * as Bluebird from 'bluebird'
|
||||||
import { VideoImportModel } from '../models/video/video-import'
|
import { VideoImportModel } from '../models/video/video-import'
|
||||||
import { AccountBlocklistModel } from '../models/account/account-blocklist'
|
import { AccountBlocklistModel } from '../models/account/account-blocklist'
|
||||||
|
import { ActorFollowModel } from '../models/activitypub/actor-follow'
|
||||||
|
import { AccountModel } from '../models/account/account'
|
||||||
|
|
||||||
class Notifier {
|
class Notifier {
|
||||||
|
|
||||||
|
@ -38,7 +40,10 @@ class Notifier {
|
||||||
|
|
||||||
notifyOnNewComment (comment: VideoCommentModel): void {
|
notifyOnNewComment (comment: VideoCommentModel): void {
|
||||||
this.notifyVideoOwnerOfNewComment(comment)
|
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 {
|
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 }))
|
.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) {
|
private async notifySubscribersOfNewVideo (video: VideoModel) {
|
||||||
// List all followers that are users
|
// List all followers that are users
|
||||||
const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
|
const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
|
||||||
|
@ -90,6 +112,8 @@ class Notifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async notifyVideoOwnerOfNewComment (comment: VideoCommentModel) {
|
private async notifyVideoOwnerOfNewComment (comment: VideoCommentModel) {
|
||||||
|
if (comment.Video.isOwned() === false) return
|
||||||
|
|
||||||
const user = await UserModel.loadByVideoId(comment.videoId)
|
const user = await UserModel.loadByVideoId(comment.videoId)
|
||||||
|
|
||||||
// Not our user or user comments its own video
|
// Not our user or user comments its own video
|
||||||
|
@ -122,11 +146,100 @@ class Notifier {
|
||||||
return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
|
return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) {
|
private async notifyOfCommentMention (comment: VideoCommentModel) {
|
||||||
const users = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
|
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
|
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) {
|
function settingGetter (user: UserModel) {
|
||||||
return user.NotificationSetting.videoAbuseAsModerator
|
return user.NotificationSetting.videoAbuseAsModerator
|
||||||
|
@ -147,7 +260,7 @@ class Notifier {
|
||||||
return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse)
|
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) {
|
private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) {
|
||||||
|
@ -264,6 +377,37 @@ class Notifier {
|
||||||
return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
|
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: {
|
private async notify (options: {
|
||||||
users: UserModel[],
|
users: UserModel[],
|
||||||
notificationCreator: (user: UserModel) => Promise<UserNotificationModel>,
|
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 { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
||||||
import { ActorModel } from '../models/activitypub/actor'
|
import { ActorModel } from '../models/activitypub/actor'
|
||||||
import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
|
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) {
|
async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) {
|
||||||
const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
|
const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
|
||||||
|
@ -96,13 +96,18 @@ export {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) {
|
function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) {
|
||||||
return UserNotificationSettingModel.create({
|
const values: UserNotificationSetting & { userId: number } = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION,
|
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
|
newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||||
myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION,
|
myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||||
myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION,
|
myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
|
blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||||
}, { transaction: t })
|
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 { AccountModel } from './account'
|
||||||
import { getSort } from '../utils'
|
import { getSort } from '../utils'
|
||||||
import { AccountBlock } from '../../../shared/models/blocklist'
|
import { AccountBlock } from '../../../shared/models/blocklist'
|
||||||
|
import { Op } from 'sequelize'
|
||||||
|
|
||||||
enum ScopeNames {
|
enum ScopeNames {
|
||||||
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
|
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
|
||||||
|
@ -73,18 +74,33 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
|
||||||
BlockedAccount: AccountModel
|
BlockedAccount: AccountModel
|
||||||
|
|
||||||
static isAccountMutedBy (accountId: number, targetAccountId: number) {
|
static isAccountMutedBy (accountId: number, targetAccountId: number) {
|
||||||
|
return AccountBlocklistModel.isAccountMutedByMulti([ accountId ], targetAccountId)
|
||||||
|
.then(result => result[accountId])
|
||||||
|
}
|
||||||
|
|
||||||
|
static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) {
|
||||||
const query = {
|
const query = {
|
||||||
attributes: [ 'id' ],
|
attributes: [ 'accountId', 'id' ],
|
||||||
where: {
|
where: {
|
||||||
accountId,
|
accountId: {
|
||||||
|
[Op.any]: accountIds
|
||||||
|
},
|
||||||
targetAccountId
|
targetAccountId
|
||||||
},
|
},
|
||||||
raw: true
|
raw: true
|
||||||
}
|
}
|
||||||
|
|
||||||
return AccountBlocklistModel.unscoped()
|
return AccountBlocklistModel.unscoped()
|
||||||
.findOne(query)
|
.findAll(query)
|
||||||
.then(a => !!a)
|
.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) {
|
static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
|
||||||
|
|
|
@ -83,6 +83,33 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
|
||||||
@Column
|
@Column
|
||||||
myVideoImportFinished: UserNotificationSettingValue
|
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)
|
@ForeignKey(() => UserModel)
|
||||||
@Column
|
@Column
|
||||||
userId: number
|
userId: number
|
||||||
|
@ -114,7 +141,10 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
|
||||||
videoAbuseAsModerator: this.videoAbuseAsModerator,
|
videoAbuseAsModerator: this.videoAbuseAsModerator,
|
||||||
blacklistOnMyVideo: this.blacklistOnMyVideo,
|
blacklistOnMyVideo: this.blacklistOnMyVideo,
|
||||||
myVideoPublished: this.myVideoPublished,
|
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 { VideoAbuseModel } from '../video/video-abuse'
|
||||||
import { VideoBlacklistModel } from '../video/video-blacklist'
|
import { VideoBlacklistModel } from '../video/video-blacklist'
|
||||||
import { VideoImportModel } from '../video/video-import'
|
import { VideoImportModel } from '../video/video-import'
|
||||||
|
import { ActorModel } from '../activitypub/actor'
|
||||||
|
import { ActorFollowModel } from '../activitypub/actor-follow'
|
||||||
|
|
||||||
enum ScopeNames {
|
enum ScopeNames {
|
||||||
WITH_ALL = 'WITH_ALL'
|
WITH_ALL = 'WITH_ALL'
|
||||||
|
@ -38,17 +40,17 @@ function buildVideoInclude (required: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildChannelInclude () {
|
function buildChannelInclude (required: boolean) {
|
||||||
return {
|
return {
|
||||||
required: true,
|
required,
|
||||||
attributes: [ 'id', 'name' ],
|
attributes: [ 'id', 'name' ],
|
||||||
model: () => VideoChannelModel.unscoped()
|
model: () => VideoChannelModel.unscoped()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAccountInclude () {
|
function buildAccountInclude (required: boolean) {
|
||||||
return {
|
return {
|
||||||
required: true,
|
required,
|
||||||
attributes: [ 'id', 'name' ],
|
attributes: [ 'id', 'name' ],
|
||||||
model: () => AccountModel.unscoped()
|
model: () => AccountModel.unscoped()
|
||||||
}
|
}
|
||||||
|
@ -58,14 +60,14 @@ function buildAccountInclude () {
|
||||||
[ScopeNames.WITH_ALL]: {
|
[ScopeNames.WITH_ALL]: {
|
||||||
include: [
|
include: [
|
||||||
Object.assign(buildVideoInclude(false), {
|
Object.assign(buildVideoInclude(false), {
|
||||||
include: [ buildChannelInclude() ]
|
include: [ buildChannelInclude(true) ]
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
attributes: [ 'id', 'originCommentId' ],
|
attributes: [ 'id', 'originCommentId' ],
|
||||||
model: () => VideoCommentModel.unscoped(),
|
model: () => VideoCommentModel.unscoped(),
|
||||||
required: false,
|
required: false,
|
||||||
include: [
|
include: [
|
||||||
buildAccountInclude(),
|
buildAccountInclude(true),
|
||||||
buildVideoInclude(true)
|
buildVideoInclude(true)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -86,6 +88,42 @@ function buildAccountInclude () {
|
||||||
model: () => VideoImportModel.unscoped(),
|
model: () => VideoImportModel.unscoped(),
|
||||||
required: false,
|
required: false,
|
||||||
include: [ buildVideoInclude(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
|
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) {
|
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
|
||||||
const query: IFindOptions<UserNotificationModel> = {
|
const query: IFindOptions<UserNotificationModel> = {
|
||||||
offset: start,
|
offset: start,
|
||||||
|
@ -264,6 +326,25 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
|
||||||
video: this.formatVideo(this.VideoBlacklist.Video)
|
video: this.formatVideo(this.VideoBlacklist.Video)
|
||||||
} : undefined
|
} : 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 {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
|
@ -273,6 +354,8 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
|
||||||
comment,
|
comment,
|
||||||
videoAbuse,
|
videoAbuse,
|
||||||
videoBlacklist,
|
videoBlacklist,
|
||||||
|
account,
|
||||||
|
actorFollow,
|
||||||
createdAt: this.createdAt.toISOString(),
|
createdAt: this.createdAt.toISOString(),
|
||||||
updatedAt: this.updatedAt.toISOString()
|
updatedAt: this.updatedAt.toISOString()
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,6 +330,16 @@ export class UserModel extends Model<UserModel> {
|
||||||
return UserModel.unscoped().findAll(query)
|
return UserModel.unscoped().findAll(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static listByUsernames (usernames: string[]) {
|
||||||
|
const query = {
|
||||||
|
where: {
|
||||||
|
username: usernames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserModel.findAll(query)
|
||||||
|
}
|
||||||
|
|
||||||
static loadById (id: number) {
|
static loadById (id: number) {
|
||||||
return UserModel.findById(id)
|
return UserModel.findById(id)
|
||||||
}
|
}
|
||||||
|
@ -424,6 +434,47 @@ export class UserModel extends Model<UserModel> {
|
||||||
return UserModel.findOne(query)
|
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) {
|
static getOriginalVideoFileTotalFromUser (user: UserModel) {
|
||||||
// Don't use sequelize because we need to use a sub query
|
// Don't use sequelize because we need to use a sub query
|
||||||
const query = UserModel.generateUserQuotaBaseSQL()
|
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 { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
|
||||||
import { VideoComment } from '../../../shared/models/videos/video-comment.model'
|
import { VideoComment } from '../../../shared/models/videos/video-comment.model'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
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 { sendDeleteVideoComment } from '../../lib/activitypub/send'
|
||||||
import { AccountModel } from '../account/account'
|
import { AccountModel } from '../account/account'
|
||||||
import { ActorModel } from '../activitypub/actor'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
|
@ -29,6 +29,9 @@ import { VideoModel } from './video'
|
||||||
import { VideoChannelModel } from './video-channel'
|
import { VideoChannelModel } from './video-channel'
|
||||||
import { getServerActor } from '../../helpers/utils'
|
import { getServerActor } from '../../helpers/utils'
|
||||||
import { UserModel } from '../account/user'
|
import { UserModel } from '../account/user'
|
||||||
|
import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
|
||||||
|
import { regexpCapture } from '../../helpers/regexp'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
enum ScopeNames {
|
enum ScopeNames {
|
||||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||||
|
@ -370,9 +373,11 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
id: {
|
id: {
|
||||||
[ Sequelize.Op.in ]: Sequelize.literal('(' +
|
[ Sequelize.Op.in ]: Sequelize.literal('(' +
|
||||||
'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
|
'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
|
||||||
'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' +
|
`SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
|
||||||
'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' +
|
'UNION ' +
|
||||||
'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' +
|
'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' +
|
||||||
|
'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
|
||||||
|
') ' +
|
||||||
'SELECT id FROM children' +
|
'SELECT id FROM children' +
|
||||||
')'),
|
')'),
|
||||||
[ Sequelize.Op.ne ]: comment.id
|
[ Sequelize.Op.ne ]: comment.id
|
||||||
|
@ -460,6 +465,34 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
return this.Account.isOwned()
|
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 () {
|
toFormattedJSON () {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
|
@ -139,7 +139,10 @@ describe('Test user notifications API validators', function () {
|
||||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION,
|
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
|
blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||||
myVideoImportFinished: 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 () {
|
it('Should fail with missing fields', async function () {
|
||||||
|
|
|
@ -10,9 +10,12 @@ import {
|
||||||
flushTests,
|
flushTests,
|
||||||
getMyUserInformation,
|
getMyUserInformation,
|
||||||
immutableAssign,
|
immutableAssign,
|
||||||
|
registerUser,
|
||||||
removeVideoFromBlacklist,
|
removeVideoFromBlacklist,
|
||||||
reportVideoAbuse,
|
reportVideoAbuse,
|
||||||
|
updateMyUser,
|
||||||
updateVideo,
|
updateVideo,
|
||||||
|
updateVideoChannel,
|
||||||
userLogin,
|
userLogin,
|
||||||
wait
|
wait
|
||||||
} from '../../../../shared/utils'
|
} from '../../../../shared/utils'
|
||||||
|
@ -21,16 +24,20 @@ import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
|
||||||
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
||||||
import { getUserNotificationSocket } from '../../../../shared/utils/socket/socket-io'
|
import { getUserNotificationSocket } from '../../../../shared/utils/socket/socket-io'
|
||||||
import {
|
import {
|
||||||
|
checkCommentMention,
|
||||||
CheckerBaseParams,
|
CheckerBaseParams,
|
||||||
|
checkMyVideoImportIsFinished,
|
||||||
|
checkNewActorFollow,
|
||||||
checkNewBlacklistOnMyVideo,
|
checkNewBlacklistOnMyVideo,
|
||||||
checkNewCommentOnMyVideo,
|
checkNewCommentOnMyVideo,
|
||||||
checkNewVideoAbuseForModerators,
|
checkNewVideoAbuseForModerators,
|
||||||
checkNewVideoFromSubscription,
|
checkNewVideoFromSubscription,
|
||||||
|
checkUserRegistered,
|
||||||
|
checkVideoIsPublished,
|
||||||
getLastNotification,
|
getLastNotification,
|
||||||
getUserNotifications,
|
getUserNotifications,
|
||||||
markAsReadNotifications,
|
markAsReadNotifications,
|
||||||
updateMyNotificationSettings,
|
updateMyNotificationSettings
|
||||||
checkVideoIsPublished, checkMyVideoImportIsFinished
|
|
||||||
} from '../../../../shared/utils/users/user-notifications'
|
} from '../../../../shared/utils/users/user-notifications'
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
|
@ -40,9 +47,9 @@ import {
|
||||||
UserNotificationType
|
UserNotificationType
|
||||||
} from '../../../../shared/models/users'
|
} from '../../../../shared/models/users'
|
||||||
import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
|
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 { 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 { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments'
|
||||||
import * as uuidv4 from 'uuid/v4'
|
import * as uuidv4 from 'uuid/v4'
|
||||||
import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist'
|
import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist'
|
||||||
|
@ -81,12 +88,15 @@ describe('Test users notifications', function () {
|
||||||
let channelId: number
|
let channelId: number
|
||||||
|
|
||||||
const allNotificationSettings: UserNotificationSetting = {
|
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,
|
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||||
|
newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||||
videoAbuseAsModerator: 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 () {
|
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 () {
|
describe('Video abuse for moderators notification' , function () {
|
||||||
let baseParams: CheckerBaseParams
|
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 () {
|
describe('Mark as read', function () {
|
||||||
it('Should mark as read some notifications', async function () {
|
it('Should mark as read some notifications', async function () {
|
||||||
const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3)
|
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 './core-utils'
|
||||||
|
import './comment-model'
|
||||||
|
|
|
@ -12,4 +12,7 @@ export interface UserNotificationSetting {
|
||||||
blacklistOnMyVideo: UserNotificationSettingValue
|
blacklistOnMyVideo: UserNotificationSettingValue
|
||||||
myVideoPublished: UserNotificationSettingValue
|
myVideoPublished: UserNotificationSettingValue
|
||||||
myVideoImportFinished: UserNotificationSettingValue
|
myVideoImportFinished: UserNotificationSettingValue
|
||||||
|
newUserRegistration: UserNotificationSettingValue
|
||||||
|
newFollow: UserNotificationSettingValue
|
||||||
|
commentMention: UserNotificationSettingValue
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,10 @@ export enum UserNotificationType {
|
||||||
UNBLACKLIST_ON_MY_VIDEO = 5,
|
UNBLACKLIST_ON_MY_VIDEO = 5,
|
||||||
MY_VIDEO_PUBLISHED = 6,
|
MY_VIDEO_PUBLISHED = 6,
|
||||||
MY_VIDEO_IMPORT_SUCCESS = 7,
|
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 {
|
export interface VideoInfo {
|
||||||
|
@ -55,6 +58,25 @@ export interface UserNotification {
|
||||||
video: VideoInfo
|
video: VideoInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
account?: {
|
||||||
|
id: number
|
||||||
|
displayName: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
actorFollow?: {
|
||||||
|
id: number
|
||||||
|
follower: {
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
}
|
||||||
|
following: {
|
||||||
|
type: 'account' | 'channel'
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,15 @@ export enum UserRight {
|
||||||
ALL,
|
ALL,
|
||||||
|
|
||||||
MANAGE_USERS,
|
MANAGE_USERS,
|
||||||
|
|
||||||
MANAGE_SERVER_FOLLOW,
|
MANAGE_SERVER_FOLLOW,
|
||||||
|
|
||||||
MANAGE_SERVER_REDUNDANCY,
|
MANAGE_SERVER_REDUNDANCY,
|
||||||
|
|
||||||
MANAGE_VIDEO_ABUSES,
|
MANAGE_VIDEO_ABUSES,
|
||||||
|
|
||||||
MANAGE_JOBS,
|
MANAGE_JOBS,
|
||||||
|
|
||||||
MANAGE_CONFIGURATION,
|
MANAGE_CONFIGURATION,
|
||||||
|
|
||||||
MANAGE_ACCOUNTS_BLOCKLIST,
|
MANAGE_ACCOUNTS_BLOCKLIST,
|
||||||
|
|
|
@ -29,7 +29,8 @@ const userRoleRights: { [ id: number ]: UserRight[] } = {
|
||||||
UserRight.UPDATE_ANY_VIDEO,
|
UserRight.UPDATE_ANY_VIDEO,
|
||||||
UserRight.SEE_ALL_VIDEOS,
|
UserRight.SEE_ALL_VIDEOS,
|
||||||
UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
|
UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
|
||||||
UserRight.MANAGE_SERVERS_BLOCKLIST
|
UserRight.MANAGE_SERVERS_BLOCKLIST,
|
||||||
|
UserRight.MANAGE_USERS
|
||||||
],
|
],
|
||||||
|
|
||||||
[UserRole.USER]: []
|
[UserRole.USER]: []
|
||||||
|
|
|
@ -98,9 +98,11 @@ async function checkNotification (
|
||||||
})
|
})
|
||||||
|
|
||||||
if (checkType === 'presence') {
|
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 {
|
} 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')
|
expect(video.id).to.be.a('number')
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkActor (channel: any) {
|
function checkActor (actor: any) {
|
||||||
expect(channel.id).to.be.a('number')
|
expect(actor.displayName).to.be.a('string')
|
||||||
expect(channel.displayName).to.be.a('string')
|
expect(actor.displayName).to.not.be.empty
|
||||||
expect(channel.displayName).to.not.be.empty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkComment (comment: any, commentId: number, threadId: number) {
|
function checkComment (comment: any, commentId: number, threadId: number) {
|
||||||
|
@ -220,6 +221,103 @@ async function checkMyVideoImportIsFinished (
|
||||||
await checkNotification(base, notificationChecker, emailFinder, type)
|
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
|
let lastEmailCount = 0
|
||||||
async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) {
|
async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) {
|
||||||
const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
|
const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
|
||||||
|
@ -312,10 +410,13 @@ export {
|
||||||
CheckerType,
|
CheckerType,
|
||||||
checkNotification,
|
checkNotification,
|
||||||
checkMyVideoImportIsFinished,
|
checkMyVideoImportIsFinished,
|
||||||
|
checkUserRegistered,
|
||||||
checkVideoIsPublished,
|
checkVideoIsPublished,
|
||||||
checkNewVideoFromSubscription,
|
checkNewVideoFromSubscription,
|
||||||
|
checkNewActorFollow,
|
||||||
checkNewCommentOnMyVideo,
|
checkNewCommentOnMyVideo,
|
||||||
checkNewBlacklistOnMyVideo,
|
checkNewBlacklistOnMyVideo,
|
||||||
|
checkCommentMention,
|
||||||
updateMyNotificationSettings,
|
updateMyNotificationSettings,
|
||||||
checkNewVideoAbuseForModerators,
|
checkNewVideoAbuseForModerators,
|
||||||
getUserNotifications,
|
getUserNotifications,
|
||||||
|
|
Loading…
Reference in New Issue