Add migrations for abuse messages

This commit is contained in:
Chocobozzz 2020-07-28 09:57:16 +02:00 committed by Chocobozzz
parent 594d3e48d8
commit d573926e9b
18 changed files with 189 additions and 43 deletions

View File

@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
newFollow: this.i18n('You or your channel(s) has a new follower'), newFollow: this.i18n('You or your channel(s) has a new follower'),
commentMention: this.i18n('Someone mentioned you in video comments'), commentMention: this.i18n('Someone mentioned you in video comments'),
newInstanceFollower: this.i18n('Your instance has a new follower'), newInstanceFollower: this.i18n('Your instance has a new follower'),
autoInstanceFollowing: this.i18n('Your instance auto followed another instance') autoInstanceFollowing: this.i18n('Your instance auto followed another instance'),
abuseNewMessage: this.i18n('An abuse received a new message'),
abuseStateChange: this.i18n('One of your abuse has been accepted or rejected by moderators')
} }
this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]

View File

@ -3,19 +3,29 @@ import { LinkifierService } from './linkifier.service'
@Injectable() @Injectable()
export class HtmlRendererService { export class HtmlRendererService {
private sanitizeHtml: typeof import ('sanitize-html')
constructor (private linkifier: LinkifierService) { constructor (private linkifier: LinkifierService) {
} }
async convertToBr (text: string) {
await this.loadSanitizeHtml()
const html = text.replace(/\r?\n/g, '<br />')
return this.sanitizeHtml(html, {
allowedTags: [ 'br' ]
})
}
async toSafeHtml (text: string) { async toSafeHtml (text: string) {
// FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function await this.loadSanitizeHtml()
const sanitizeHtml: typeof import ('sanitize-html') = (await import('sanitize-html') as any).default
// Convert possible markdown to html // Convert possible markdown to html
const html = this.linkifier.linkify(text) const html = this.linkifier.linkify(text)
return sanitizeHtml(html, { return this.sanitizeHtml(html, {
allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ], allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
allowedSchemes: [ 'http', 'https' ], allowedSchemes: [ 'http', 'https' ],
allowedAttributes: { allowedAttributes: {
@ -37,4 +47,9 @@ export class HtmlRendererService {
} }
}) })
} }
private async loadSanitizeHtml () {
// FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
this.sanitizeHtml = (await import('sanitize-html') as any).default
}
} }

View File

@ -42,6 +42,7 @@
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
<th i18n style="width: 80px;">Messages</th> <th i18n style="width: 80px;">Messages</th>
<th i18n *ngIf="isAdminView()" style="width: 100px;">Internal note</th>
<th style="width: 150px;"></th> <th style="width: 150px;"></th>
</tr> </tr>
</ng-template> </ng-template>
@ -144,13 +145,11 @@
</ng-container> </ng-container>
<td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
<td class="c-hand abuse-states" [pRowToggler]="abuse"> <td class="c-hand abuse-states" [pRowToggler]="abuse">
<span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span> <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
<span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span> <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
<span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
</td> </td>
<td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)"> <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
@ -161,6 +160,10 @@
</ng-container> </ng-container>
</td> </td>
<td *ngIf="isAdminView()" class="internal-note" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment">
{{ abuse.moderationComment }}
</td>
<td class="action-cell"> <td class="action-cell">
<my-action-dropdown <my-action-dropdown
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"

View File

@ -278,7 +278,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
isDisplayed: abuse => this.isLocalAbuse(abuse) isDisplayed: abuse => this.isLocalAbuse(abuse)
}, },
{ {
label: this.i18n('Update note'), label: this.i18n('Update internal note'),
handler: abuse => this.openModerationCommentModal(abuse), handler: abuse => this.openModerationCommentModal(abuse),
isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
}, },

View File

@ -9,7 +9,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="messages" #messagesBlock> <div class="messages">
<div <div
*ngFor="let message of abuseMessages" *ngFor="let message of abuseMessages"
class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }" class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }"
@ -18,7 +18,7 @@
<div class="author">{{ message.account.name }}</div> <div class="author">{{ message.account.name }}</div>
<div class="bubble"> <div class="bubble">
<div class="content">{{ message.message }}</div> <div class="content" [innerHTML]="message.messageHtml"></div>
<div class="date">{{ message.createdAt | date }}</div> <div class="date">{{ message.createdAt | date }}</div>
</div> </div>
</div> </div>

View File

@ -20,6 +20,7 @@ textarea {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: scroll; overflow-y: scroll;
max-height: 50vh;
} }
.no-messages { .no-messages {

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { AuthService, Notifier } from '@app/core' import { AuthService, Notifier, HtmlRendererService } from '@app/core'
import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@ -14,13 +14,12 @@ import { AbuseService } from '../shared-moderation'
}) })
export class AbuseMessageModalComponent extends FormReactive implements OnInit { export class AbuseMessageModalComponent extends FormReactive implements OnInit {
@ViewChild('modal', { static: true }) modal: NgbModal @ViewChild('modal', { static: true }) modal: NgbModal
@ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
@Input() isAdminView: boolean @Input() isAdminView: boolean
@Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>() @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
abuseMessages: AbuseMessage[] = [] abuseMessages: (AbuseMessage & { messageHtml: string })[] = []
textareaMessage: string textareaMessage: string
sendingMessage = false sendingMessage = false
noResults = false noResults = false
@ -33,6 +32,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
private abuseValidatorsService: AbuseValidatorsService, private abuseValidatorsService: AbuseValidatorsService,
private modalService: NgbModal, private modalService: NgbModal,
private i18n: I18n, private i18n: I18n,
private htmlRenderer: HtmlRendererService,
private auth: AuthService, private auth: AuthService,
private notifier: Notifier, private notifier: Notifier,
private abuseService: AbuseService private abuseService: AbuseService
@ -108,15 +108,21 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
private loadMessages () { private loadMessages () {
this.abuseService.listAbuseMessages(this.abuse) this.abuseService.listAbuseMessages(this.abuse)
.subscribe( .subscribe(
res => { async res => {
this.abuseMessages = res.data this.abuseMessages = []
for (const m of res.data) {
this.abuseMessages.push(Object.assign(m, {
messageHtml: await this.htmlRenderer.convertToBr(m.message)
}))
}
this.noResults = this.abuseMessages.length === 0 this.noResults = this.abuseMessages.length === 0
setTimeout(() => { setTimeout(() => {
if (!this.messagesBlock) return // Don't use ViewChild: it is not supported inside a ng-template
const messagesBlock = document.querySelector('.messages')
const element = this.messagesBlock.nativeElement as HTMLElement messagesBlock.scroll(0, messagesBlock.scrollHeight)
element.scrollIntoView({ block: 'end', inline: 'nearest' })
}) })
}, },

View File

@ -1,5 +1,14 @@
import {
AbuseState,
ActorInfo,
FollowState,
UserNotification as UserNotificationServer,
UserNotificationType,
VideoInfo,
UserRight
} from '@shared/models'
import { Actor } from '../account/actor.model' import { Actor } from '../account/actor.model'
import { ActorInfo, Avatar, FollowState, UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '@shared/models' import { AuthUser } from '@app/core'
export class UserNotification implements UserNotificationServer { export class UserNotification implements UserNotificationServer {
id: number id: number
@ -27,6 +36,7 @@ export class UserNotification implements UserNotificationServer {
abuse?: { abuse?: {
id: number id: number
state: AbuseState
video?: VideoInfo video?: VideoInfo
@ -69,13 +79,14 @@ export class UserNotification implements UserNotificationServer {
videoUrl?: string videoUrl?: string
commentUrl?: any[] commentUrl?: any[]
abuseUrl?: string abuseUrl?: string
abuseQueryParams?: { [id: string]: string } = {}
videoAutoBlacklistUrl?: string videoAutoBlacklistUrl?: string
accountUrl?: string accountUrl?: string
videoImportIdentifier?: string videoImportIdentifier?: string
videoImportUrl?: string videoImportUrl?: string
instanceFollowUrl?: string instanceFollowUrl?: string
constructor (hash: UserNotificationServer) { constructor (hash: UserNotificationServer, user: AuthUser) {
this.id = hash.id this.id = hash.id
this.type = hash.type this.type = hash.type
this.read = hash.read this.read = hash.read
@ -122,12 +133,25 @@ export class UserNotification implements UserNotificationServer {
case UserNotificationType.NEW_ABUSE_FOR_MODERATORS: case UserNotificationType.NEW_ABUSE_FOR_MODERATORS:
this.abuseUrl = '/admin/moderation/abuses/list' this.abuseUrl = '/admin/moderation/abuses/list'
this.abuseQueryParams.search = '#' + this.abuse.id
if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment) else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment)
else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account) else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account)
break break
case UserNotificationType.ABUSE_STATE_CHANGE:
this.abuseUrl = '/my-account/abuses'
this.abuseQueryParams.search = '#' + this.abuse.id
break
case UserNotificationType.ABUSE_NEW_MESSAGE:
this.abuseUrl = user.hasRight(UserRight.MANAGE_ABUSES)
? '/admin/moderation/abuses/list'
: '/my-account/abuses'
this.abuseQueryParams.search = '#' + this.abuse.id
break
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list' this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
// Backward compatibility where we did not assign videoBlacklist to this type of notification before // Backward compatibility where we did not assign videoBlacklist to this type of notification before

View File

@ -1,7 +1,7 @@
import { catchError, map, tap } from 'rxjs/operators' import { catchError, map, tap } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http' import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket } from '@app/core' import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket, AuthService } from '@app/core'
import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models' import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models'
import { environment } from '../../../../environments/environment' import { environment } from '../../../../environments/environment'
import { UserNotification } from './user-notification.model' import { UserNotification } from './user-notification.model'
@ -14,6 +14,7 @@ export class UserNotificationService {
constructor ( constructor (
private authHttp: HttpClient, private authHttp: HttpClient,
private auth: AuthService,
private restExtractor: RestExtractor, private restExtractor: RestExtractor,
private restService: RestService, private restService: RestService,
private userNotificationSocket: UserNotificationSocket private userNotificationSocket: UserNotificationSocket
@ -84,6 +85,6 @@ export class UserNotificationService {
} }
private formatNotification (notification: UserNotificationServer) { private formatNotification (notification: UserNotificationServer) {
return new UserNotification(notification) return new UserNotification(notification, this.auth.getUser())
} }
} }

View File

@ -46,20 +46,38 @@
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
<div class="message" *ngIf="notification.videoUrl" i18n> <div class="message" *ngIf="notification.videoUrl" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a> <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a>
</div> </div>
<div class="message" *ngIf="notification.commentUrl" i18n> <div class="message" *ngIf="notification.commentUrl" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new comment abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.abuse.comment.video.name }}</a> <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new comment abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.abuse.comment.video.name }}</a>
</div> </div>
<div class="message" *ngIf="notification.accountUrl" i18n> <div class="message" *ngIf="notification.accountUrl" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new account abuse</a> has been created on account <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.abuse.account.displayName }}</a> <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new account abuse</a> has been created on account <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.abuse.account.displayName }}</a>
</div> </div>
<!-- Deleted entity associated to the abuse --> <!-- Deleted entity associated to the abuse -->
<div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n> <div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new abuse</a> has been created <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new abuse</a> has been created
</div>
</ng-container>
<ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE">
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
<div class="message" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">Your abuse {{ notification.abuse.id }}</a> has been
<ng-container *ngIf="isAccepted(notification)">accepted</ng-container>
<ng-container *ngIf="!isAccepted(notification)">rejected</ng-container>
</div>
</ng-container>
<ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE">
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
<div class="message" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">Abuse {{ notification.abuse.id }}</a> has a new message
</div> </div>
</ng-container> </ng-container>

View File

@ -1,7 +1,7 @@
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { ComponentPagination, hasMoreItems, Notifier } from '@app/core' import { ComponentPagination, hasMoreItems, Notifier } from '@app/core'
import { UserNotificationType } from '@shared/models' import { UserNotificationType, AbuseState } from '@shared/models'
import { UserNotification } from './user-notification.model' import { UserNotification } from './user-notification.model'
import { UserNotificationService } from './user-notification.service' import { UserNotificationService } from './user-notification.service'
@ -116,4 +116,8 @@ export class UserNotificationsComponent implements OnInit {
this.sortField = column this.sortField = column
this.loadNotifications(true) this.loadNotifications(true)
} }
isAccepted (notification: UserNotification) {
return notification.abuse.state === AbuseState.ACCEPTED
}
} }

View File

@ -21,6 +21,7 @@ cp "./tsconfig.json" "./dist"
npm run tsc -- --incremental --sourceMap npm run tsc -- --incremental --sourceMap
cp -r ./server/static ./server/assets ./dist/server cp -r ./server/static ./server/assets ./dist/server
cp -r "./server/lib/emails" "./dist/server/lib"
NODE_ENV=test node node_modules/.bin/concurrently -k \ NODE_ENV=test node node_modules/.bin/concurrently -k \
"node_modules/.bin/nodemon --delay 1 --watch ./dist dist/server" \ "node_modules/.bin/nodemon --delay 1 --watch ./dist dist/server" \

View File

@ -23,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 520 const LAST_MIGRATION_VERSION = 525
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,54 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
await utils.sequelize.query(`
CREATE TABLE IF NOT EXISTS "abuseMessage" (
"id" serial,
"message" text NOT NULL,
"byModerator" boolean NOT NULL,
"accountId" integer REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
"abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"createdAt" timestamp WITH time zone NOT NULL,
"updatedAt" timestamp WITH time zone NOT NULL,
PRIMARY KEY ("id")
);
`)
const notificationSettingColumns = [ 'abuseStateChange', 'abuseNewMessage' ]
for (const column of notificationSettingColumns) {
const data = {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('userNotificationSetting', column, data)
}
{
const query = 'UPDATE "userNotificationSetting" SET "abuseStateChange" = 3, "abuseNewMessage" = 3'
await utils.sequelize.query(query)
}
for (const column of notificationSettingColumns) {
const data = {
type: Sequelize.INTEGER,
defaultValue: null,
allowNull: false
}
await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -11,7 +11,7 @@ import { isTestInstance, root } from '../helpers/core-utils'
import { bunyanLogger, logger } from '../helpers/logger' import { bunyanLogger, logger } from '../helpers/logger'
import { CONFIG, isEmailEnabled } from '../initializers/config' import { CONFIG, isEmailEnabled } from '../initializers/config'
import { WEBSERVER } from '../initializers/constants' import { WEBSERVER } from '../initializers/constants'
import { MAbuseFull, MAbuseMessage, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
import { JobQueue } from './job-queue' import { JobQueue } from './job-queue'
@ -362,9 +362,11 @@ class Emailer {
? 'Report #' + abuse.id + ' has been accepted' ? 'Report #' + abuse.id + ' has been accepted'
: 'Report #' + abuse.id + ' has been rejected' : 'Report #' + abuse.id + ' has been rejected'
const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
const action = { const action = {
text, text,
url: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id url: abuseUrl
} }
const emailPayload: EmailPayload = { const emailPayload: EmailPayload = {
@ -374,6 +376,7 @@ class Emailer {
locals: { locals: {
action, action,
abuseId: abuse.id, abuseId: abuse.id,
abuseUrl,
isAccepted: abuse.state === AbuseState.ACCEPTED isAccepted: abuse.state === AbuseState.ACCEPTED
} }
} }
@ -381,15 +384,24 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
} }
addAbuseNewMessageNotification (to: string[], options: { target: 'moderator' | 'reporter', abuse: MAbuseFull, message: MAbuseMessage }) { addAbuseNewMessageNotification (
const { abuse, target, message } = options to: string[],
options: {
target: 'moderator' | 'reporter'
abuse: MAbuseFull
message: MAbuseMessage
accountMessage: MAccountDefault
}) {
const { abuse, target, message, accountMessage } = options
const text = 'New message on report #' + abuse.id
const abuseUrl = target === 'moderator'
? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
const text = 'New message on abuse #' + abuse.id
const action = { const action = {
text, text,
url: target === 'moderator' url: abuseUrl
? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
} }
const emailPayload: EmailPayload = { const emailPayload: EmailPayload = {
@ -397,7 +409,9 @@ class Emailer {
to, to,
subject: text, subject: text,
locals: { locals: {
abuseId: abuse.id,
abuseUrl: action.url, abuseUrl: action.url,
messageAccountName: accountMessage.getDisplayName(),
messageText: message.message, messageText: message.message,
action action
} }

View File

@ -2,10 +2,10 @@ extends ../common/greetings
include ../common/mixins.pug include ../common/mixins.pug
block title block title
| New abuse message | New message on abuse report
block content block content
p p
| A new message was created on #[a(href=WEBSERVER.URL) abuse ##{abuseId} on #{WEBSERVER.HOST}] | A new message by #{messageAccountName} was posted on #[a(href=abuseUrl) abuse report ##{abuseId}] on #{WEBSERVER.HOST}
blockquote #{messageText} blockquote #{messageText}
br(style="display: none;") br(style="display: none;")

View File

@ -2,8 +2,8 @@ extends ../common/greetings
include ../common/mixins.pug include ../common/mixins.pug
block title block title
| Abuse state changed | Abuse report state changed
block content block content
p p
| #[a(href=abuseUrl) Your abuse ##{abuseId} on #{WEBSERVER.HOST}] has been #{isAccepted ? 'accepted' : 'rejected'} | #[a(href=abuseUrl) Your abuse report ##{abuseId}] on #{WEBSERVER.HOST} has been #{isAccepted ? 'accepted' : 'rejected'}

View File

@ -24,6 +24,7 @@ import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../type
import { isBlockedByServerOrAccount } from './blocklist' import { isBlockedByServerOrAccount } from './blocklist'
import { Emailer } from './emailer' import { Emailer } from './emailer'
import { PeerTubeSocket } from './peertube-socket' import { PeerTubeSocket } from './peertube-socket'
import { AccountModel } from '@server/models/account/account'
class Notifier { class Notifier {
@ -137,7 +138,7 @@ class Notifier {
}) })
} }
notifyOnAbuseMessage (abuse: MAbuseFull, message: AbuseMessageModel): void { notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void {
this.notifyOfNewAbuseMessage(abuse, message) this.notifyOfNewAbuseMessage(abuse, message)
.catch(err => { .catch(err => {
logger.error('Cannot notify on new abuse %d message.', abuse.id, { err }) logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })
@ -436,6 +437,8 @@ class Notifier {
const url = this.getAbuseUrl(abuse) const url = this.getAbuseUrl(abuse)
logger.info('Notifying reporter and moderators of new abuse message on %s.', url) logger.info('Notifying reporter and moderators of new abuse message on %s.', url)
const accountMessage = await AccountModel.load(message.accountId)
function settingGetter (user: MUserWithNotificationSetting) { function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.abuseNewMessage return user.NotificationSetting.abuseNewMessage
} }
@ -452,11 +455,11 @@ class Notifier {
} }
function emailSenderReporter (emails: string[]) { function emailSenderReporter (emails: string[]) {
return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message }) return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message, accountMessage })
} }
function emailSenderModerators (emails: string[]) { function emailSenderModerators (emails: string[]) {
return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message }) return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message, accountMessage })
} }
async function buildReporterOptions () { async function buildReporterOptions () {