Add abuse messages/states notifications

pull/2926/head
Chocobozzz 2020-07-27 16:26:25 +02:00 committed by Chocobozzz
parent 94148c9028
commit 594d3e48d8
19 changed files with 510 additions and 48 deletions

View File

@ -16,11 +16,11 @@
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced report filters</h6>
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
</div>
</div>
<input

View File

@ -25,6 +25,8 @@ import {
setDefaultSort
} from '../../middlewares'
import { AccountModel } from '../../models/account/account'
import { Notifier } from '@server/lib/notifier'
import { logger } from '@server/helpers/logger'
const abuseRouter = express.Router()
@ -123,19 +125,28 @@ async function listAbusesForAdmins (req: express.Request, res: express.Response)
async function updateAbuse (req: express.Request, res: express.Response) {
const abuse = res.locals.abuse
let stateUpdated = false
if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment
if (req.body.state !== undefined) abuse.state = req.body.state
if (req.body.state !== undefined) {
abuse.state = req.body.state
stateUpdated = true
}
await sequelizeTypescript.transaction(t => {
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: {

View File

@ -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)

View File

@ -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()

View File

@ -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;")

View File

@ -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'}

View File

@ -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<UserNotificationModelForApi>({
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<UserNotificationModelForApi>({
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())
}

View File

@ -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
}

View File

@ -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<AbuseModel> {
return AbuseModel.findOne(query)
}
static loadFull (id: number): Bluebird<MAbuseFull> {
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<AbuseModel> {
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
}
}

View File

@ -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<UserNotificationSettingM
@Column
commentMention: UserNotificationSettingValue
@AllowNull(false)
@Default(null)
@Is(
'UserNotificationSettingAbuseStateChange',
value => 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<UserNotificationSettingM
commentMention: this.commentMention,
newFollow: this.newFollow,
newInstanceFollower: this.newInstanceFollower,
autoInstanceFollowing: this.autoInstanceFollowing
autoInstanceFollowing: this.autoInstanceFollowing,
abuseNewMessage: this.abuseNewMessage,
abuseStateChange: this.abuseStateChange
}
}
}

View File

@ -88,7 +88,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
},
{
attributes: [ 'id' ],
attributes: [ 'id', 'state' ],
model: AbuseModel.unscoped(),
required: false,
include: [
@ -504,6 +504,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
return {
id: abuse.id,
state: abuse.state,
video: videoAbuse,
comment: commentAbuse,
account: accountAbuse

View File

@ -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',

View File

@ -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 () {

View File

@ -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

View File

@ -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<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M>
type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
@ -34,7 +35,7 @@ export type MVideoAbuseVideoUrl =
export type MVideoAbuseVideoFull =
MVideoAbuse &
UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles>
UseVideoAbuse<'Video', Omit<MVideoAccountLightBlacklistAllFiles, 'VideoFiles' | 'VideoStreamingPlaylists'>>
export type MVideoAbuseFormattable =
MVideoAbuse &
@ -49,7 +50,7 @@ export type MCommentAbuseAccount =
export type MCommentAbuseAccountVideo =
MCommentAbuse &
UseCommentAbuse<'VideoComment', MCommentOwnerVideo>
UseCommentAbuse<'VideoComment', MCommentOwner & PickWith<VideoCommentModel, 'Video', MVideo>>
export type MCommentAbuseUrl =
MCommentAbuse &
@ -79,14 +80,6 @@ export type MAbuseAccountVideo =
Use<'VideoAbuse', MVideoAbuseVideoFull> &
Use<'ReporterAccount', MAccountDefault>
export type MAbuseAP =
MAbuse &
Pick<AbuseModel, 'toActivityPubObject'> &
Use<'ReporterAccount', MAccountUrl> &
Use<'FlaggedAccount', MAccountUrl> &
Use<'VideoAbuse', MVideoAbuseVideo> &
Use<'VideoCommentAbuse', MCommentAbuseAccount>
export type MAbuseFull =
MAbuse &
Pick<AbuseModel, 'toActivityPubObject'> &
@ -111,3 +104,11 @@ export type MAbuseUserFormattable =
Use<'FlaggedAccount', MAccountFormattable> &
Use<'VideoAbuse', MVideoAbuseFormattable> &
Use<'VideoCommentAbuse', MCommentAbuseFormattable>
export type MAbuseAP =
MAbuse &
Pick<AbuseModel, 'toActivityPubObject'> &
Use<'ReporterAccount', MAccountUrl> &
Use<'FlaggedAccount', MAccountUrl> &
Use<'VideoAbuse', MVideoAbuseVideo> &
Use<'VideoCommentAbuse', MCommentAbuseAccount>

View File

@ -56,7 +56,7 @@ export module UserNotificationIncludes {
PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'id' | 'name' | 'uuid'>>>
export type AbuseInclude =
Pick<AbuseModel, 'id'> &
Pick<AbuseModel, 'id' | 'state'> &
PickWith<AbuseModel, 'VideoAbuse', VideoAbuseInclude> &
PickWith<AbuseModel, 'VideoCommentAbuse', VideoCommentAbuseInclude> &
PickWith<AbuseModel, 'FlaggedAccount', AccountIncludeActor>

View File

@ -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,

View File

@ -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
}

View File

@ -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