diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index 8562e564b..89a04c078 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
newFollow: this.i18n('You or your channel(s) has a new follower'),
commentMention: this.i18n('Someone mentioned you in video comments'),
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)[]
diff --git a/client/src/app/core/renderer/html-renderer.service.ts b/client/src/app/core/renderer/html-renderer.service.ts
index f0527c759..302d92ed9 100644
--- a/client/src/app/core/renderer/html-renderer.service.ts
+++ b/client/src/app/core/renderer/html-renderer.service.ts
@@ -3,19 +3,29 @@ import { LinkifierService } from './linkifier.service'
@Injectable()
export class HtmlRendererService {
+ private sanitizeHtml: typeof import ('sanitize-html')
constructor (private linkifier: LinkifierService) {
}
+ async convertToBr (text: string) {
+ await this.loadSanitizeHtml()
+
+ const html = text.replace(/\r?\n/g, '
')
+
+ return this.sanitizeHtml(html, {
+ allowedTags: [ 'br' ]
+ })
+ }
+
async toSafeHtml (text: string) {
- // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
- const sanitizeHtml: typeof import ('sanitize-html') = (await import('sanitize-html') as any).default
+ await this.loadSanitizeHtml()
// Convert possible markdown to html
const html = this.linkifier.linkify(text)
- return sanitizeHtml(html, {
+ return this.sanitizeHtml(html, {
allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
allowedSchemes: [ 'http', 'https' ],
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
+ }
}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
index 17b3742d6..d90b93fff 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
@@ -42,6 +42,7 @@
Created |
State |
Messages |
+ Internal note |
|
@@ -144,13 +145,11 @@
-
{{ abuse.createdAt | date: 'short' }} |
-
|
@@ -161,6 +160,10 @@
|
+
+ {{ abuse.moderationComment }}
+ |
+
this.isLocalAbuse(abuse)
},
{
- label: this.i18n('Update note'),
+ label: this.i18n('Update internal note'),
handler: abuse => this.openModerationCommentModal(abuse),
isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
},
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
index cb965b71d..17e9ce4cf 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
@@ -9,7 +9,7 @@
-
+
{{ message.account.name }}
- {{ message.message }}
+
{{ message.createdAt | date }}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
index 4dd025fc4..4163722dd 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
@@ -20,6 +20,7 @@ textarea {
display: flex;
flex-direction: column;
overflow-y: scroll;
+ max-height: 50vh;
}
.no-messages {
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
index 03f5ad735..6686d91f4 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
@@ -1,5 +1,5 @@
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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'
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 {
@ViewChild('modal', { static: true }) modal: NgbModal
- @ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
@Input() isAdminView: boolean
@Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
- abuseMessages: AbuseMessage[] = []
+ abuseMessages: (AbuseMessage & { messageHtml: string })[] = []
textareaMessage: string
sendingMessage = false
noResults = false
@@ -33,6 +32,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
private abuseValidatorsService: AbuseValidatorsService,
private modalService: NgbModal,
private i18n: I18n,
+ private htmlRenderer: HtmlRendererService,
private auth: AuthService,
private notifier: Notifier,
private abuseService: AbuseService
@@ -108,15 +108,21 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
private loadMessages () {
this.abuseService.listAbuseMessages(this.abuse)
.subscribe(
- res => {
- this.abuseMessages = res.data
+ async res => {
+ 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
setTimeout(() => {
- if (!this.messagesBlock) return
-
- const element = this.messagesBlock.nativeElement as HTMLElement
- element.scrollIntoView({ block: 'end', inline: 'nearest' })
+ // Don't use ViewChild: it is not supported inside a ng-template
+ const messagesBlock = document.querySelector('.messages')
+ messagesBlock.scroll(0, messagesBlock.scrollHeight)
})
},
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index 61b48a806..a068daaac 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -1,5 +1,14 @@
+import {
+ AbuseState,
+ ActorInfo,
+ FollowState,
+ UserNotification as UserNotificationServer,
+ UserNotificationType,
+ VideoInfo,
+ UserRight
+} from '@shared/models'
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 {
id: number
@@ -27,6 +36,7 @@ export class UserNotification implements UserNotificationServer {
abuse?: {
id: number
+ state: AbuseState
video?: VideoInfo
@@ -69,13 +79,14 @@ export class UserNotification implements UserNotificationServer {
videoUrl?: string
commentUrl?: any[]
abuseUrl?: string
+ abuseQueryParams?: { [id: string]: string } = {}
videoAutoBlacklistUrl?: string
accountUrl?: string
videoImportIdentifier?: string
videoImportUrl?: string
instanceFollowUrl?: string
- constructor (hash: UserNotificationServer) {
+ constructor (hash: UserNotificationServer, user: AuthUser) {
this.id = hash.id
this.type = hash.type
this.read = hash.read
@@ -122,12 +133,25 @@ export class UserNotification implements UserNotificationServer {
case UserNotificationType.NEW_ABUSE_FOR_MODERATORS:
this.abuseUrl = '/admin/moderation/abuses/list'
+ this.abuseQueryParams.search = '#' + this.abuse.id
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.account) this.accountUrl = this.buildAccountUrl(this.abuse.account)
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:
this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
// Backward compatibility where we did not assign videoBlacklist to this type of notification before
diff --git a/client/src/app/shared/shared-main/users/user-notification.service.ts b/client/src/app/shared/shared-main/users/user-notification.service.ts
index ecc66ecdb..7b9dc34be 100644
--- a/client/src/app/shared/shared-main/users/user-notification.service.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.service.ts
@@ -1,7 +1,7 @@
import { catchError, map, tap } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http'
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 { environment } from '../../../../environments/environment'
import { UserNotification } from './user-notification.model'
@@ -14,6 +14,7 @@ export class UserNotificationService {
constructor (
private authHttp: HttpClient,
+ private auth: AuthService,
private restExtractor: RestExtractor,
private restService: RestService,
private userNotificationSocket: UserNotificationSocket
@@ -84,6 +85,6 @@ export class UserNotificationService {
}
private formatNotification (notification: UserNotificationServer) {
- return new UserNotification(notification)
+ return new UserNotification(notification, this.auth.getUser())
}
}
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 8127ae979..a56a0859b 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -46,20 +46,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts
index 7518dbdd0..387c49d94 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.ts
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts
@@ -1,7 +1,7 @@
import { Subject } from 'rxjs'
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/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 { UserNotificationService } from './user-notification.service'
@@ -116,4 +116,8 @@ export class UserNotificationsComponent implements OnInit {
this.sortField = column
this.loadNotifications(true)
}
+
+ isAccepted (notification: UserNotification) {
+ return notification.abuse.state === AbuseState.ACCEPTED
+ }
}
diff --git a/scripts/dev/server.sh b/scripts/dev/server.sh
index 680ca3d79..5aac470eb 100755
--- a/scripts/dev/server.sh
+++ b/scripts/dev/server.sh
@@ -21,6 +21,7 @@ cp "./tsconfig.json" "./dist"
npm run tsc -- --incremental --sourceMap
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_modules/.bin/nodemon --delay 1 --watch ./dist dist/server" \
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index a40a22395..ca6c2a7ff 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -23,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 520
+const LAST_MIGRATION_VERSION = 525
// ---------------------------------------------------------------------------
diff --git a/server/initializers/migrations/0525-abuse-messages.ts b/server/initializers/migrations/0525-abuse-messages.ts
new file mode 100644
index 000000000..c8fd7cbcf
--- /dev/null
+++ b/server/initializers/migrations/0525-abuse-messages.ts
@@ -0,0 +1,54 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+}): Promise {
+ 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
+}
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 9c49aa2f6..25b0aaedd 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -11,7 +11,7 @@ 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, 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 { JobQueue } from './job-queue'
@@ -362,9 +362,11 @@ class Emailer {
? 'Report #' + abuse.id + ' has been accepted'
: 'Report #' + abuse.id + ' has been rejected'
+ const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
+
const action = {
text,
- url: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
+ url: abuseUrl
}
const emailPayload: EmailPayload = {
@@ -374,6 +376,7 @@ class Emailer {
locals: {
action,
abuseId: abuse.id,
+ abuseUrl,
isAccepted: abuse.state === AbuseState.ACCEPTED
}
}
@@ -381,15 +384,24 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
- addAbuseNewMessageNotification (to: string[], options: { target: 'moderator' | 'reporter', abuse: MAbuseFull, message: MAbuseMessage }) {
- const { abuse, target, message } = options
+ addAbuseNewMessageNotification (
+ 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 = {
text,
- url: target === 'moderator'
- ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
- : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
+ url: abuseUrl
}
const emailPayload: EmailPayload = {
@@ -397,7 +409,9 @@ class Emailer {
to,
subject: text,
locals: {
+ abuseId: abuse.id,
abuseUrl: action.url,
+ messageAccountName: accountMessage.getDisplayName(),
messageText: message.message,
action
}
diff --git a/server/lib/emails/abuse-new-message/html.pug b/server/lib/emails/abuse-new-message/html.pug
index a4180aba1..0841775d2 100644
--- a/server/lib/emails/abuse-new-message/html.pug
+++ b/server/lib/emails/abuse-new-message/html.pug
@@ -2,10 +2,10 @@ extends ../common/greetings
include ../common/mixins.pug
block title
- | New abuse message
+ | New message on abuse report
block content
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}
br(style="display: none;")
diff --git a/server/lib/emails/abuse-state-change/html.pug b/server/lib/emails/abuse-state-change/html.pug
index a94c8521d..ca89a2f05 100644
--- a/server/lib/emails/abuse-state-change/html.pug
+++ b/server/lib/emails/abuse-state-change/html.pug
@@ -2,8 +2,8 @@ extends ../common/greetings
include ../common/mixins.pug
block title
- | Abuse state changed
+ | Abuse report state changed
block content
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'}
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 5c50fcf01..9c2f16c27 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -24,6 +24,7 @@ import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../type
import { isBlockedByServerOrAccount } from './blocklist'
import { Emailer } from './emailer'
import { PeerTubeSocket } from './peertube-socket'
+import { AccountModel } from '@server/models/account/account'
class Notifier {
@@ -137,7 +138,7 @@ class Notifier {
})
}
- notifyOnAbuseMessage (abuse: MAbuseFull, message: AbuseMessageModel): void {
+ notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void {
this.notifyOfNewAbuseMessage(abuse, message)
.catch(err => {
logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })
@@ -436,6 +437,8 @@ class Notifier {
const url = this.getAbuseUrl(abuse)
logger.info('Notifying reporter and moderators of new abuse message on %s.', url)
+ const accountMessage = await AccountModel.load(message.accountId)
+
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.abuseNewMessage
}
@@ -452,11 +455,11 @@ class Notifier {
}
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[]) {
- return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message })
+ return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message, accountMessage })
}
async function buildReporterOptions () {
|