diff options
author | Chocobozzz <me@florianbigard.com> | 2020-07-27 16:26:25 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-07-31 11:35:19 +0200 |
commit | 594d3e48d8a887bbf48ce4cc594c1c36c9640fb1 (patch) | |
tree | bae28fa6215a3a3c6ccd78aea6ea7e75c500a96f | |
parent | 94148c9028829b5576a5dcbfba2c7fb9cf6443d3 (diff) | |
download | PeerTube-594d3e48d8a887bbf48ce4cc594c1c36c9640fb1.tar.gz PeerTube-594d3e48d8a887bbf48ce4cc594c1c36c9640fb1.tar.zst PeerTube-594d3e48d8a887bbf48ce4cc594c1c36c9640fb1.zip |
Add abuse messages/states notifications
19 files changed, 510 insertions, 48 deletions
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 @@ | |||
16 | 16 | ||
17 | <div role="menu" ngbDropdownMenu> | 17 | <div role="menu" ngbDropdownMenu> |
18 | <h6 class="dropdown-header" i18n>Advanced report filters</h6> | 18 | <h6 class="dropdown-header" i18n>Advanced report filters</h6> |
19 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a> | 19 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a> |
20 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a> | 20 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a> |
21 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a> | 21 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a> |
22 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a> | 22 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a> |
23 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a> | 23 | <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a> |
24 | </div> | 24 | </div> |
25 | </div> | 25 | </div> |
26 | <input | 26 | <input |
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' |
27 | import { AccountModel } from '../../models/account/account' | 27 | import { AccountModel } from '../../models/account/account' |
28 | import { Notifier } from '@server/lib/notifier' | ||
29 | import { logger } from '@server/helpers/logger' | ||
28 | 30 | ||
29 | const abuseRouter = express.Router() | 31 | const abuseRouter = express.Router() |
30 | 32 | ||
@@ -123,19 +125,28 @@ async function listAbusesForAdmins (req: express.Request, res: express.Response) | |||
123 | 125 | ||
124 | async function updateAbuse (req: express.Request, res: express.Response) { | 126 | async 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 | ||
141 | async function deleteAbuse (req: express.Request, res: express.Response) { | 152 | async 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 | ||
153 | async function reportAbuse (req: express.Request, res: express.Response) { | 164 | async 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' | |||
5 | import { VideoChannelModel } from '@server/models/video/video-channel' | 5 | import { VideoChannelModel } from '@server/models/video/video-channel' |
6 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | 6 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' |
7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' | 7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' |
8 | import { UserAbuse, EmailPayload } from '@shared/models' | 8 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' |
9 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' | 9 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' |
10 | import { isTestInstance, root } from '../helpers/core-utils' | 10 | import { isTestInstance, root } from '../helpers/core-utils' |
11 | import { bunyanLogger, logger } from '../helpers/logger' | 11 | import { bunyanLogger, logger } from '../helpers/logger' |
12 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 12 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
13 | import { WEBSERVER } from '../initializers/constants' | 13 | import { WEBSERVER } from '../initializers/constants' |
14 | import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' | 14 | import { MAbuseFull, MAbuseMessage, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' |
15 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' | 15 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' |
16 | import { JobQueue } from './job-queue' | 16 | import { 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 @@ | |||
1 | extends ../common/greetings | ||
2 | include ../common/mixins.pug | ||
3 | |||
4 | block title | ||
5 | | New abuse message | ||
6 | |||
7 | block 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 @@ | |||
1 | extends ../common/greetings | ||
2 | include ../common/mixins.pug | ||
3 | |||
4 | block title | ||
5 | | Abuse state changed | ||
6 | |||
7 | block 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 @@ | |||
1 | import { AbuseMessageModel } from '@server/models/abuse/abuse-message' | ||
1 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
2 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | 3 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' |
3 | import { | 4 | import { |
@@ -18,7 +19,7 @@ import { CONFIG } from '../initializers/config' | |||
18 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
19 | import { UserModel } from '../models/account/user' | 20 | import { UserModel } from '../models/account/user' |
20 | import { UserNotificationModel } from '../models/account/user-notification' | 21 | import { UserNotificationModel } from '../models/account/user-notification' |
21 | import { MAbuseFull, MAccountServer, MActorFollowFull } from '../types/models' | 22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' |
22 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' | 23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' |
23 | import { isBlockedByServerOrAccount } from './blocklist' | 24 | import { isBlockedByServerOrAccount } from './blocklist' |
24 | import { Emailer } from './emailer' | 25 | import { 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' |
34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | 34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
35 | import { MAbuse, MAbuseAdminFormattable, MAbuseAP, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' | 35 | import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' |
36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
37 | import { getSort, throwIfNotValid } from '../utils' | 37 | import { getSort, throwIfNotValid } from '../utils' |
38 | import { ThumbnailModel } from '../video/thumbnail' | 38 | import { ThumbnailModel } from '../video/thumbnail' |
39 | import { VideoModel } from '../video/video' | 39 | import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' |
40 | import { VideoBlacklistModel } from '../video/video-blacklist' | 40 | import { VideoBlacklistModel } from '../video/video-blacklist' |
41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' | 41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' |
42 | import { VideoCommentModel } from '../video/video-comment' | 42 | import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment' |
43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' | 43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' |
44 | import { VideoAbuseModel } from './video-abuse' | 44 | import { VideoAbuseModel } from './video-abuse' |
45 | import { VideoCommentAbuseModel } from './video-comment-abuse' | 45 | import { 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' |
15 | import { throwIfNotValid } from '../utils' | 15 | import { MNotificationSettingFormattable } from '@server/types/models' |
16 | import { UserModel } from './user' | ||
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 16 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
19 | import { clearCacheByUserId } from '../../lib/oauth-model' | 18 | import { clearCacheByUserId } from '../../lib/oauth-model' |
20 | import { MNotificationSettingFormattable } from '@server/types/models' | 19 | import { throwIfNotValid } from '../utils' |
20 | import { 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 | |||
44 | import { VideoModel } from './video' | 44 | import { VideoModel } from './video' |
45 | import { VideoChannelModel } from './video-channel' | 45 | import { VideoChannelModel } from './video-channel' |
46 | 46 | ||
47 | enum ScopeNames { | 47 | export 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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { v4 as uuidv4 } from 'uuid' | 4 | import { v4 as uuidv4 } from 'uuid' |
5 | |||
5 | import { | 6 | import { |
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' |
26 | import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index' | 29 | import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index' |
27 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | 30 | import { 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' |
43 | import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions' | 48 | import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions' |
44 | import { CustomConfig } from '../../../../shared/models/server' | 49 | import { CustomConfig } from '../../../../shared/models/server' |
45 | import { UserNotification } from '../../../../shared/models/users' | 50 | import { UserNotification } from '../../../../shared/models/users' |
46 | import { VideoPrivacy } from '../../../../shared/models/videos' | 51 | import { VideoPrivacy } from '../../../../shared/models/videos' |
52 | import { AbuseState } from '@shared/models' | ||
47 | 53 | ||
48 | describe('Test moderation notifications', function () { | 54 | describe('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' | |||
5 | import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl, MAccount } from '../account' | 5 | import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl, MAccount } from '../account' |
6 | import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video' | 6 | import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video' |
7 | import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' | 7 | import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' |
8 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
8 | 9 | ||
9 | type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M> | 10 | type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M> |
10 | type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M> | 11 | type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M> |
@@ -34,7 +35,7 @@ export type MVideoAbuseVideoUrl = | |||
34 | 35 | ||
35 | export type MVideoAbuseVideoFull = | 36 | export type MVideoAbuseVideoFull = |
36 | MVideoAbuse & | 37 | MVideoAbuse & |
37 | UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles> | 38 | UseVideoAbuse<'Video', Omit<MVideoAccountLightBlacklistAllFiles, 'VideoFiles' | 'VideoStreamingPlaylists'>> |
38 | 39 | ||
39 | export type MVideoAbuseFormattable = | 40 | export type MVideoAbuseFormattable = |
40 | MVideoAbuse & | 41 | MVideoAbuse & |
@@ -49,7 +50,7 @@ export type MCommentAbuseAccount = | |||
49 | 50 | ||
50 | export type MCommentAbuseAccountVideo = | 51 | export type MCommentAbuseAccountVideo = |
51 | MCommentAbuse & | 52 | MCommentAbuse & |
52 | UseCommentAbuse<'VideoComment', MCommentOwnerVideo> | 53 | UseCommentAbuse<'VideoComment', MCommentOwner & PickWith<VideoCommentModel, 'Video', MVideo>> |
53 | 54 | ||
54 | export type MCommentAbuseUrl = | 55 | export 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 | ||
82 | export 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 | |||
90 | export type MAbuseFull = | 83 | export 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 | |||
108 | export 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> |
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 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { inspect } from 'util' | 4 | import { inspect } from 'util' |
5 | import { AbuseState } from '@shared/models' | ||
5 | import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' | 6 | import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' |
6 | import { MockSmtpServer } from '../miscs/email' | 7 | import { MockSmtpServer } from '../miscs/email' |
7 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' | 8 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' |
@@ -464,6 +465,62 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU | |||
464 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) | 465 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) |
465 | } | 466 | } |
466 | 467 | ||
468 | async function checkNewAbuseMessage (base: CheckerBaseParams, abuseId: number, message: string, toEmail: string, type: CheckerType) { | ||
469 | const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE | ||
470 | |||
471 | function notificationChecker (notification: UserNotification, type: CheckerType) { | ||
472 | if (type === 'presence') { | ||
473 | expect(notification).to.not.be.undefined | ||
474 | expect(notification.type).to.equal(notificationType) | ||
475 | |||
476 | expect(notification.abuse.id).to.equal(abuseId) | ||
477 | } else { | ||
478 | expect(notification).to.satisfy((n: UserNotification) => { | ||
479 | return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId | ||
480 | }) | ||
481 | } | ||
482 | } | ||
483 | |||
484 | function emailNotificationFinder (email: object) { | ||
485 | const text = email['text'] | ||
486 | const to = email['to'].filter(t => t.address === toEmail) | ||
487 | |||
488 | return text.indexOf(message) !== -1 && to.length !== 0 | ||
489 | } | ||
490 | |||
491 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) | ||
492 | } | ||
493 | |||
494 | async function checkAbuseStateChange (base: CheckerBaseParams, abuseId: number, state: AbuseState, type: CheckerType) { | ||
495 | const notificationType = UserNotificationType.ABUSE_STATE_CHANGE | ||
496 | |||
497 | function notificationChecker (notification: UserNotification, type: CheckerType) { | ||
498 | if (type === 'presence') { | ||
499 | expect(notification).to.not.be.undefined | ||
500 | expect(notification.type).to.equal(notificationType) | ||
501 | |||
502 | expect(notification.abuse.id).to.equal(abuseId) | ||
503 | expect(notification.abuse.state).to.equal(state) | ||
504 | } else { | ||
505 | expect(notification).to.satisfy((n: UserNotification) => { | ||
506 | return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId | ||
507 | }) | ||
508 | } | ||
509 | } | ||
510 | |||
511 | function emailNotificationFinder (email: object) { | ||
512 | const text = email['text'] | ||
513 | |||
514 | const contains = state === AbuseState.ACCEPTED | ||
515 | ? ' accepted' | ||
516 | : ' rejected' | ||
517 | |||
518 | return text.indexOf(contains) !== -1 | ||
519 | } | ||
520 | |||
521 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) | ||
522 | } | ||
523 | |||
467 | async function checkNewCommentAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { | 524 | async function checkNewCommentAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { |
468 | const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS | 525 | const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS |
469 | 526 | ||
@@ -579,6 +636,8 @@ function getAllNotificationsSettings () { | |||
579 | newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 636 | newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
580 | newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 637 | newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
581 | newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 638 | newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
639 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
640 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
582 | autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | 641 | autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL |
583 | } as UserNotificationSetting | 642 | } as UserNotificationSetting |
584 | } | 643 | } |
@@ -676,6 +735,8 @@ export { | |||
676 | updateMyNotificationSettings, | 735 | updateMyNotificationSettings, |
677 | checkNewVideoAbuseForModerators, | 736 | checkNewVideoAbuseForModerators, |
678 | checkVideoAutoBlacklistForModerators, | 737 | checkVideoAutoBlacklistForModerators, |
738 | checkNewAbuseMessage, | ||
739 | checkAbuseStateChange, | ||
679 | getUserNotifications, | 740 | getUserNotifications, |
680 | markAsReadNotifications, | 741 | markAsReadNotifications, |
681 | getLastNotification, | 742 | 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 { | |||
5 | } | 5 | } |
6 | 6 | ||
7 | export interface UserNotificationSetting { | 7 | export interface UserNotificationSetting { |
8 | newVideoFromSubscription: UserNotificationSettingValue | ||
9 | newCommentOnMyVideo: UserNotificationSettingValue | ||
10 | abuseAsModerator: UserNotificationSettingValue | 8 | abuseAsModerator: UserNotificationSettingValue |
11 | videoAutoBlacklistAsModerator: UserNotificationSettingValue | 9 | videoAutoBlacklistAsModerator: UserNotificationSettingValue |
10 | newUserRegistration: UserNotificationSettingValue | ||
11 | |||
12 | newVideoFromSubscription: UserNotificationSettingValue | ||
13 | |||
12 | blacklistOnMyVideo: UserNotificationSettingValue | 14 | blacklistOnMyVideo: UserNotificationSettingValue |
13 | myVideoPublished: UserNotificationSettingValue | 15 | myVideoPublished: UserNotificationSettingValue |
14 | myVideoImportFinished: UserNotificationSettingValue | 16 | myVideoImportFinished: UserNotificationSettingValue |
15 | newUserRegistration: UserNotificationSettingValue | 17 | |
16 | newFollow: UserNotificationSettingValue | ||
17 | commentMention: UserNotificationSettingValue | 18 | commentMention: UserNotificationSettingValue |
19 | newCommentOnMyVideo: UserNotificationSettingValue | ||
20 | |||
21 | newFollow: UserNotificationSettingValue | ||
18 | newInstanceFollower: UserNotificationSettingValue | 22 | newInstanceFollower: UserNotificationSettingValue |
19 | autoInstanceFollowing: UserNotificationSettingValue | 23 | autoInstanceFollowing: UserNotificationSettingValue |
24 | |||
25 | abuseStateChange: UserNotificationSettingValue | ||
26 | abuseNewMessage: UserNotificationSettingValue | ||
20 | } | 27 | } |
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 @@ | |||
1 | import { FollowState } from '../actors' | 1 | import { FollowState } from '../actors' |
2 | import { AbuseState } from '../moderation' | ||
2 | 3 | ||
3 | export enum UserNotificationType { | 4 | export enum UserNotificationType { |
4 | NEW_VIDEO_FROM_SUBSCRIPTION = 1, | 5 | NEW_VIDEO_FROM_SUBSCRIPTION = 1, |
@@ -21,7 +22,11 @@ export enum UserNotificationType { | |||
21 | 22 | ||
22 | NEW_INSTANCE_FOLLOWER = 13, | 23 | NEW_INSTANCE_FOLLOWER = 13, |
23 | 24 | ||
24 | AUTO_INSTANCE_FOLLOWING = 14 | 25 | AUTO_INSTANCE_FOLLOWING = 14, |
26 | |||
27 | ABUSE_STATE_CHANGE = 15, | ||
28 | |||
29 | ABUSE_NEW_MESSAGE = 16 | ||
25 | } | 30 | } |
26 | 31 | ||
27 | export interface VideoInfo { | 32 | export interface VideoInfo { |
@@ -66,6 +71,7 @@ export interface UserNotification { | |||
66 | 71 | ||
67 | abuse?: { | 72 | abuse?: { |
68 | id: number | 73 | id: number |
74 | state: AbuseState | ||
69 | 75 | ||
70 | video?: VideoInfo | 76 | video?: VideoInfo |
71 | 77 | ||