aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-07-28 09:57:16 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-07-31 11:35:19 +0200
commitd573926e9b94fb19c8f51c53f71fc853182e1761 (patch)
tree907cc81c7275efe30aa90047c0763a7254bd1063
parent594d3e48d8a887bbf48ce4cc594c1c36c9640fb1 (diff)
downloadPeerTube-d573926e9b94fb19c8f51c53f71fc853182e1761.tar.gz
PeerTube-d573926e9b94fb19c8f51c53f71fc853182e1761.tar.zst
PeerTube-d573926e9b94fb19c8f51c53f71fc853182e1761.zip
Add migrations for abuse messages
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts4
-rw-r--r--client/src/app/core/renderer/html-renderer.service.ts21
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.html7
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts2
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html4
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss1
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts24
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts28
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.service.ts5
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html26
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.ts6
-rwxr-xr-xscripts/dev/server.sh1
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0525-abuse-messages.ts54
-rw-r--r--server/lib/emailer.ts30
-rw-r--r--server/lib/emails/abuse-new-message/html.pug4
-rw-r--r--server/lib/emails/abuse-state-change/html.pug4
-rw-r--r--server/lib/notifier.ts9
18 files changed, 189 insertions, 43 deletions
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 {
42 newFollow: this.i18n('You or your channel(s) has a new follower'), 42 newFollow: this.i18n('You or your channel(s) has a new follower'),
43 commentMention: this.i18n('Someone mentioned you in video comments'), 43 commentMention: this.i18n('Someone mentioned you in video comments'),
44 newInstanceFollower: this.i18n('Your instance has a new follower'), 44 newInstanceFollower: this.i18n('Your instance has a new follower'),
45 autoInstanceFollowing: this.i18n('Your instance auto followed another instance') 45 autoInstanceFollowing: this.i18n('Your instance auto followed another instance'),
46 abuseNewMessage: this.i18n('An abuse received a new message'),
47 abuseStateChange: this.i18n('One of your abuse has been accepted or rejected by moderators')
46 } 48 }
47 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] 49 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
48 50
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'
3 3
4@Injectable() 4@Injectable()
5export class HtmlRendererService { 5export class HtmlRendererService {
6 private sanitizeHtml: typeof import ('sanitize-html')
6 7
7 constructor (private linkifier: LinkifierService) { 8 constructor (private linkifier: LinkifierService) {
8 9
9 } 10 }
10 11
12 async convertToBr (text: string) {
13 await this.loadSanitizeHtml()
14
15 const html = text.replace(/\r?\n/g, '<br />')
16
17 return this.sanitizeHtml(html, {
18 allowedTags: [ 'br' ]
19 })
20 }
21
11 async toSafeHtml (text: string) { 22 async toSafeHtml (text: string) {
12 // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function 23 await this.loadSanitizeHtml()
13 const sanitizeHtml: typeof import ('sanitize-html') = (await import('sanitize-html') as any).default
14 24
15 // Convert possible markdown to html 25 // Convert possible markdown to html
16 const html = this.linkifier.linkify(text) 26 const html = this.linkifier.linkify(text)
17 27
18 return sanitizeHtml(html, { 28 return this.sanitizeHtml(html, {
19 allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ], 29 allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
20 allowedSchemes: [ 'http', 'https' ], 30 allowedSchemes: [ 'http', 'https' ],
21 allowedAttributes: { 31 allowedAttributes: {
@@ -37,4 +47,9 @@ export class HtmlRendererService {
37 } 47 }
38 }) 48 })
39 } 49 }
50
51 private async loadSanitizeHtml () {
52 // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
53 this.sanitizeHtml = (await import('sanitize-html') as any).default
54 }
40} 55}
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 @@
42 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 42 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
43 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> 43 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
44 <th i18n style="width: 80px;">Messages</th> 44 <th i18n style="width: 80px;">Messages</th>
45 <th i18n *ngIf="isAdminView()" style="width: 100px;">Internal note</th>
45 <th style="width: 150px;"></th> 46 <th style="width: 150px;"></th>
46 </tr> 47 </tr>
47 </ng-template> 48 </ng-template>
@@ -144,13 +145,11 @@
144 145
145 </ng-container> 146 </ng-container>
146 147
147
148 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> 148 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
149 149
150 <td class="c-hand abuse-states" [pRowToggler]="abuse"> 150 <td class="c-hand abuse-states" [pRowToggler]="abuse">
151 <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span> 151 <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
152 <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span> 152 <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
153 <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
154 </td> 153 </td>
155 154
156 <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)"> 155 <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
@@ -161,6 +160,10 @@
161 </ng-container> 160 </ng-container>
162 </td> 161 </td>
163 162
163 <td *ngIf="isAdminView()" class="internal-note" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment">
164 {{ abuse.moderationComment }}
165 </td>
166
164 <td class="action-cell"> 167 <td class="action-cell">
165 <my-action-dropdown 168 <my-action-dropdown
166 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" 169 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index 1d17c9ec9..21d2ea47d 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -278,7 +278,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
278 isDisplayed: abuse => this.isLocalAbuse(abuse) 278 isDisplayed: abuse => this.isLocalAbuse(abuse)
279 }, 279 },
280 { 280 {
281 label: this.i18n('Update note'), 281 label: this.i18n('Update internal note'),
282 handler: abuse => this.openModerationCommentModal(abuse), 282 handler: abuse => this.openModerationCommentModal(abuse),
283 isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment 283 isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
284 }, 284 },
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 @@
9 </div> 9 </div>
10 10
11 <div class="modal-body"> 11 <div class="modal-body">
12 <div class="messages" #messagesBlock> 12 <div class="messages">
13 <div 13 <div
14 *ngFor="let message of abuseMessages" 14 *ngFor="let message of abuseMessages"
15 class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }" 15 class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }"
@@ -18,7 +18,7 @@
18 <div class="author">{{ message.account.name }}</div> 18 <div class="author">{{ message.account.name }}</div>
19 19
20 <div class="bubble"> 20 <div class="bubble">
21 <div class="content">{{ message.message }}</div> 21 <div class="content" [innerHTML]="message.messageHtml"></div>
22 <div class="date">{{ message.createdAt | date }}</div> 22 <div class="date">{{ message.createdAt | date }}</div>
23 </div> 23 </div>
24 </div> 24 </div>
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 {
20 display: flex; 20 display: flex;
21 flex-direction: column; 21 flex-direction: column;
22 overflow-y: scroll; 22 overflow-y: scroll;
23 max-height: 50vh;
23} 24}
24 25
25.no-messages { 26.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 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { AuthService, Notifier } from '@app/core' 2import { AuthService, Notifier, HtmlRendererService } from '@app/core'
3import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -14,13 +14,12 @@ import { AbuseService } from '../shared-moderation'
14}) 14})
15export class AbuseMessageModalComponent extends FormReactive implements OnInit { 15export class AbuseMessageModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal 16 @ViewChild('modal', { static: true }) modal: NgbModal
17 @ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
18 17
19 @Input() isAdminView: boolean 18 @Input() isAdminView: boolean
20 19
21 @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>() 20 @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
22 21
23 abuseMessages: AbuseMessage[] = [] 22 abuseMessages: (AbuseMessage & { messageHtml: string })[] = []
24 textareaMessage: string 23 textareaMessage: string
25 sendingMessage = false 24 sendingMessage = false
26 noResults = false 25 noResults = false
@@ -33,6 +32,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
33 private abuseValidatorsService: AbuseValidatorsService, 32 private abuseValidatorsService: AbuseValidatorsService,
34 private modalService: NgbModal, 33 private modalService: NgbModal,
35 private i18n: I18n, 34 private i18n: I18n,
35 private htmlRenderer: HtmlRendererService,
36 private auth: AuthService, 36 private auth: AuthService,
37 private notifier: Notifier, 37 private notifier: Notifier,
38 private abuseService: AbuseService 38 private abuseService: AbuseService
@@ -108,15 +108,21 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
108 private loadMessages () { 108 private loadMessages () {
109 this.abuseService.listAbuseMessages(this.abuse) 109 this.abuseService.listAbuseMessages(this.abuse)
110 .subscribe( 110 .subscribe(
111 res => { 111 async res => {
112 this.abuseMessages = res.data 112 this.abuseMessages = []
113
114 for (const m of res.data) {
115 this.abuseMessages.push(Object.assign(m, {
116 messageHtml: await this.htmlRenderer.convertToBr(m.message)
117 }))
118 }
119
113 this.noResults = this.abuseMessages.length === 0 120 this.noResults = this.abuseMessages.length === 0
114 121
115 setTimeout(() => { 122 setTimeout(() => {
116 if (!this.messagesBlock) return 123 // Don't use ViewChild: it is not supported inside a ng-template
117 124 const messagesBlock = document.querySelector('.messages')
118 const element = this.messagesBlock.nativeElement as HTMLElement 125 messagesBlock.scroll(0, messagesBlock.scrollHeight)
119 element.scrollIntoView({ block: 'end', inline: 'nearest' })
120 }) 126 })
121 }, 127 },
122 128
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 @@
1import {
2 AbuseState,
3 ActorInfo,
4 FollowState,
5 UserNotification as UserNotificationServer,
6 UserNotificationType,
7 VideoInfo,
8 UserRight
9} from '@shared/models'
1import { Actor } from '../account/actor.model' 10import { Actor } from '../account/actor.model'
2import { ActorInfo, Avatar, FollowState, UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '@shared/models' 11import { AuthUser } from '@app/core'
3 12
4export class UserNotification implements UserNotificationServer { 13export class UserNotification implements UserNotificationServer {
5 id: number 14 id: number
@@ -27,6 +36,7 @@ export class UserNotification implements UserNotificationServer {
27 36
28 abuse?: { 37 abuse?: {
29 id: number 38 id: number
39 state: AbuseState
30 40
31 video?: VideoInfo 41 video?: VideoInfo
32 42
@@ -69,13 +79,14 @@ export class UserNotification implements UserNotificationServer {
69 videoUrl?: string 79 videoUrl?: string
70 commentUrl?: any[] 80 commentUrl?: any[]
71 abuseUrl?: string 81 abuseUrl?: string
82 abuseQueryParams?: { [id: string]: string } = {}
72 videoAutoBlacklistUrl?: string 83 videoAutoBlacklistUrl?: string
73 accountUrl?: string 84 accountUrl?: string
74 videoImportIdentifier?: string 85 videoImportIdentifier?: string
75 videoImportUrl?: string 86 videoImportUrl?: string
76 instanceFollowUrl?: string 87 instanceFollowUrl?: string
77 88
78 constructor (hash: UserNotificationServer) { 89 constructor (hash: UserNotificationServer, user: AuthUser) {
79 this.id = hash.id 90 this.id = hash.id
80 this.type = hash.type 91 this.type = hash.type
81 this.read = hash.read 92 this.read = hash.read
@@ -122,12 +133,25 @@ export class UserNotification implements UserNotificationServer {
122 133
123 case UserNotificationType.NEW_ABUSE_FOR_MODERATORS: 134 case UserNotificationType.NEW_ABUSE_FOR_MODERATORS:
124 this.abuseUrl = '/admin/moderation/abuses/list' 135 this.abuseUrl = '/admin/moderation/abuses/list'
136 this.abuseQueryParams.search = '#' + this.abuse.id
125 137
126 if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) 138 if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
127 else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment) 139 else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment)
128 else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account) 140 else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account)
129 break 141 break
130 142
143 case UserNotificationType.ABUSE_STATE_CHANGE:
144 this.abuseUrl = '/my-account/abuses'
145 this.abuseQueryParams.search = '#' + this.abuse.id
146 break
147
148 case UserNotificationType.ABUSE_NEW_MESSAGE:
149 this.abuseUrl = user.hasRight(UserRight.MANAGE_ABUSES)
150 ? '/admin/moderation/abuses/list'
151 : '/my-account/abuses'
152 this.abuseQueryParams.search = '#' + this.abuse.id
153 break
154
131 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: 155 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
132 this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list' 156 this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
133 // Backward compatibility where we did not assign videoBlacklist to this type of notification before 157 // 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 @@
1import { catchError, map, tap } from 'rxjs/operators' 1import { catchError, map, tap } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket } from '@app/core' 4import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket, AuthService } from '@app/core'
5import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models' 5import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models'
6import { environment } from '../../../../environments/environment' 6import { environment } from '../../../../environments/environment'
7import { UserNotification } from './user-notification.model' 7import { UserNotification } from './user-notification.model'
@@ -14,6 +14,7 @@ export class UserNotificationService {
14 14
15 constructor ( 15 constructor (
16 private authHttp: HttpClient, 16 private authHttp: HttpClient,
17 private auth: AuthService,
17 private restExtractor: RestExtractor, 18 private restExtractor: RestExtractor,
18 private restService: RestService, 19 private restService: RestService,
19 private userNotificationSocket: UserNotificationSocket 20 private userNotificationSocket: UserNotificationSocket
@@ -84,6 +85,6 @@ export class UserNotificationService {
84 } 85 }
85 86
86 private formatNotification (notification: UserNotificationServer) { 87 private formatNotification (notification: UserNotificationServer) {
87 return new UserNotification(notification) 88 return new UserNotification(notification, this.auth.getUser())
88 } 89 }
89} 90}
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 @@
46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
47 47
48 <div class="message" *ngIf="notification.videoUrl" i18n> 48 <div class="message" *ngIf="notification.videoUrl" i18n>
49 <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> 49 <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>
50 </div> 50 </div>
51 51
52 <div class="message" *ngIf="notification.commentUrl" i18n> 52 <div class="message" *ngIf="notification.commentUrl" i18n>
53 <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> 53 <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>
54 </div> 54 </div>
55 55
56 <div class="message" *ngIf="notification.accountUrl" i18n> 56 <div class="message" *ngIf="notification.accountUrl" i18n>
57 <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> 57 <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>
58 </div> 58 </div>
59 59
60 <!-- Deleted entity associated to the abuse --> 60 <!-- Deleted entity associated to the abuse -->
61 <div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n> 61 <div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n>
62 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new abuse</a> has been created 62 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new abuse</a> has been created
63 </div>
64 </ng-container>
65
66 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE">
67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
68
69 <div class="message" i18n>
70 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">Your abuse {{ notification.abuse.id }}</a> has been
71 <ng-container *ngIf="isAccepted(notification)">accepted</ng-container>
72 <ng-container *ngIf="!isAccepted(notification)">rejected</ng-container>
73 </div>
74 </ng-container>
75
76 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE">
77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
78
79 <div class="message" i18n>
80 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">Abuse {{ notification.abuse.id }}</a> has a new message
63 </div> 81 </div>
64 </ng-container> 82 </ng-container>
65 83
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 @@
1import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
2import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 2import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
3import { ComponentPagination, hasMoreItems, Notifier } from '@app/core' 3import { ComponentPagination, hasMoreItems, Notifier } from '@app/core'
4import { UserNotificationType } from '@shared/models' 4import { UserNotificationType, AbuseState } from '@shared/models'
5import { UserNotification } from './user-notification.model' 5import { UserNotification } from './user-notification.model'
6import { UserNotificationService } from './user-notification.service' 6import { UserNotificationService } from './user-notification.service'
7 7
@@ -116,4 +116,8 @@ export class UserNotificationsComponent implements OnInit {
116 this.sortField = column 116 this.sortField = column
117 this.loadNotifications(true) 117 this.loadNotifications(true)
118 } 118 }
119
120 isAccepted (notification: UserNotification) {
121 return notification.abuse.state === AbuseState.ACCEPTED
122 }
119} 123}
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"
21 21
22npm run tsc -- --incremental --sourceMap 22npm run tsc -- --incremental --sourceMap
23cp -r ./server/static ./server/assets ./dist/server 23cp -r ./server/static ./server/assets ./dist/server
24cp -r "./server/lib/emails" "./dist/server/lib"
24 25
25NODE_ENV=test node node_modules/.bin/concurrently -k \ 26NODE_ENV=test node node_modules/.bin/concurrently -k \
26 "node_modules/.bin/nodemon --delay 1 --watch ./dist dist/server" \ 27 "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'
23 23
24// --------------------------------------------------------------------------- 24// ---------------------------------------------------------------------------
25 25
26const LAST_MIGRATION_VERSION = 520 26const LAST_MIGRATION_VERSION = 525
27 27
28// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
29 29
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 await utils.sequelize.query(`
9 CREATE TABLE IF NOT EXISTS "abuseMessage" (
10 "id" serial,
11 "message" text NOT NULL,
12 "byModerator" boolean NOT NULL,
13 "accountId" integer REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
14 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
15 "createdAt" timestamp WITH time zone NOT NULL,
16 "updatedAt" timestamp WITH time zone NOT NULL,
17 PRIMARY KEY ("id")
18 );
19 `)
20
21 const notificationSettingColumns = [ 'abuseStateChange', 'abuseNewMessage' ]
22
23 for (const column of notificationSettingColumns) {
24 const data = {
25 type: Sequelize.INTEGER,
26 defaultValue: null,
27 allowNull: true
28 }
29 await utils.queryInterface.addColumn('userNotificationSetting', column, data)
30 }
31
32 {
33 const query = 'UPDATE "userNotificationSetting" SET "abuseStateChange" = 3, "abuseNewMessage" = 3'
34 await utils.sequelize.query(query)
35 }
36
37 for (const column of notificationSettingColumns) {
38 const data = {
39 type: Sequelize.INTEGER,
40 defaultValue: null,
41 allowNull: false
42 }
43 await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
44 }
45}
46
47function down (options) {
48 throw new Error('Not implemented.')
49}
50
51export {
52 up,
53 down
54}
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'
11import { bunyanLogger, logger } from '../helpers/logger' 11import { bunyanLogger, logger } from '../helpers/logger'
12import { CONFIG, isEmailEnabled } from '../initializers/config' 12import { CONFIG, isEmailEnabled } from '../initializers/config'
13import { WEBSERVER } from '../initializers/constants' 13import { WEBSERVER } from '../initializers/constants'
14import { MAbuseFull, MAbuseMessage, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' 14import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' 15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
16import { JobQueue } from './job-queue' 16import { JobQueue } from './job-queue'
17 17
@@ -362,9 +362,11 @@ class Emailer {
362 ? 'Report #' + abuse.id + ' has been accepted' 362 ? 'Report #' + abuse.id + ' has been accepted'
363 : 'Report #' + abuse.id + ' has been rejected' 363 : 'Report #' + abuse.id + ' has been rejected'
364 364
365 const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
366
365 const action = { 367 const action = {
366 text, 368 text,
367 url: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id 369 url: abuseUrl
368 } 370 }
369 371
370 const emailPayload: EmailPayload = { 372 const emailPayload: EmailPayload = {
@@ -374,6 +376,7 @@ class Emailer {
374 locals: { 376 locals: {
375 action, 377 action,
376 abuseId: abuse.id, 378 abuseId: abuse.id,
379 abuseUrl,
377 isAccepted: abuse.state === AbuseState.ACCEPTED 380 isAccepted: abuse.state === AbuseState.ACCEPTED
378 } 381 }
379 } 382 }
@@ -381,15 +384,24 @@ class Emailer {
381 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 384 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
382 } 385 }
383 386
384 addAbuseNewMessageNotification (to: string[], options: { target: 'moderator' | 'reporter', abuse: MAbuseFull, message: MAbuseMessage }) { 387 addAbuseNewMessageNotification (
385 const { abuse, target, message } = options 388 to: string[],
389 options: {
390 target: 'moderator' | 'reporter'
391 abuse: MAbuseFull
392 message: MAbuseMessage
393 accountMessage: MAccountDefault
394 }) {
395 const { abuse, target, message, accountMessage } = options
396
397 const text = 'New message on report #' + abuse.id
398 const abuseUrl = target === 'moderator'
399 ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
400 : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
386 401
387 const text = 'New message on abuse #' + abuse.id
388 const action = { 402 const action = {
389 text, 403 text,
390 url: target === 'moderator' 404 url: abuseUrl
391 ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
392 : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
393 } 405 }
394 406
395 const emailPayload: EmailPayload = { 407 const emailPayload: EmailPayload = {
@@ -397,7 +409,9 @@ class Emailer {
397 to, 409 to,
398 subject: text, 410 subject: text,
399 locals: { 411 locals: {
412 abuseId: abuse.id,
400 abuseUrl: action.url, 413 abuseUrl: action.url,
414 messageAccountName: accountMessage.getDisplayName(),
401 messageText: message.message, 415 messageText: message.message,
402 action 416 action
403 } 417 }
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
2include ../common/mixins.pug 2include ../common/mixins.pug
3 3
4block title 4block title
5 | New abuse message 5 | New message on abuse report
6 6
7block content 7block content
8 p 8 p
9 | A new message was created on #[a(href=WEBSERVER.URL) abuse ##{abuseId} on #{WEBSERVER.HOST}] 9 | A new message by #{messageAccountName} was posted on #[a(href=abuseUrl) abuse report ##{abuseId}] on #{WEBSERVER.HOST}
10 blockquote #{messageText} 10 blockquote #{messageText}
11 br(style="display: none;") 11 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
2include ../common/mixins.pug 2include ../common/mixins.pug
3 3
4block title 4block title
5 | Abuse state changed 5 | Abuse report state changed
6 6
7block content 7block content
8 p 8 p
9 | #[a(href=abuseUrl) Your abuse ##{abuseId} on #{WEBSERVER.HOST}] has been #{isAccepted ? 'accepted' : 'rejected'} 9 | #[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
24import { isBlockedByServerOrAccount } from './blocklist' 24import { isBlockedByServerOrAccount } from './blocklist'
25import { Emailer } from './emailer' 25import { Emailer } from './emailer'
26import { PeerTubeSocket } from './peertube-socket' 26import { PeerTubeSocket } from './peertube-socket'
27import { AccountModel } from '@server/models/account/account'
27 28
28class Notifier { 29class Notifier {
29 30
@@ -137,7 +138,7 @@ class Notifier {
137 }) 138 })
138 } 139 }
139 140
140 notifyOnAbuseMessage (abuse: MAbuseFull, message: AbuseMessageModel): void { 141 notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void {
141 this.notifyOfNewAbuseMessage(abuse, message) 142 this.notifyOfNewAbuseMessage(abuse, message)
142 .catch(err => { 143 .catch(err => {
143 logger.error('Cannot notify on new abuse %d message.', abuse.id, { err }) 144 logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })
@@ -436,6 +437,8 @@ class Notifier {
436 const url = this.getAbuseUrl(abuse) 437 const url = this.getAbuseUrl(abuse)
437 logger.info('Notifying reporter and moderators of new abuse message on %s.', url) 438 logger.info('Notifying reporter and moderators of new abuse message on %s.', url)
438 439
440 const accountMessage = await AccountModel.load(message.accountId)
441
439 function settingGetter (user: MUserWithNotificationSetting) { 442 function settingGetter (user: MUserWithNotificationSetting) {
440 return user.NotificationSetting.abuseNewMessage 443 return user.NotificationSetting.abuseNewMessage
441 } 444 }
@@ -452,11 +455,11 @@ class Notifier {
452 } 455 }
453 456
454 function emailSenderReporter (emails: string[]) { 457 function emailSenderReporter (emails: string[]) {
455 return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message }) 458 return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message, accountMessage })
456 } 459 }
457 460
458 function emailSenderModerators (emails: string[]) { 461 function emailSenderModerators (emails: string[]) {
459 return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message }) 462 return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message, accountMessage })
460 } 463 }
461 464
462 async function buildReporterOptions () { 465 async function buildReporterOptions () {