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