mirror of https://github.com/Chocobozzz/PeerTube
Add abuse messages/states notifications
parent
94148c9028
commit
594d3e48d8
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;")
|
|
@ -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'}
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue