diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
index a6f707a47..17b3742d6 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
@@ -16,11 +16,11 @@
{
return abuse.save({ transaction: t })
})
- // TODO: Notification
+ if (stateUpdated === true) {
+ AbuseModel.loadFull(abuse.id)
+ .then(abuseFull => Notifier.Instance.notifyOnAbuseStateChange(abuseFull))
+ .catch(err => logger.error('Cannot notify on abuse state change', { err }))
+ }
// Do not send the delete to other instances, we updated OUR copy of this abuse
- return res.type('json').status(204).end()
+ return res.sendStatus(204)
}
async function deleteAbuse (req: express.Request, res: express.Response) {
@@ -147,7 +158,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) {
// Do not send the delete to other instances, we delete OUR copy of this abuse
- return res.type('json').status(204).end()
+ return res.sendStatus(204)
}
async function reportAbuse (req: express.Request, res: express.Response) {
@@ -219,7 +230,9 @@ async function addAbuseMessage (req: express.Request, res: express.Response) {
abuseId: abuse.id
})
- // TODO: Notification
+ AbuseModel.loadFull(abuse.id)
+ .then(abuseFull => Notifier.Instance.notifyOnAbuseMessage(abuseFull, abuseMessage))
+ .catch(err => logger.error('Cannot notify on new abuse message', { err }))
return res.json({
abuseMessage: {
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index 0be51c128..050866960 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -77,7 +77,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
newUserRegistration: body.newUserRegistration,
commentMention: body.commentMention,
newInstanceFollower: body.newInstanceFollower,
- autoInstanceFollowing: body.autoInstanceFollowing
+ autoInstanceFollowing: body.autoInstanceFollowing,
+ abuseNewMessage: body.abuseNewMessage,
+ abuseStateChange: body.abuseStateChange
}
await UserNotificationSettingModel.update(values, query)
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index c6ad03328..9c49aa2f6 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -5,13 +5,13 @@ import { join } from 'path'
import { VideoChannelModel } from '@server/models/video/video-channel'
import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
-import { UserAbuse, EmailPayload } from '@shared/models'
+import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
import { SendEmailOptions } from '../../shared/models/server/emailer.model'
import { isTestInstance, root } from '../helpers/core-utils'
import { bunyanLogger, logger } from '../helpers/logger'
import { CONFIG, isEmailEnabled } from '../initializers/config'
import { WEBSERVER } from '../initializers/constants'
-import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
+import { MAbuseFull, MAbuseMessage, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
import { JobQueue } from './job-queue'
@@ -357,6 +357,55 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
+ addAbuseStateChangeNotification (to: string[], abuse: MAbuseFull) {
+ const text = abuse.state === AbuseState.ACCEPTED
+ ? 'Report #' + abuse.id + ' has been accepted'
+ : 'Report #' + abuse.id + ' has been rejected'
+
+ const action = {
+ text,
+ url: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
+ }
+
+ const emailPayload: EmailPayload = {
+ template: 'abuse-state-change',
+ to,
+ subject: text,
+ locals: {
+ action,
+ abuseId: abuse.id,
+ isAccepted: abuse.state === AbuseState.ACCEPTED
+ }
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
+ addAbuseNewMessageNotification (to: string[], options: { target: 'moderator' | 'reporter', abuse: MAbuseFull, message: MAbuseMessage }) {
+ const { abuse, target, message } = options
+
+ const text = 'New message on abuse #' + abuse.id
+ const action = {
+ text,
+ url: target === 'moderator'
+ ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
+ : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
+ }
+
+ const emailPayload: EmailPayload = {
+ template: 'abuse-new-message',
+ to,
+ subject: text,
+ locals: {
+ abuseUrl: action.url,
+ messageText: message.message,
+ action
+ }
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
diff --git a/server/lib/emails/abuse-new-message/html.pug b/server/lib/emails/abuse-new-message/html.pug
new file mode 100644
index 000000000..a4180aba1
--- /dev/null
+++ b/server/lib/emails/abuse-new-message/html.pug
@@ -0,0 +1,11 @@
+extends ../common/greetings
+include ../common/mixins.pug
+
+block title
+ | New abuse message
+
+block content
+ p
+ | A new message was created on #[a(href=WEBSERVER.URL) abuse ##{abuseId} on #{WEBSERVER.HOST}]
+ blockquote #{messageText}
+ br(style="display: none;")
diff --git a/server/lib/emails/abuse-state-change/html.pug b/server/lib/emails/abuse-state-change/html.pug
new file mode 100644
index 000000000..a94c8521d
--- /dev/null
+++ b/server/lib/emails/abuse-state-change/html.pug
@@ -0,0 +1,9 @@
+extends ../common/greetings
+include ../common/mixins.pug
+
+block title
+ | Abuse state changed
+
+block content
+ p
+ | #[a(href=abuseUrl) Your abuse ##{abuseId} on #{WEBSERVER.HOST}] has been #{isAccepted ? 'accepted' : 'rejected'}
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 8f165d2fd..5c50fcf01 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -1,3 +1,4 @@
+import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
import { getServerActor } from '@server/models/application/application'
import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
import {
@@ -18,7 +19,7 @@ import { CONFIG } from '../initializers/config'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { UserModel } from '../models/account/user'
import { UserNotificationModel } from '../models/account/user-notification'
-import { MAbuseFull, MAccountServer, MActorFollowFull } from '../types/models'
+import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models'
import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
import { isBlockedByServerOrAccount } from './blocklist'
import { Emailer } from './emailer'
@@ -129,6 +130,20 @@ class Notifier {
})
}
+ notifyOnAbuseStateChange (abuse: MAbuseFull): void {
+ this.notifyReporterOfAbuseStateChange(abuse)
+ .catch(err => {
+ logger.error('Cannot notify reporter of abuse %d state change.', abuse.id, { err })
+ })
+ }
+
+ notifyOnAbuseMessage (abuse: MAbuseFull, message: AbuseMessageModel): void {
+ this.notifyOfNewAbuseMessage(abuse, message)
+ .catch(err => {
+ logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })
+ })
+ }
+
private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
// List all followers that are users
const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -359,9 +374,7 @@ class Notifier {
const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
if (moderators.length === 0) return
- const url = abuseInstance.VideoAbuse?.Video?.url ||
- abuseInstance.VideoCommentAbuse?.VideoComment?.url ||
- abuseInstance.FlaggedAccount.Actor.url
+ const url = this.getAbuseUrl(abuseInstance)
logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
@@ -387,6 +400,97 @@ class Notifier {
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
}
+ private async notifyReporterOfAbuseStateChange (abuse: MAbuseFull) {
+ // Only notify our users
+ if (abuse.ReporterAccount.isOwned() !== true) return
+
+ const url = this.getAbuseUrl(abuse)
+
+ logger.info('Notifying reporter of abuse % of state change.', url)
+
+ const reporter = await UserModel.loadByAccountActorId(abuse.ReporterAccount.actorId)
+
+ function settingGetter (user: MUserWithNotificationSetting) {
+ return user.NotificationSetting.abuseStateChange
+ }
+
+ async function notificationCreator (user: MUserWithNotificationSetting) {
+ const notification = await UserNotificationModel.create({
+ type: UserNotificationType.ABUSE_STATE_CHANGE,
+ userId: user.id,
+ abuseId: abuse.id
+ })
+ notification.Abuse = abuse
+
+ return notification
+ }
+
+ function emailSender (emails: string[]) {
+ return Emailer.Instance.addAbuseStateChangeNotification(emails, abuse)
+ }
+
+ return this.notify({ users: [ reporter ], settingGetter, notificationCreator, emailSender })
+ }
+
+ private async notifyOfNewAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage) {
+ const url = this.getAbuseUrl(abuse)
+ logger.info('Notifying reporter and moderators of new abuse message on %s.', url)
+
+ function settingGetter (user: MUserWithNotificationSetting) {
+ return user.NotificationSetting.abuseNewMessage
+ }
+
+ async function notificationCreator (user: MUserWithNotificationSetting) {
+ const notification = await UserNotificationModel.create({
+ type: UserNotificationType.ABUSE_NEW_MESSAGE,
+ userId: user.id,
+ abuseId: abuse.id
+ })
+ notification.Abuse = abuse
+
+ return notification
+ }
+
+ function emailSenderReporter (emails: string[]) {
+ return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message })
+ }
+
+ function emailSenderModerators (emails: string[]) {
+ return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message })
+ }
+
+ async function buildReporterOptions () {
+ // Only notify our users
+ if (abuse.ReporterAccount.isOwned() !== true) return
+
+ const reporter = await UserModel.loadByAccountActorId(abuse.ReporterAccount.actorId)
+ // Don't notify my own message
+ if (reporter.Account.id === message.accountId) return
+
+ return { users: [ reporter ], settingGetter, notificationCreator, emailSender: emailSenderReporter }
+ }
+
+ async function buildModeratorsOptions () {
+ let moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
+ // Don't notify my own message
+ moderators = moderators.filter(m => m.Account.id !== message.accountId)
+
+ if (moderators.length === 0) return
+
+ return { users: moderators, settingGetter, notificationCreator, emailSender: emailSenderModerators }
+ }
+
+ const [ reporterOptions, moderatorsOptions ] = await Promise.all([
+ buildReporterOptions(),
+ buildModeratorsOptions()
+ ])
+
+ return Promise.all([
+ this.notify(reporterOptions),
+ this.notify(moderatorsOptions)
+ ])
+ }
+
private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) {
const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
if (moderators.length === 0) return
@@ -599,6 +703,12 @@ class Notifier {
return isBlockedByServerOrAccount(targetAccount, user?.Account)
}
+ private getAbuseUrl (abuse: MAbuseFull) {
+ return abuse.VideoAbuse?.Video?.url ||
+ abuse.VideoCommentAbuse?.VideoComment?.url ||
+ abuse.FlaggedAccount.Actor.url
+ }
+
static get Instance () {
return this.instance || (this.instance = new this())
}
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 6e7a738ee..aa14f0b54 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -141,6 +141,8 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
commentMention: UserNotificationSettingValue.WEB,
newFollow: UserNotificationSettingValue.WEB,
newInstanceFollower: UserNotificationSettingValue.WEB,
+ abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
autoInstanceFollowing: UserNotificationSettingValue.WEB
}
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts
index 3353e9e41..1b599db62 100644
--- a/server/models/abuse/abuse.ts
+++ b/server/models/abuse/abuse.ts
@@ -32,14 +32,14 @@ import {
UserVideoAbuse
} from '@shared/models'
import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
-import { MAbuse, MAbuseAdminFormattable, MAbuseAP, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models'
+import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models'
import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
import { getSort, throwIfNotValid } from '../utils'
import { ThumbnailModel } from '../video/thumbnail'
-import { VideoModel } from '../video/video'
+import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video'
import { VideoBlacklistModel } from '../video/video-blacklist'
import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel'
-import { VideoCommentModel } from '../video/video-comment'
+import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment'
import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder'
import { VideoAbuseModel } from './video-abuse'
import { VideoCommentAbuseModel } from './video-comment-abuse'
@@ -307,6 +307,52 @@ export class AbuseModel extends Model {
return AbuseModel.findOne(query)
}
+ static loadFull (id: number): Bluebird {
+ const query = {
+ where: {
+ id
+ },
+ include: [
+ {
+ model: AccountModel.scope(AccountScopeNames.SUMMARY),
+ required: false,
+ as: 'ReporterAccount'
+ },
+ {
+ model: AccountModel.scope(AccountScopeNames.SUMMARY),
+ as: 'FlaggedAccount'
+ },
+ {
+ model: VideoAbuseModel,
+ required: false,
+ include: [
+ {
+ model: VideoModel.scope([ VideoScopeNames.WITH_ACCOUNT_DETAILS ])
+ }
+ ]
+ },
+ {
+ model: VideoCommentAbuseModel,
+ required: false,
+ include: [
+ {
+ model: VideoCommentModel.scope([
+ CommentScopeNames.WITH_ACCOUNT
+ ]),
+ include: [
+ {
+ model: VideoModel
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+
+ return AbuseModel.findOne(query)
+ }
+
static async listForAdminApi (parameters: {
start: number
count: number
@@ -455,7 +501,7 @@ export class AbuseModel extends Model {
blacklisted: abuseModel.Video?.isBlacklisted() || false,
thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
- channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel,
+ channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel
}
}
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index d8f3f13da..acc192d53 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -12,12 +12,12 @@ import {
Table,
UpdatedAt
} from 'sequelize-typescript'
+import { MNotificationSettingFormattable } from '@server/types/models'
+import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
+import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
+import { clearCacheByUserId } from '../../lib/oauth-model'
import { throwIfNotValid } from '../utils'
import { UserModel } from './user'
-import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
-import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
-import { clearCacheByUserId } from '../../lib/oauth-model'
-import { MNotificationSettingFormattable } from '@server/types/models'
@Table({
tableName: 'userNotificationSetting',
@@ -138,6 +138,24 @@ export class UserNotificationSettingModel extends Model throwIfNotValid(value, isUserNotificationSettingValid, 'abuseStateChange')
+ )
+ @Column
+ abuseStateChange: UserNotificationSettingValue
+
+ @AllowNull(false)
+ @Default(null)
+ @Is(
+ 'UserNotificationSettingAbuseNewMessage',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseNewMessage')
+ )
+ @Column
+ abuseNewMessage: UserNotificationSettingValue
+
@ForeignKey(() => UserModel)
@Column
userId: number
@@ -175,7 +193,9 @@ export class UserNotificationSettingModel extends Model {
return {
id: abuse.id,
+ state: abuse.state,
video: videoAbuse,
comment: commentAbuse,
account: accountAbuse
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 75b914b8c..1d5c7280d 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -44,7 +44,7 @@ import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIf
import { VideoModel } from './video'
import { VideoChannelModel } from './video-channel'
-enum ScopeNames {
+export enum ScopeNames {
WITH_ACCOUNT = 'WITH_ACCOUNT',
WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API',
WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 883b1d29c..c6384677e 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -173,7 +173,9 @@ describe('Test user notifications API validators', function () {
newFollow: UserNotificationSettingValue.WEB,
newUserRegistration: UserNotificationSettingValue.WEB,
newInstanceFollower: UserNotificationSettingValue.WEB,
- autoInstanceFollowing: UserNotificationSettingValue.WEB
+ autoInstanceFollowing: UserNotificationSettingValue.WEB,
+ abuseNewMessage: UserNotificationSettingValue.WEB,
+ abuseStateChange: UserNotificationSettingValue.WEB
}
it('Should fail with missing fields', async function () {
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts
index 9faaacb91..721a445ab 100644
--- a/server/tests/api/notifications/moderation-notifications.ts
+++ b/server/tests/api/notifications/moderation-notifications.ts
@@ -2,6 +2,7 @@
import 'mocha'
import { v4 as uuidv4 } from 'uuid'
+
import {
addVideoCommentThread,
addVideoToBlacklist,
@@ -21,7 +22,9 @@ import {
unfollow,
updateCustomConfig,
updateCustomSubConfig,
- wait
+ wait,
+ updateAbuse,
+ addAbuseMessage
} from '../../../../shared/extra-utils'
import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
@@ -38,12 +41,15 @@ import {
checkUserRegistered,
checkVideoAutoBlacklistForModerators,
checkVideoIsPublished,
- prepareNotificationsTest
+ prepareNotificationsTest,
+ checkAbuseStateChange,
+ checkNewAbuseMessage
} from '../../../../shared/extra-utils/users/user-notifications'
import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions'
import { CustomConfig } from '../../../../shared/models/server'
import { UserNotification } from '../../../../shared/models/users'
import { VideoPrivacy } from '../../../../shared/models/videos'
+import { AbuseState } from '@shared/models'
describe('Test moderation notifications', function () {
let servers: ServerInfo[] = []
@@ -65,7 +71,7 @@ describe('Test moderation notifications', function () {
adminNotificationsServer2 = res.adminNotificationsServer2
})
- describe('Video abuse for moderators notification', function () {
+ describe('Abuse for moderators notification', function () {
let baseParams: CheckerBaseParams
before(() => {
@@ -169,6 +175,122 @@ describe('Test moderation notifications', function () {
})
})
+ describe('Abuse state change notification', function () {
+ let baseParams: CheckerBaseParams
+ let abuseId: number
+
+ before(async function () {
+ baseParams = {
+ server: servers[0],
+ emails,
+ socketNotifications: userNotifications,
+ token: userAccessToken
+ }
+
+ const name = 'abuse ' + uuidv4()
+ const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
+ const video = resVideo.body.video
+
+ const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason' })
+ abuseId = res.body.abuse.id
+ })
+
+ it('Should send a notification to reporter if the abuse has been accepted', async function () {
+ this.timeout(10000)
+
+ await updateAbuse(servers[0].url, servers[0].accessToken, abuseId, { state: AbuseState.ACCEPTED })
+ await waitJobs(servers)
+
+ await checkAbuseStateChange(baseParams, abuseId, AbuseState.ACCEPTED, 'presence')
+ })
+
+ it('Should send a notification to reporter if the abuse has been rejected', async function () {
+ this.timeout(10000)
+
+ await updateAbuse(servers[0].url, servers[0].accessToken, abuseId, { state: AbuseState.REJECTED })
+ await waitJobs(servers)
+
+ await checkAbuseStateChange(baseParams, abuseId, AbuseState.REJECTED, 'presence')
+ })
+ })
+
+ describe('New abuse message notification', function () {
+ let baseParamsUser: CheckerBaseParams
+ let baseParamsAdmin: CheckerBaseParams
+ let abuseId: number
+ let abuseId2: number
+
+ before(async function () {
+ baseParamsUser = {
+ server: servers[0],
+ emails,
+ socketNotifications: userNotifications,
+ token: userAccessToken
+ }
+
+ baseParamsAdmin = {
+ server: servers[0],
+ emails,
+ socketNotifications: adminNotifications,
+ token: servers[0].accessToken
+ }
+
+ const name = 'abuse ' + uuidv4()
+ const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
+ const video = resVideo.body.video
+
+ {
+ const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason' })
+ abuseId = res.body.abuse.id
+ }
+
+ {
+ const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason 2' })
+ abuseId2 = res.body.abuse.id
+ }
+ })
+
+ it('Should send a notification to reporter on new message', async function () {
+ this.timeout(10000)
+
+ const message = 'my super message to users'
+ await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, message)
+ await waitJobs(servers)
+
+ await checkNewAbuseMessage(baseParamsUser, abuseId, message, 'user_1@example.com', 'presence')
+ })
+
+ it('Should not send a notification to the admin if sent by the admin', async function () {
+ this.timeout(10000)
+
+ const message = 'my super message that should not be sent to the admin'
+ await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, message)
+ await waitJobs(servers)
+
+ await checkNewAbuseMessage(baseParamsAdmin, abuseId, message, 'admin1@example.com', 'absence')
+ })
+
+ it('Should send a notification to moderators', async function () {
+ this.timeout(10000)
+
+ const message = 'my super message to moderators'
+ await addAbuseMessage(servers[0].url, userAccessToken, abuseId2, message)
+ await waitJobs(servers)
+
+ await checkNewAbuseMessage(baseParamsAdmin, abuseId2, message, 'admin1@example.com', 'presence')
+ })
+
+ it('Should not send a notification to reporter if sent by the reporter', async function () {
+ this.timeout(10000)
+
+ const message = 'my super message that should not be sent to reporter'
+ await addAbuseMessage(servers[0].url, userAccessToken, abuseId2, message)
+ await waitJobs(servers)
+
+ await checkNewAbuseMessage(baseParamsUser, abuseId2, message, 'user_1@example.com', 'absence')
+ })
+ })
+
describe('Video blacklist on my video', function () {
let baseParams: CheckerBaseParams
diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts
index d793a720f..5409dfd6b 100644
--- a/server/types/models/moderation/abuse.ts
+++ b/server/types/models/moderation/abuse.ts
@@ -5,6 +5,7 @@ import { AbuseModel } from '../../../models/abuse/abuse'
import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl, MAccount } from '../account'
import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video'
import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
+import { VideoCommentModel } from '@server/models/video/video-comment'
type Use = PickWith
type UseVideoAbuse = PickWith
@@ -34,7 +35,7 @@ export type MVideoAbuseVideoUrl =
export type MVideoAbuseVideoFull =
MVideoAbuse &
- UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles>
+ UseVideoAbuse<'Video', Omit>
export type MVideoAbuseFormattable =
MVideoAbuse &
@@ -49,7 +50,7 @@ export type MCommentAbuseAccount =
export type MCommentAbuseAccountVideo =
MCommentAbuse &
- UseCommentAbuse<'VideoComment', MCommentOwnerVideo>
+ UseCommentAbuse<'VideoComment', MCommentOwner & PickWith>
export type MCommentAbuseUrl =
MCommentAbuse &
@@ -79,14 +80,6 @@ export type MAbuseAccountVideo =
Use<'VideoAbuse', MVideoAbuseVideoFull> &
Use<'ReporterAccount', MAccountDefault>
-export type MAbuseAP =
- MAbuse &
- Pick &
- Use<'ReporterAccount', MAccountUrl> &
- Use<'FlaggedAccount', MAccountUrl> &
- Use<'VideoAbuse', MVideoAbuseVideo> &
- Use<'VideoCommentAbuse', MCommentAbuseAccount>
-
export type MAbuseFull =
MAbuse &
Pick &
@@ -111,3 +104,11 @@ export type MAbuseUserFormattable =
Use<'FlaggedAccount', MAccountFormattable> &
Use<'VideoAbuse', MVideoAbuseFormattable> &
Use<'VideoCommentAbuse', MCommentAbuseFormattable>
+
+export type MAbuseAP =
+ MAbuse &
+ Pick &
+ Use<'ReporterAccount', MAccountUrl> &
+ Use<'FlaggedAccount', MAccountUrl> &
+ Use<'VideoAbuse', MVideoAbuseVideo> &
+ Use<'VideoCommentAbuse', MCommentAbuseAccount>
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts
index f59eb7260..58764a748 100644
--- a/server/types/models/user/user-notification.ts
+++ b/server/types/models/user/user-notification.ts
@@ -56,7 +56,7 @@ export module UserNotificationIncludes {
PickWith>>
export type AbuseInclude =
- Pick &
+ Pick &
PickWith &
PickWith &
PickWith
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts
index 2061e3353..98d222e1d 100644
--- a/shared/extra-utils/users/user-notifications.ts
+++ b/shared/extra-utils/users/user-notifications.ts
@@ -2,6 +2,7 @@
import { expect } from 'chai'
import { inspect } from 'util'
+import { AbuseState } from '@shared/models'
import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
import { MockSmtpServer } from '../miscs/email'
import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
@@ -464,6 +465,62 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
await checkNotification(base, notificationChecker, emailNotificationFinder, type)
}
+async function checkNewAbuseMessage (base: CheckerBaseParams, abuseId: number, message: string, toEmail: string, type: CheckerType) {
+ const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
+
+ function notificationChecker (notification: UserNotification, type: CheckerType) {
+ if (type === 'presence') {
+ expect(notification).to.not.be.undefined
+ expect(notification.type).to.equal(notificationType)
+
+ expect(notification.abuse.id).to.equal(abuseId)
+ } else {
+ expect(notification).to.satisfy((n: UserNotification) => {
+ return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId
+ })
+ }
+ }
+
+ function emailNotificationFinder (email: object) {
+ const text = email['text']
+ const to = email['to'].filter(t => t.address === toEmail)
+
+ return text.indexOf(message) !== -1 && to.length !== 0
+ }
+
+ await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+}
+
+async function checkAbuseStateChange (base: CheckerBaseParams, abuseId: number, state: AbuseState, type: CheckerType) {
+ const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
+
+ function notificationChecker (notification: UserNotification, type: CheckerType) {
+ if (type === 'presence') {
+ expect(notification).to.not.be.undefined
+ expect(notification.type).to.equal(notificationType)
+
+ expect(notification.abuse.id).to.equal(abuseId)
+ expect(notification.abuse.state).to.equal(state)
+ } else {
+ expect(notification).to.satisfy((n: UserNotification) => {
+ return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId
+ })
+ }
+ }
+
+ function emailNotificationFinder (email: object) {
+ const text = email['text']
+
+ const contains = state === AbuseState.ACCEPTED
+ ? ' accepted'
+ : ' rejected'
+
+ return text.indexOf(contains) !== -1
+ }
+
+ await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+}
+
async function checkNewCommentAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
@@ -579,6 +636,8 @@ function getAllNotificationsSettings () {
newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
} as UserNotificationSetting
}
@@ -676,6 +735,8 @@ export {
updateMyNotificationSettings,
checkNewVideoAbuseForModerators,
checkVideoAutoBlacklistForModerators,
+ checkNewAbuseMessage,
+ checkAbuseStateChange,
getUserNotifications,
markAsReadNotifications,
getLastNotification,
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts
index 4e2230a76..c7590fa8a 100644
--- a/shared/models/users/user-notification-setting.model.ts
+++ b/shared/models/users/user-notification-setting.model.ts
@@ -5,16 +5,23 @@ export enum UserNotificationSettingValue {
}
export interface UserNotificationSetting {
- newVideoFromSubscription: UserNotificationSettingValue
- newCommentOnMyVideo: UserNotificationSettingValue
abuseAsModerator: UserNotificationSettingValue
videoAutoBlacklistAsModerator: UserNotificationSettingValue
+ newUserRegistration: UserNotificationSettingValue
+
+ newVideoFromSubscription: UserNotificationSettingValue
+
blacklistOnMyVideo: UserNotificationSettingValue
myVideoPublished: UserNotificationSettingValue
myVideoImportFinished: UserNotificationSettingValue
- newUserRegistration: UserNotificationSettingValue
- newFollow: UserNotificationSettingValue
+
commentMention: UserNotificationSettingValue
+ newCommentOnMyVideo: UserNotificationSettingValue
+
+ newFollow: UserNotificationSettingValue
newInstanceFollower: UserNotificationSettingValue
autoInstanceFollowing: UserNotificationSettingValue
+
+ abuseStateChange: UserNotificationSettingValue
+ abuseNewMessage: UserNotificationSettingValue
}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
index 5f7c33976..e2f2234e4 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -1,4 +1,5 @@
import { FollowState } from '../actors'
+import { AbuseState } from '../moderation'
export enum UserNotificationType {
NEW_VIDEO_FROM_SUBSCRIPTION = 1,
@@ -21,7 +22,11 @@ export enum UserNotificationType {
NEW_INSTANCE_FOLLOWER = 13,
- AUTO_INSTANCE_FOLLOWING = 14
+ AUTO_INSTANCE_FOLLOWING = 14,
+
+ ABUSE_STATE_CHANGE = 15,
+
+ ABUSE_NEW_MESSAGE = 16
}
export interface VideoInfo {
@@ -66,6 +71,7 @@ export interface UserNotification {
abuse?: {
id: number
+ state: AbuseState
video?: VideoInfo