aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin/moderation
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+admin/moderation')
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.html18
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts358
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss11
3 files changed, 251 insertions, 136 deletions
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
index 1ad73e38a..99502304d 100644
--- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
@@ -1,6 +1,6 @@
1<p-table 1<p-table
2 [value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" 2 [value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true" [lazyLoadOnInit]="false"
4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports" 5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
6 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows" 6 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
@@ -128,6 +128,22 @@
128 </td> 128 </td>
129 </ng-container> 129 </ng-container>
130 130
131 <ng-container *ngIf="!abuse.comment && !abuse.video">
132 <td *ngIf="abuse.flaggedAccount">
133 <a [href]="getAccountUrl(abuse)" class="table-account-link" target="_blank" rel="noopener noreferrer">
134 <span>{{ abuse.flaggedAccount.displayName }}</span>
135
136 <span class="account-flagged-handle">{{ abuse.flaggedAccount.nameWithHostForced }}</span>
137 </a>
138 </td>
139
140 <td i18n *ngIf="!abuse.flaggedAccount">
141 Account deleted
142 </td>
143
144 </ng-container>
145
146
131 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> 147 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
132 148
133 <td class="c-hand abuse-states" [pRowToggler]="abuse"> 149 <td class="c-hand abuse-states" [pRowToggler]="abuse">
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
index 1ea61ed37..74c5fe2b3 100644
--- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
@@ -1,3 +1,5 @@
1import * as debug from 'debug'
2import truncate from 'lodash-es/truncate'
1import { SortMeta } from 'primeng/api' 3import { SortMeta } from 'primeng/api'
2import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' 4import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
3import { environment } from 'src/environments/environment' 5import { environment } from 'src/environments/environment'
@@ -7,11 +9,15 @@ import { ActivatedRoute, Params, Router } from '@angular/router'
7import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' 9import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
8import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' 10import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' 11import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
12import { VideoCommentService } from '@app/shared/shared-video-comment'
10import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
11import { Abuse, AbuseState } from '@shared/models' 14import { Abuse, AbuseState } from '@shared/models'
12import { ModerationCommentModalComponent } from './moderation-comment-modal.component' 15import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
13import truncate from 'lodash-es/truncate'
14 16
17const logger = debug('peertube:moderation:AbuseListComponent')
18
19// Don't use an abuse model because we need external services to compute some properties
20// And this model is only used in this component
15export type ProcessedAbuse = Abuse & { 21export type ProcessedAbuse = Abuse & {
16 moderationCommentHtml?: string, 22 moderationCommentHtml?: string,
17 reasonHtml?: string 23 reasonHtml?: string
@@ -45,12 +51,13 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
45 sort: SortMeta = { field: 'createdAt', order: 1 } 51 sort: SortMeta = { field: 'createdAt', order: 1 }
46 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 52 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
47 53
48 abuseActions: DropdownAction<Abuse>[][] = [] 54 abuseActions: DropdownAction<ProcessedAbuse>[][] = []
49 55
50 constructor ( 56 constructor (
51 private notifier: Notifier, 57 private notifier: Notifier,
52 private abuseService: AbuseService, 58 private abuseService: AbuseService,
53 private blocklistService: BlocklistService, 59 private blocklistService: BlocklistService,
60 private commentService: VideoCommentService,
54 private videoService: VideoService, 61 private videoService: VideoService,
55 private videoBlocklistService: VideoBlockService, 62 private videoBlocklistService: VideoBlockService,
56 private confirmService: ConfirmService, 63 private confirmService: ConfirmService,
@@ -63,140 +70,15 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
63 super() 70 super()
64 71
65 this.abuseActions = [ 72 this.abuseActions = [
66 [ 73 this.buildInternalActions(),
67 {
68 label: this.i18n('Internal actions'),
69 isHeader: true
70 },
71 {
72 label: this.i18n('Delete report'),
73 handler: abuse => this.removeAbuse(abuse)
74 },
75 {
76 label: this.i18n('Add note'),
77 handler: abuse => this.openModerationCommentModal(abuse),
78 isDisplayed: abuse => !abuse.moderationComment
79 },
80 {
81 label: this.i18n('Update note'),
82 handler: abuse => this.openModerationCommentModal(abuse),
83 isDisplayed: abuse => !!abuse.moderationComment
84 },
85 {
86 label: this.i18n('Mark as accepted'),
87 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
88 isDisplayed: abuse => !this.isAbuseAccepted(abuse)
89 },
90 {
91 label: this.i18n('Mark as rejected'),
92 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
93 isDisplayed: abuse => !this.isAbuseRejected(abuse)
94 }
95 ],
96 [
97 {
98 label: this.i18n('Actions for the video'),
99 isHeader: true,
100 isDisplayed: abuse => abuse.video && !abuse.video.deleted
101 },
102 {
103 label: this.i18n('Block video'),
104 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
105 handler: abuse => {
106 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
107 .subscribe(
108 () => {
109 this.notifier.success(this.i18n('Video blocked.'))
110
111 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
112 },
113
114 err => this.notifier.error(err.message)
115 )
116 }
117 },
118 {
119 label: this.i18n('Unblock video'),
120 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
121 handler: abuse => {
122 this.videoBlocklistService.unblockVideo(abuse.video.id)
123 .subscribe(
124 () => {
125 this.notifier.success(this.i18n('Video unblocked.'))
126
127 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
128 },
129
130 err => this.notifier.error(err.message)
131 )
132 }
133 },
134 {
135 label: this.i18n('Delete video'),
136 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
137 handler: async abuse => {
138 const res = await this.confirmService.confirm(
139 this.i18n('Do you really want to delete this video?'),
140 this.i18n('Delete')
141 )
142 if (res === false) return
143 74
144 this.videoService.removeVideo(abuse.video.id) 75 this.buildFlaggedAccountActions(),
145 .subscribe(
146 () => {
147 this.notifier.success(this.i18n('Video deleted.'))
148 76
149 this.updateAbuseState(abuse, AbuseState.ACCEPTED) 77 this.buildCommentActions(),
150 },
151 78
152 err => this.notifier.error(err.message) 79 this.buildVideoActions(),
153 ) 80
154 } 81 this.buildAccountActions()
155 }
156 ],
157 [
158 {
159 label: this.i18n('Actions for the reporter'),
160 isHeader: true,
161 isDisplayed: abuse => !!abuse.reporterAccount
162 },
163 {
164 label: this.i18n('Mute reporter'),
165 isDisplayed: abuse => !!abuse.reporterAccount,
166 handler: async abuse => {
167 const account = abuse.reporterAccount as Account
168
169 this.blocklistService.blockAccountByInstance(account)
170 .subscribe(
171 () => {
172 this.notifier.success(
173 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
174 )
175
176 account.mutedByInstance = true
177 },
178
179 err => this.notifier.error(err.message)
180 )
181 }
182 },
183 {
184 label: this.i18n('Mute server'),
185 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
186 handler: async abuse => {
187 this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
188 .subscribe(
189 () => {
190 this.notifier.success(
191 this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host })
192 )
193 },
194
195 err => this.notifier.error(err.message)
196 )
197 }
198 }
199 ]
200 ] 82 ]
201 } 83 }
202 84
@@ -207,6 +89,8 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
207 .subscribe(params => { 89 .subscribe(params => {
208 this.search = params.search || '' 90 this.search = params.search || ''
209 91
92 logger('On URL change (search: %s).', this.search)
93
210 this.setTableFilter(this.search) 94 this.setTableFilter(this.search)
211 this.loadData() 95 this.loadData()
212 }) 96 })
@@ -264,6 +148,10 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
264 return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId 148 return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
265 } 149 }
266 150
151 getAccountUrl (abuse: ProcessedAbuse) {
152 return '/accounts/' + abuse.flaggedAccount.nameWithHost
153 }
154
267 getVideoEmbed (abuse: Abuse) { 155 getVideoEmbed (abuse: Abuse) {
268 return buildVideoEmbed( 156 return buildVideoEmbed(
269 buildVideoLink({ 157 buildVideoLink({
@@ -304,6 +192,8 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
304 } 192 }
305 193
306 protected loadData () { 194 protected loadData () {
195 logger('Load data.')
196
307 return this.abuseService.getAbuses({ 197 return this.abuseService.getAbuses({
308 pagination: this.pagination, 198 pagination: this.pagination,
309 sort: this.sort, 199 sort: this.sort,
@@ -356,6 +246,208 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
356 ) 246 )
357 } 247 }
358 248
249 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
250 return [
251 {
252 label: this.i18n('Internal actions'),
253 isHeader: true
254 },
255 {
256 label: this.i18n('Delete report'),
257 handler: abuse => this.removeAbuse(abuse)
258 },
259 {
260 label: this.i18n('Add note'),
261 handler: abuse => this.openModerationCommentModal(abuse),
262 isDisplayed: abuse => !abuse.moderationComment
263 },
264 {
265 label: this.i18n('Update note'),
266 handler: abuse => this.openModerationCommentModal(abuse),
267 isDisplayed: abuse => !!abuse.moderationComment
268 },
269 {
270 label: this.i18n('Mark as accepted'),
271 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
272 isDisplayed: abuse => !this.isAbuseAccepted(abuse)
273 },
274 {
275 label: this.i18n('Mark as rejected'),
276 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
277 isDisplayed: abuse => !this.isAbuseRejected(abuse)
278 }
279 ]
280 }
281
282 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
283 return [
284 {
285 label: this.i18n('Actions for the flagged account'),
286 isHeader: true,
287 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
288 },
289
290 {
291 label: this.i18n('Mute account'),
292 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
293 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
294 },
295
296 {
297 label: this.i18n('Mute server account'),
298 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
299 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
300 }
301 ]
302 }
303
304 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
305 return [
306 {
307 label: this.i18n('Actions for the reporter'),
308 isHeader: true,
309 isDisplayed: abuse => !!abuse.reporterAccount
310 },
311
312 {
313 label: this.i18n('Mute reporter'),
314 isDisplayed: abuse => !!abuse.reporterAccount,
315 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
316 },
317
318 {
319 label: this.i18n('Mute server'),
320 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
321 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
322 }
323 ]
324 }
325
326 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
327 return [
328 {
329 label: this.i18n('Actions for the video'),
330 isHeader: true,
331 isDisplayed: abuse => abuse.video && !abuse.video.deleted
332 },
333 {
334 label: this.i18n('Block video'),
335 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
336 handler: abuse => {
337 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
338 .subscribe(
339 () => {
340 this.notifier.success(this.i18n('Video blocked.'))
341
342 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
343 },
344
345 err => this.notifier.error(err.message)
346 )
347 }
348 },
349 {
350 label: this.i18n('Unblock video'),
351 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
352 handler: abuse => {
353 this.videoBlocklistService.unblockVideo(abuse.video.id)
354 .subscribe(
355 () => {
356 this.notifier.success(this.i18n('Video unblocked.'))
357
358 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
359 },
360
361 err => this.notifier.error(err.message)
362 )
363 }
364 },
365 {
366 label: this.i18n('Delete video'),
367 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
368 handler: async abuse => {
369 const res = await this.confirmService.confirm(
370 this.i18n('Do you really want to delete this video?'),
371 this.i18n('Delete')
372 )
373 if (res === false) return
374
375 this.videoService.removeVideo(abuse.video.id)
376 .subscribe(
377 () => {
378 this.notifier.success(this.i18n('Video deleted.'))
379
380 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
381 },
382
383 err => this.notifier.error(err.message)
384 )
385 }
386 }
387 ]
388 }
389
390 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
391 return [
392 {
393 label: this.i18n('Actions for the comment'),
394 isHeader: true,
395 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
396 },
397
398 {
399 label: this.i18n('Delete comment'),
400 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
401 handler: async abuse => {
402 const res = await this.confirmService.confirm(
403 this.i18n('Do you really want to delete this comment?'),
404 this.i18n('Delete')
405 )
406 if (res === false) return
407
408 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
409 .subscribe(
410 () => {
411 this.notifier.success(this.i18n('Comment deleted.'))
412
413 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
414 },
415
416 err => this.notifier.error(err.message)
417 )
418 }
419 }
420 ]
421 }
422
423 private muteAccountHelper (account: Account) {
424 this.blocklistService.blockAccountByInstance(account)
425 .subscribe(
426 () => {
427 this.notifier.success(
428 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
429 )
430
431 account.mutedByInstance = true
432 },
433
434 err => this.notifier.error(err.message)
435 )
436 }
437
438 private muteServerHelper (host: string) {
439 this.blocklistService.blockServerByInstance(host)
440 .subscribe(
441 () => {
442 this.notifier.success(
443 this.i18n('Server {{host}} muted by the instance.', { host: host })
444 )
445 },
446
447 err => this.notifier.error(err.message)
448 )
449 }
450
359 private toHtml (text: string) { 451 private toHtml (text: string) {
360 return this.markdownRenderer.textMarkdownToHTML(text) 452 return this.markdownRenderer.textMarkdownToHTML(text)
361 } 453 }
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss
index f73c71dc5..65fe94d39 100644
--- a/client/src/app/+admin/moderation/moderation.component.scss
+++ b/client/src/app/+admin/moderation/moderation.component.scss
@@ -96,7 +96,8 @@ my-action-dropdown.show {
96 top: 3px; 96 top: 3px;
97} 97}
98 98
99.table-comment-link { 99.table-comment-link,
100.table-account-link {
100 @include disable-outline; 101 @include disable-outline;
101 102
102 color: var(--mainForegroundColor); 103 color: var(--mainForegroundColor);
@@ -106,7 +107,13 @@ my-action-dropdown.show {
106 } 107 }
107} 108}
108 109
109.comment-flagged-account { 110.table-account-link {
111 display: flex;
112 flex-direction: column;
113}
114
115.comment-flagged-account,
116.account-flagged-handle {
110 font-size: 11px; 117 font-size: 11px;
111 color: var(--greyForegroundColor); 118 color: var(--greyForegroundColor);
112} 119}