aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/abuse.ts23
-rw-r--r--server/controllers/api/users/my-notifications.ts4
-rw-r--r--server/lib/emailer.ts53
-rw-r--r--server/lib/emails/abuse-new-message/html.pug11
-rw-r--r--server/lib/emails/abuse-state-change/html.pug9
-rw-r--r--server/lib/notifier.ts118
-rw-r--r--server/lib/user.ts2
-rw-r--r--server/models/abuse/abuse.ts54
-rw-r--r--server/models/account/user-notification-setting.ts30
-rw-r--r--server/models/account/user-notification.ts3
-rw-r--r--server/models/video/video-comment.ts2
-rw-r--r--server/tests/api/check-params/user-notifications.ts4
-rw-r--r--server/tests/api/notifications/moderation-notifications.ts128
-rw-r--r--server/types/models/moderation/abuse.ts21
-rw-r--r--server/types/models/user/user-notification.ts2
15 files changed, 426 insertions, 38 deletions
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
index 72e62fc0b..03e6be8c8 100644
--- a/server/controllers/api/abuse.ts
+++ b/server/controllers/api/abuse.ts
@@ -25,6 +25,8 @@ import {
25 setDefaultSort 25 setDefaultSort
26} from '../../middlewares' 26} from '../../middlewares'
27import { AccountModel } from '../../models/account/account' 27import { AccountModel } from '../../models/account/account'
28import { Notifier } from '@server/lib/notifier'
29import { logger } from '@server/helpers/logger'
28 30
29const abuseRouter = express.Router() 31const abuseRouter = express.Router()
30 32
@@ -123,19 +125,28 @@ async function listAbusesForAdmins (req: express.Request, res: express.Response)
123 125
124async function updateAbuse (req: express.Request, res: express.Response) { 126async function updateAbuse (req: express.Request, res: express.Response) {
125 const abuse = res.locals.abuse 127 const abuse = res.locals.abuse
128 let stateUpdated = false
126 129
127 if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment 130 if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment
128 if (req.body.state !== undefined) abuse.state = req.body.state 131
132 if (req.body.state !== undefined) {
133 abuse.state = req.body.state
134 stateUpdated = true
135 }
129 136
130 await sequelizeTypescript.transaction(t => { 137 await sequelizeTypescript.transaction(t => {
131 return abuse.save({ transaction: t }) 138 return abuse.save({ transaction: t })
132 }) 139 })
133 140
134 // TODO: Notification 141 if (stateUpdated === true) {
142 AbuseModel.loadFull(abuse.id)
143 .then(abuseFull => Notifier.Instance.notifyOnAbuseStateChange(abuseFull))
144 .catch(err => logger.error('Cannot notify on abuse state change', { err }))
145 }
135 146
136 // Do not send the delete to other instances, we updated OUR copy of this abuse 147 // Do not send the delete to other instances, we updated OUR copy of this abuse
137 148
138 return res.type('json').status(204).end() 149 return res.sendStatus(204)
139} 150}
140 151
141async function deleteAbuse (req: express.Request, res: express.Response) { 152async function deleteAbuse (req: express.Request, res: express.Response) {
@@ -147,7 +158,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) {
147 158
148 // Do not send the delete to other instances, we delete OUR copy of this abuse 159 // Do not send the delete to other instances, we delete OUR copy of this abuse
149 160
150 return res.type('json').status(204).end() 161 return res.sendStatus(204)
151} 162}
152 163
153async function reportAbuse (req: express.Request, res: express.Response) { 164async function reportAbuse (req: express.Request, res: express.Response) {
@@ -219,7 +230,9 @@ async function addAbuseMessage (req: express.Request, res: express.Response) {
219 abuseId: abuse.id 230 abuseId: abuse.id
220 }) 231 })
221 232
222 // TODO: Notification 233 AbuseModel.loadFull(abuse.id)
234 .then(abuseFull => Notifier.Instance.notifyOnAbuseMessage(abuseFull, abuseMessage))
235 .catch(err => logger.error('Cannot notify on new abuse message', { err }))
223 236
224 return res.json({ 237 return res.json({
225 abuseMessage: { 238 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
77 newUserRegistration: body.newUserRegistration, 77 newUserRegistration: body.newUserRegistration,
78 commentMention: body.commentMention, 78 commentMention: body.commentMention,
79 newInstanceFollower: body.newInstanceFollower, 79 newInstanceFollower: body.newInstanceFollower,
80 autoInstanceFollowing: body.autoInstanceFollowing 80 autoInstanceFollowing: body.autoInstanceFollowing,
81 abuseNewMessage: body.abuseNewMessage,
82 abuseStateChange: body.abuseStateChange
81 } 83 }
82 84
83 await UserNotificationSettingModel.update(values, query) 85 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'
5import { VideoChannelModel } from '@server/models/video/video-channel' 5import { VideoChannelModel } from '@server/models/video/video-channel'
6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' 6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' 7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { UserAbuse, EmailPayload } from '@shared/models' 8import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
9import { SendEmailOptions } from '../../shared/models/server/emailer.model' 9import { SendEmailOptions } from '../../shared/models/server/emailer.model'
10import { isTestInstance, root } from '../helpers/core-utils' 10import { isTestInstance, root } from '../helpers/core-utils'
11import { bunyanLogger, logger } from '../helpers/logger' 11import { bunyanLogger, logger } from '../helpers/logger'
12import { CONFIG, isEmailEnabled } from '../initializers/config' 12import { CONFIG, isEmailEnabled } from '../initializers/config'
13import { WEBSERVER } from '../initializers/constants' 13import { WEBSERVER } from '../initializers/constants'
14import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' 14import { MAbuseFull, MAbuseMessage, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' 15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
16import { JobQueue } from './job-queue' 16import { JobQueue } from './job-queue'
17 17
@@ -357,6 +357,55 @@ class Emailer {
357 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 357 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
358 } 358 }
359 359
360 addAbuseStateChangeNotification (to: string[], abuse: MAbuseFull) {
361 const text = abuse.state === AbuseState.ACCEPTED
362 ? 'Report #' + abuse.id + ' has been accepted'
363 : 'Report #' + abuse.id + ' has been rejected'
364
365 const action = {
366 text,
367 url: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
368 }
369
370 const emailPayload: EmailPayload = {
371 template: 'abuse-state-change',
372 to,
373 subject: text,
374 locals: {
375 action,
376 abuseId: abuse.id,
377 isAccepted: abuse.state === AbuseState.ACCEPTED
378 }
379 }
380
381 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
382 }
383
384 addAbuseNewMessageNotification (to: string[], options: { target: 'moderator' | 'reporter', abuse: MAbuseFull, message: MAbuseMessage }) {
385 const { abuse, target, message } = options
386
387 const text = 'New message on abuse #' + abuse.id
388 const action = {
389 text,
390 url: target === 'moderator'
391 ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
392 : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
393 }
394
395 const emailPayload: EmailPayload = {
396 template: 'abuse-new-message',
397 to,
398 subject: text,
399 locals: {
400 abuseUrl: action.url,
401 messageText: message.message,
402 action
403 }
404 }
405
406 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
407 }
408
360 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 409 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
361 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 410 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
362 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 411 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 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | New abuse message
6
7block content
8 p
9 | A new message was created on #[a(href=WEBSERVER.URL) abuse ##{abuseId} on #{WEBSERVER.HOST}]
10 blockquote #{messageText}
11 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 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | Abuse state changed
6
7block content
8 p
9 | #[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 @@
1import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
1import { getServerActor } from '@server/models/application/application' 2import { getServerActor } from '@server/models/application/application'
2import { ServerBlocklistModel } from '@server/models/server/server-blocklist' 3import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
3import { 4import {
@@ -18,7 +19,7 @@ import { CONFIG } from '../initializers/config'
18import { AccountBlocklistModel } from '../models/account/account-blocklist' 19import { AccountBlocklistModel } from '../models/account/account-blocklist'
19import { UserModel } from '../models/account/user' 20import { UserModel } from '../models/account/user'
20import { UserNotificationModel } from '../models/account/user-notification' 21import { UserNotificationModel } from '../models/account/user-notification'
21import { MAbuseFull, MAccountServer, MActorFollowFull } from '../types/models' 22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models'
22import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' 23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
23import { isBlockedByServerOrAccount } from './blocklist' 24import { isBlockedByServerOrAccount } from './blocklist'
24import { Emailer } from './emailer' 25import { Emailer } from './emailer'
@@ -129,6 +130,20 @@ class Notifier {
129 }) 130 })
130 } 131 }
131 132
133 notifyOnAbuseStateChange (abuse: MAbuseFull): void {
134 this.notifyReporterOfAbuseStateChange(abuse)
135 .catch(err => {
136 logger.error('Cannot notify reporter of abuse %d state change.', abuse.id, { err })
137 })
138 }
139
140 notifyOnAbuseMessage (abuse: MAbuseFull, message: AbuseMessageModel): void {
141 this.notifyOfNewAbuseMessage(abuse, message)
142 .catch(err => {
143 logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })
144 })
145 }
146
132 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { 147 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
133 // List all followers that are users 148 // List all followers that are users
134 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) 149 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -359,9 +374,7 @@ class Notifier {
359 const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) 374 const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
360 if (moderators.length === 0) return 375 if (moderators.length === 0) return
361 376
362 const url = abuseInstance.VideoAbuse?.Video?.url || 377 const url = this.getAbuseUrl(abuseInstance)
363 abuseInstance.VideoCommentAbuse?.VideoComment?.url ||
364 abuseInstance.FlaggedAccount.Actor.url
365 378
366 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url) 379 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
367 380
@@ -387,6 +400,97 @@ class Notifier {
387 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 400 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
388 } 401 }
389 402
403 private async notifyReporterOfAbuseStateChange (abuse: MAbuseFull) {
404 // Only notify our users
405 if (abuse.ReporterAccount.isOwned() !== true) return
406
407 const url = this.getAbuseUrl(abuse)
408
409 logger.info('Notifying reporter of abuse % of state change.', url)
410
411 const reporter = await UserModel.loadByAccountActorId(abuse.ReporterAccount.actorId)
412
413 function settingGetter (user: MUserWithNotificationSetting) {
414 return user.NotificationSetting.abuseStateChange
415 }
416
417 async function notificationCreator (user: MUserWithNotificationSetting) {
418 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
419 type: UserNotificationType.ABUSE_STATE_CHANGE,
420 userId: user.id,
421 abuseId: abuse.id
422 })
423 notification.Abuse = abuse
424
425 return notification
426 }
427
428 function emailSender (emails: string[]) {
429 return Emailer.Instance.addAbuseStateChangeNotification(emails, abuse)
430 }
431
432 return this.notify({ users: [ reporter ], settingGetter, notificationCreator, emailSender })
433 }
434
435 private async notifyOfNewAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage) {
436 const url = this.getAbuseUrl(abuse)
437 logger.info('Notifying reporter and moderators of new abuse message on %s.', url)
438
439 function settingGetter (user: MUserWithNotificationSetting) {
440 return user.NotificationSetting.abuseNewMessage
441 }
442
443 async function notificationCreator (user: MUserWithNotificationSetting) {
444 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
445 type: UserNotificationType.ABUSE_NEW_MESSAGE,
446 userId: user.id,
447 abuseId: abuse.id
448 })
449 notification.Abuse = abuse
450
451 return notification
452 }
453
454 function emailSenderReporter (emails: string[]) {
455 return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message })
456 }
457
458 function emailSenderModerators (emails: string[]) {
459 return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message })
460 }
461
462 async function buildReporterOptions () {
463 // Only notify our users
464 if (abuse.ReporterAccount.isOwned() !== true) return
465
466 const reporter = await UserModel.loadByAccountActorId(abuse.ReporterAccount.actorId)
467 // Don't notify my own message
468 if (reporter.Account.id === message.accountId) return
469
470 return { users: [ reporter ], settingGetter, notificationCreator, emailSender: emailSenderReporter }
471 }
472
473 async function buildModeratorsOptions () {
474 let moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
475 // Don't notify my own message
476 moderators = moderators.filter(m => m.Account.id !== message.accountId)
477
478 if (moderators.length === 0) return
479
480 return { users: moderators, settingGetter, notificationCreator, emailSender: emailSenderModerators }
481 }
482
483 const [ reporterOptions, moderatorsOptions ] = await Promise.all([
484 buildReporterOptions(),
485 buildModeratorsOptions()
486 ])
487
488 return Promise.all([
489 this.notify(reporterOptions),
490 this.notify(moderatorsOptions)
491 ])
492 }
493
390 private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) { 494 private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) {
391 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST) 495 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
392 if (moderators.length === 0) return 496 if (moderators.length === 0) return
@@ -599,6 +703,12 @@ class Notifier {
599 return isBlockedByServerOrAccount(targetAccount, user?.Account) 703 return isBlockedByServerOrAccount(targetAccount, user?.Account)
600 } 704 }
601 705
706 private getAbuseUrl (abuse: MAbuseFull) {
707 return abuse.VideoAbuse?.Video?.url ||
708 abuse.VideoCommentAbuse?.VideoComment?.url ||
709 abuse.FlaggedAccount.Actor.url
710 }
711
602 static get Instance () { 712 static get Instance () {
603 return this.instance || (this.instance = new this()) 713 return this.instance || (this.instance = new this())
604 } 714 }
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 |
141 commentMention: UserNotificationSettingValue.WEB, 141 commentMention: UserNotificationSettingValue.WEB,
142 newFollow: UserNotificationSettingValue.WEB, 142 newFollow: UserNotificationSettingValue.WEB,
143 newInstanceFollower: UserNotificationSettingValue.WEB, 143 newInstanceFollower: UserNotificationSettingValue.WEB,
144 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
145 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
144 autoInstanceFollowing: UserNotificationSettingValue.WEB 146 autoInstanceFollowing: UserNotificationSettingValue.WEB
145 } 147 }
146 148
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 {
32 UserVideoAbuse 32 UserVideoAbuse
33} from '@shared/models' 33} from '@shared/models'
34import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' 34import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
35import { MAbuse, MAbuseAdminFormattable, MAbuseAP, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' 35import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models'
36import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' 36import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
37import { getSort, throwIfNotValid } from '../utils' 37import { getSort, throwIfNotValid } from '../utils'
38import { ThumbnailModel } from '../video/thumbnail' 38import { ThumbnailModel } from '../video/thumbnail'
39import { VideoModel } from '../video/video' 39import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video'
40import { VideoBlacklistModel } from '../video/video-blacklist' 40import { VideoBlacklistModel } from '../video/video-blacklist'
41import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' 41import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel'
42import { VideoCommentModel } from '../video/video-comment' 42import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment'
43import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' 43import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder'
44import { VideoAbuseModel } from './video-abuse' 44import { VideoAbuseModel } from './video-abuse'
45import { VideoCommentAbuseModel } from './video-comment-abuse' 45import { VideoCommentAbuseModel } from './video-comment-abuse'
@@ -307,6 +307,52 @@ export class AbuseModel extends Model<AbuseModel> {
307 return AbuseModel.findOne(query) 307 return AbuseModel.findOne(query)
308 } 308 }
309 309
310 static loadFull (id: number): Bluebird<MAbuseFull> {
311 const query = {
312 where: {
313 id
314 },
315 include: [
316 {
317 model: AccountModel.scope(AccountScopeNames.SUMMARY),
318 required: false,
319 as: 'ReporterAccount'
320 },
321 {
322 model: AccountModel.scope(AccountScopeNames.SUMMARY),
323 as: 'FlaggedAccount'
324 },
325 {
326 model: VideoAbuseModel,
327 required: false,
328 include: [
329 {
330 model: VideoModel.scope([ VideoScopeNames.WITH_ACCOUNT_DETAILS ])
331 }
332 ]
333 },
334 {
335 model: VideoCommentAbuseModel,
336 required: false,
337 include: [
338 {
339 model: VideoCommentModel.scope([
340 CommentScopeNames.WITH_ACCOUNT
341 ]),
342 include: [
343 {
344 model: VideoModel
345 }
346 ]
347 }
348 ]
349 }
350 ]
351 }
352
353 return AbuseModel.findOne(query)
354 }
355
310 static async listForAdminApi (parameters: { 356 static async listForAdminApi (parameters: {
311 start: number 357 start: number
312 count: number 358 count: number
@@ -455,7 +501,7 @@ export class AbuseModel extends Model<AbuseModel> {
455 blacklisted: abuseModel.Video?.isBlacklisted() || false, 501 blacklisted: abuseModel.Video?.isBlacklisted() || false,
456 thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), 502 thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
457 503
458 channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel, 504 channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel
459 } 505 }
460 } 506 }
461 507
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 {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { throwIfNotValid } from '../utils' 15import { MNotificationSettingFormattable } from '@server/types/models'
16import { UserModel } from './user'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
18import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 16import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
19import { clearCacheByUserId } from '../../lib/oauth-model' 18import { clearCacheByUserId } from '../../lib/oauth-model'
20import { MNotificationSettingFormattable } from '@server/types/models' 19import { throwIfNotValid } from '../utils'
20import { UserModel } from './user'
21 21
22@Table({ 22@Table({
23 tableName: 'userNotificationSetting', 23 tableName: 'userNotificationSetting',
@@ -138,6 +138,24 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
138 @Column 138 @Column
139 commentMention: UserNotificationSettingValue 139 commentMention: UserNotificationSettingValue
140 140
141 @AllowNull(false)
142 @Default(null)
143 @Is(
144 'UserNotificationSettingAbuseStateChange',
145 value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseStateChange')
146 )
147 @Column
148 abuseStateChange: UserNotificationSettingValue
149
150 @AllowNull(false)
151 @Default(null)
152 @Is(
153 'UserNotificationSettingAbuseNewMessage',
154 value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseNewMessage')
155 )
156 @Column
157 abuseNewMessage: UserNotificationSettingValue
158
141 @ForeignKey(() => UserModel) 159 @ForeignKey(() => UserModel)
142 @Column 160 @Column
143 userId: number 161 userId: number
@@ -175,7 +193,9 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
175 commentMention: this.commentMention, 193 commentMention: this.commentMention,
176 newFollow: this.newFollow, 194 newFollow: this.newFollow,
177 newInstanceFollower: this.newInstanceFollower, 195 newInstanceFollower: this.newInstanceFollower,
178 autoInstanceFollowing: this.autoInstanceFollowing 196 autoInstanceFollowing: this.autoInstanceFollowing,
197 abuseNewMessage: this.abuseNewMessage,
198 abuseStateChange: this.abuseStateChange
179 } 199 }
180 } 200 }
181} 201}
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 2945bf709..bd89b8973 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -88,7 +88,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
88 }, 88 },
89 89
90 { 90 {
91 attributes: [ 'id' ], 91 attributes: [ 'id', 'state' ],
92 model: AbuseModel.unscoped(), 92 model: AbuseModel.unscoped(),
93 required: false, 93 required: false,
94 include: [ 94 include: [
@@ -504,6 +504,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
504 504
505 return { 505 return {
506 id: abuse.id, 506 id: abuse.id,
507 state: abuse.state,
507 video: videoAbuse, 508 video: videoAbuse,
508 comment: commentAbuse, 509 comment: commentAbuse,
509 account: accountAbuse 510 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
44import { VideoModel } from './video' 44import { VideoModel } from './video'
45import { VideoChannelModel } from './video-channel' 45import { VideoChannelModel } from './video-channel'
46 46
47enum ScopeNames { 47export enum ScopeNames {
48 WITH_ACCOUNT = 'WITH_ACCOUNT', 48 WITH_ACCOUNT = 'WITH_ACCOUNT',
49 WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API', 49 WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API',
50 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', 50 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 () {
173 newFollow: UserNotificationSettingValue.WEB, 173 newFollow: UserNotificationSettingValue.WEB,
174 newUserRegistration: UserNotificationSettingValue.WEB, 174 newUserRegistration: UserNotificationSettingValue.WEB,
175 newInstanceFollower: UserNotificationSettingValue.WEB, 175 newInstanceFollower: UserNotificationSettingValue.WEB,
176 autoInstanceFollowing: UserNotificationSettingValue.WEB 176 autoInstanceFollowing: UserNotificationSettingValue.WEB,
177 abuseNewMessage: UserNotificationSettingValue.WEB,
178 abuseStateChange: UserNotificationSettingValue.WEB
177 } 179 }
178 180
179 it('Should fail with missing fields', async function () { 181 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 @@
2 2
3import 'mocha' 3import 'mocha'
4import { v4 as uuidv4 } from 'uuid' 4import { v4 as uuidv4 } from 'uuid'
5
5import { 6import {
6 addVideoCommentThread, 7 addVideoCommentThread,
7 addVideoToBlacklist, 8 addVideoToBlacklist,
@@ -21,7 +22,9 @@ import {
21 unfollow, 22 unfollow,
22 updateCustomConfig, 23 updateCustomConfig,
23 updateCustomSubConfig, 24 updateCustomSubConfig,
24 wait 25 wait,
26 updateAbuse,
27 addAbuseMessage
25} from '../../../../shared/extra-utils' 28} from '../../../../shared/extra-utils'
26import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index' 29import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
27import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' 30import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
@@ -38,12 +41,15 @@ import {
38 checkUserRegistered, 41 checkUserRegistered,
39 checkVideoAutoBlacklistForModerators, 42 checkVideoAutoBlacklistForModerators,
40 checkVideoIsPublished, 43 checkVideoIsPublished,
41 prepareNotificationsTest 44 prepareNotificationsTest,
45 checkAbuseStateChange,
46 checkNewAbuseMessage
42} from '../../../../shared/extra-utils/users/user-notifications' 47} from '../../../../shared/extra-utils/users/user-notifications'
43import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions' 48import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions'
44import { CustomConfig } from '../../../../shared/models/server' 49import { CustomConfig } from '../../../../shared/models/server'
45import { UserNotification } from '../../../../shared/models/users' 50import { UserNotification } from '../../../../shared/models/users'
46import { VideoPrivacy } from '../../../../shared/models/videos' 51import { VideoPrivacy } from '../../../../shared/models/videos'
52import { AbuseState } from '@shared/models'
47 53
48describe('Test moderation notifications', function () { 54describe('Test moderation notifications', function () {
49 let servers: ServerInfo[] = [] 55 let servers: ServerInfo[] = []
@@ -65,7 +71,7 @@ describe('Test moderation notifications', function () {
65 adminNotificationsServer2 = res.adminNotificationsServer2 71 adminNotificationsServer2 = res.adminNotificationsServer2
66 }) 72 })
67 73
68 describe('Video abuse for moderators notification', function () { 74 describe('Abuse for moderators notification', function () {
69 let baseParams: CheckerBaseParams 75 let baseParams: CheckerBaseParams
70 76
71 before(() => { 77 before(() => {
@@ -169,6 +175,122 @@ describe('Test moderation notifications', function () {
169 }) 175 })
170 }) 176 })
171 177
178 describe('Abuse state change notification', function () {
179 let baseParams: CheckerBaseParams
180 let abuseId: number
181
182 before(async function () {
183 baseParams = {
184 server: servers[0],
185 emails,
186 socketNotifications: userNotifications,
187 token: userAccessToken
188 }
189
190 const name = 'abuse ' + uuidv4()
191 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
192 const video = resVideo.body.video
193
194 const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason' })
195 abuseId = res.body.abuse.id
196 })
197
198 it('Should send a notification to reporter if the abuse has been accepted', async function () {
199 this.timeout(10000)
200
201 await updateAbuse(servers[0].url, servers[0].accessToken, abuseId, { state: AbuseState.ACCEPTED })
202 await waitJobs(servers)
203
204 await checkAbuseStateChange(baseParams, abuseId, AbuseState.ACCEPTED, 'presence')
205 })
206
207 it('Should send a notification to reporter if the abuse has been rejected', async function () {
208 this.timeout(10000)
209
210 await updateAbuse(servers[0].url, servers[0].accessToken, abuseId, { state: AbuseState.REJECTED })
211 await waitJobs(servers)
212
213 await checkAbuseStateChange(baseParams, abuseId, AbuseState.REJECTED, 'presence')
214 })
215 })
216
217 describe('New abuse message notification', function () {
218 let baseParamsUser: CheckerBaseParams
219 let baseParamsAdmin: CheckerBaseParams
220 let abuseId: number
221 let abuseId2: number
222
223 before(async function () {
224 baseParamsUser = {
225 server: servers[0],
226 emails,
227 socketNotifications: userNotifications,
228 token: userAccessToken
229 }
230
231 baseParamsAdmin = {
232 server: servers[0],
233 emails,
234 socketNotifications: adminNotifications,
235 token: servers[0].accessToken
236 }
237
238 const name = 'abuse ' + uuidv4()
239 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
240 const video = resVideo.body.video
241
242 {
243 const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason' })
244 abuseId = res.body.abuse.id
245 }
246
247 {
248 const res = await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: video.id, reason: 'super reason 2' })
249 abuseId2 = res.body.abuse.id
250 }
251 })
252
253 it('Should send a notification to reporter on new message', async function () {
254 this.timeout(10000)
255
256 const message = 'my super message to users'
257 await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, message)
258 await waitJobs(servers)
259
260 await checkNewAbuseMessage(baseParamsUser, abuseId, message, 'user_1@example.com', 'presence')
261 })
262
263 it('Should not send a notification to the admin if sent by the admin', async function () {
264 this.timeout(10000)
265
266 const message = 'my super message that should not be sent to the admin'
267 await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, message)
268 await waitJobs(servers)
269
270 await checkNewAbuseMessage(baseParamsAdmin, abuseId, message, 'admin1@example.com', 'absence')
271 })
272
273 it('Should send a notification to moderators', async function () {
274 this.timeout(10000)
275
276 const message = 'my super message to moderators'
277 await addAbuseMessage(servers[0].url, userAccessToken, abuseId2, message)
278 await waitJobs(servers)
279
280 await checkNewAbuseMessage(baseParamsAdmin, abuseId2, message, 'admin1@example.com', 'presence')
281 })
282
283 it('Should not send a notification to reporter if sent by the reporter', async function () {
284 this.timeout(10000)
285
286 const message = 'my super message that should not be sent to reporter'
287 await addAbuseMessage(servers[0].url, userAccessToken, abuseId2, message)
288 await waitJobs(servers)
289
290 await checkNewAbuseMessage(baseParamsUser, abuseId2, message, 'user_1@example.com', 'absence')
291 })
292 })
293
172 describe('Video blacklist on my video', function () { 294 describe('Video blacklist on my video', function () {
173 let baseParams: CheckerBaseParams 295 let baseParams: CheckerBaseParams
174 296
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'
5import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl, MAccount } from '../account' 5import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl, MAccount } from '../account'
6import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video' 6import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video'
7import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' 7import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
8import { VideoCommentModel } from '@server/models/video/video-comment'
8 9
9type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M> 10type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M>
10type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M> 11type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
@@ -34,7 +35,7 @@ export type MVideoAbuseVideoUrl =
34 35
35export type MVideoAbuseVideoFull = 36export type MVideoAbuseVideoFull =
36 MVideoAbuse & 37 MVideoAbuse &
37 UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles> 38 UseVideoAbuse<'Video', Omit<MVideoAccountLightBlacklistAllFiles, 'VideoFiles' | 'VideoStreamingPlaylists'>>
38 39
39export type MVideoAbuseFormattable = 40export type MVideoAbuseFormattable =
40 MVideoAbuse & 41 MVideoAbuse &
@@ -49,7 +50,7 @@ export type MCommentAbuseAccount =
49 50
50export type MCommentAbuseAccountVideo = 51export type MCommentAbuseAccountVideo =
51 MCommentAbuse & 52 MCommentAbuse &
52 UseCommentAbuse<'VideoComment', MCommentOwnerVideo> 53 UseCommentAbuse<'VideoComment', MCommentOwner & PickWith<VideoCommentModel, 'Video', MVideo>>
53 54
54export type MCommentAbuseUrl = 55export type MCommentAbuseUrl =
55 MCommentAbuse & 56 MCommentAbuse &
@@ -79,14 +80,6 @@ export type MAbuseAccountVideo =
79 Use<'VideoAbuse', MVideoAbuseVideoFull> & 80 Use<'VideoAbuse', MVideoAbuseVideoFull> &
80 Use<'ReporterAccount', MAccountDefault> 81 Use<'ReporterAccount', MAccountDefault>
81 82
82export type MAbuseAP =
83 MAbuse &
84 Pick<AbuseModel, 'toActivityPubObject'> &
85 Use<'ReporterAccount', MAccountUrl> &
86 Use<'FlaggedAccount', MAccountUrl> &
87 Use<'VideoAbuse', MVideoAbuseVideo> &
88 Use<'VideoCommentAbuse', MCommentAbuseAccount>
89
90export type MAbuseFull = 83export type MAbuseFull =
91 MAbuse & 84 MAbuse &
92 Pick<AbuseModel, 'toActivityPubObject'> & 85 Pick<AbuseModel, 'toActivityPubObject'> &
@@ -111,3 +104,11 @@ export type MAbuseUserFormattable =
111 Use<'FlaggedAccount', MAccountFormattable> & 104 Use<'FlaggedAccount', MAccountFormattable> &
112 Use<'VideoAbuse', MVideoAbuseFormattable> & 105 Use<'VideoAbuse', MVideoAbuseFormattable> &
113 Use<'VideoCommentAbuse', MCommentAbuseFormattable> 106 Use<'VideoCommentAbuse', MCommentAbuseFormattable>
107
108export type MAbuseAP =
109 MAbuse &
110 Pick<AbuseModel, 'toActivityPubObject'> &
111 Use<'ReporterAccount', MAccountUrl> &
112 Use<'FlaggedAccount', MAccountUrl> &
113 Use<'VideoAbuse', MVideoAbuseVideo> &
114 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 {
56 PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'id' | 'name' | 'uuid'>>> 56 PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'id' | 'name' | 'uuid'>>>
57 57
58 export type AbuseInclude = 58 export type AbuseInclude =
59 Pick<AbuseModel, 'id'> & 59 Pick<AbuseModel, 'id' | 'state'> &
60 PickWith<AbuseModel, 'VideoAbuse', VideoAbuseInclude> & 60 PickWith<AbuseModel, 'VideoAbuse', VideoAbuseInclude> &
61 PickWith<AbuseModel, 'VideoCommentAbuse', VideoCommentAbuseInclude> & 61 PickWith<AbuseModel, 'VideoCommentAbuse', VideoCommentAbuseInclude> &
62 PickWith<AbuseModel, 'FlaggedAccount', AccountIncludeActor> 62 PickWith<AbuseModel, 'FlaggedAccount', AccountIncludeActor>