aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-07-27 11:40:30 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-07-31 11:35:19 +0200
commit94148c9028829b5576a5dcbfba2c7fb9cf6443d3 (patch)
tree2774f272329111abd03e8441ff936da11fb1a3f3 /client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
parent441e453ae53e491b09c9b09b00b041788176ce64 (diff)
downloadPeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.tar.gz
PeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.tar.zst
PeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.zip
Add abuse messages management in my account
Diffstat (limited to 'client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts')
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts470
1 files changed, 3 insertions, 467 deletions
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 86121fe58..85a150de9 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,474 +1,10 @@
1import * as debug from 'debug' 1import { Component } from '@angular/core'
2import truncate from 'lodash-es/truncate'
3import { SortMeta } from 'primeng/api'
4import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
5import { environment } from 'src/environments/environment'
6import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
7import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
8import { ActivatedRoute, Params, Router } from '@angular/router'
9import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
10import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
11import { AbuseService, BlocklistService, VideoBlockService, AbuseMessageModalComponent } from '@app/shared/shared-moderation'
12import { VideoCommentService } from '@app/shared/shared-video-comment'
13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { AdminAbuse, AbuseState } from '@shared/models'
15import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
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
21export type ProcessedAbuse = AdminAbuse & {
22 moderationCommentHtml?: string,
23 reasonHtml?: string
24 embedHtml?: SafeHtml
25 updatedAt?: Date
26
27 // override bare server-side definitions with rich client-side definitions
28 reporterAccount?: Account
29 flaggedAccount?: Account
30
31 truncatedCommentHtml?: string
32 commentHtml?: string
33
34 video: AdminAbuse['video'] & {
35 channel: AdminAbuse['video']['channel'] & {
36 ownerAccount: Account
37 }
38 }
39}
40 2
41@Component({ 3@Component({
42 selector: 'my-abuse-list', 4 selector: 'my-abuse-list',
43 templateUrl: './abuse-list.component.html', 5 templateUrl: './abuse-list.component.html',
44 styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ] 6 styleUrls: [ ]
45}) 7})
46export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { 8export class AbuseListComponent {
47 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
48 @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
49
50 abuses: ProcessedAbuse[] = []
51 totalRecords = 0
52 sort: SortMeta = { field: 'createdAt', order: 1 }
53 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
54
55 abuseActions: DropdownAction<ProcessedAbuse>[][] = []
56
57 constructor (
58 private notifier: Notifier,
59 private abuseService: AbuseService,
60 private blocklistService: BlocklistService,
61 private commentService: VideoCommentService,
62 private videoService: VideoService,
63 private videoBlocklistService: VideoBlockService,
64 private confirmService: ConfirmService,
65 private i18n: I18n,
66 private markdownRenderer: MarkdownService,
67 private sanitizer: DomSanitizer,
68 private route: ActivatedRoute,
69 private router: Router
70 ) {
71 super()
72
73 this.abuseActions = [
74 this.buildInternalActions(),
75
76 this.buildFlaggedAccountActions(),
77
78 this.buildCommentActions(),
79
80 this.buildVideoActions(),
81
82 this.buildAccountActions()
83 ]
84 }
85
86 ngOnInit () {
87 this.initialize()
88
89 this.route.queryParams
90 .subscribe(params => {
91 this.search = params.search || ''
92
93 logger('On URL change (search: %s).', this.search)
94
95 this.setTableFilter(this.search)
96 this.loadData()
97 })
98 }
99
100 ngAfterViewInit () {
101 if (this.search) this.setTableFilter(this.search)
102 }
103
104 getIdentifier () {
105 return 'AbuseListComponent'
106 }
107
108 openModerationCommentModal (abuse: AdminAbuse) {
109 this.moderationCommentModal.openModal(abuse)
110 }
111
112 onModerationCommentUpdated () {
113 this.loadData()
114 }
115
116 /* Table filter functions */
117 onAbuseSearch (event: Event) {
118 this.onSearch(event)
119 this.setQueryParams((event.target as HTMLInputElement).value)
120 }
121
122 setQueryParams (search: string) {
123 const queryParams: Params = {}
124 if (search) Object.assign(queryParams, { search })
125
126 this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams })
127 }
128
129 resetTableFilter () {
130 this.setTableFilter('')
131 this.setQueryParams('')
132 this.resetSearch()
133 }
134 /* END Table filter functions */
135
136 isAbuseAccepted (abuse: AdminAbuse) {
137 return abuse.state.id === AbuseState.ACCEPTED
138 }
139
140 isAbuseRejected (abuse: AdminAbuse) {
141 return abuse.state.id === AbuseState.REJECTED
142 }
143
144 getVideoUrl (abuse: AdminAbuse) {
145 return Video.buildClientUrl(abuse.video.uuid)
146 }
147
148 getCommentUrl (abuse: AdminAbuse) {
149 return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
150 }
151
152 getAccountUrl (abuse: ProcessedAbuse) {
153 return '/accounts/' + abuse.flaggedAccount.nameWithHost
154 }
155
156 getVideoEmbed (abuse: AdminAbuse) {
157 return buildVideoEmbed(
158 buildVideoLink({
159 baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
160 title: false,
161 warningTitle: false,
162 startTime: abuse.startAt,
163 stopTime: abuse.endAt
164 })
165 )
166 }
167
168 switchToDefaultAvatar ($event: Event) {
169 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
170 }
171
172 async removeAbuse (abuse: AdminAbuse) {
173 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
174 if (res === false) return
175
176 this.abuseService.removeAbuse(abuse).subscribe(
177 () => {
178 this.notifier.success(this.i18n('Abuse deleted.'))
179 this.loadData()
180 },
181
182 err => this.notifier.error(err.message)
183 )
184 }
185
186 updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
187 this.abuseService.updateAbuse(abuse, { state })
188 .subscribe(
189 () => this.loadData(),
190
191 err => this.notifier.error(err.message)
192 )
193 }
194
195 onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
196 const abuse = this.abuses.find(a => a.id === event.abuseId)
197
198 if (!abuse) {
199 console.error('Cannot find abuse %d.', event.abuseId)
200 return
201 }
202
203 abuse.countMessages = event.countMessages
204 }
205
206 openAbuseMessagesModal (abuse: AdminAbuse) {
207 this.abuseMessagesModal.openModal(abuse)
208 }
209
210 protected loadData () {
211 logger('Load data.')
212
213 return this.abuseService.getAdminAbuses({
214 pagination: this.pagination,
215 sort: this.sort,
216 search: this.search
217 }).subscribe(
218 async resultList => {
219 this.totalRecords = resultList.total
220
221 this.abuses = []
222
223 for (const a of resultList.data) {
224 const abuse = a as ProcessedAbuse
225
226 abuse.reasonHtml = await this.toHtml(abuse.reason)
227 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
228
229 if (abuse.video) {
230 abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
231
232 if (abuse.video.channel?.ownerAccount) {
233 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
234 }
235 }
236
237 if (abuse.comment) {
238 if (abuse.comment.deleted) {
239 abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment')
240 } else {
241 const truncated = truncate(abuse.comment.text, { length: 100 })
242 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true)
243 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true)
244 }
245 }
246
247 if (abuse.reporterAccount) {
248 abuse.reporterAccount = new Account(abuse.reporterAccount)
249 }
250
251 if (abuse.flaggedAccount) {
252 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
253 }
254
255 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
256
257 this.abuses.push(abuse)
258 }
259 },
260
261 err => this.notifier.error(err.message)
262 )
263 }
264
265 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
266 return [
267 {
268 label: this.i18n('Internal actions'),
269 isHeader: true
270 },
271 {
272 label: this.i18n('Delete report'),
273 handler: abuse => this.removeAbuse(abuse)
274 },
275 {
276 label: this.i18n('Messages'),
277 handler: abuse => this.openAbuseMessagesModal(abuse)
278 },
279 {
280 label: this.i18n('Add internal note'),
281 handler: abuse => this.openModerationCommentModal(abuse),
282 isDisplayed: abuse => !abuse.moderationComment
283 },
284 {
285 label: this.i18n('Update note'),
286 handler: abuse => this.openModerationCommentModal(abuse),
287 isDisplayed: abuse => !!abuse.moderationComment
288 },
289 {
290 label: this.i18n('Mark as accepted'),
291 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
292 isDisplayed: abuse => !this.isAbuseAccepted(abuse)
293 },
294 {
295 label: this.i18n('Mark as rejected'),
296 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
297 isDisplayed: abuse => !this.isAbuseRejected(abuse)
298 }
299 ]
300 }
301
302 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
303 return [
304 {
305 label: this.i18n('Actions for the flagged account'),
306 isHeader: true,
307 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
308 },
309
310 {
311 label: this.i18n('Mute account'),
312 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
313 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
314 },
315
316 {
317 label: this.i18n('Mute server account'),
318 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
319 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
320 }
321 ]
322 }
323
324 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
325 return [
326 {
327 label: this.i18n('Actions for the reporter'),
328 isHeader: true,
329 isDisplayed: abuse => !!abuse.reporterAccount
330 },
331
332 {
333 label: this.i18n('Mute reporter'),
334 isDisplayed: abuse => !!abuse.reporterAccount,
335 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
336 },
337
338 {
339 label: this.i18n('Mute server'),
340 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
341 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
342 }
343 ]
344 }
345
346 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
347 return [
348 {
349 label: this.i18n('Actions for the video'),
350 isHeader: true,
351 isDisplayed: abuse => abuse.video && !abuse.video.deleted
352 },
353 {
354 label: this.i18n('Block video'),
355 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
356 handler: abuse => {
357 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
358 .subscribe(
359 () => {
360 this.notifier.success(this.i18n('Video blocked.'))
361
362 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
363 },
364
365 err => this.notifier.error(err.message)
366 )
367 }
368 },
369 {
370 label: this.i18n('Unblock video'),
371 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
372 handler: abuse => {
373 this.videoBlocklistService.unblockVideo(abuse.video.id)
374 .subscribe(
375 () => {
376 this.notifier.success(this.i18n('Video unblocked.'))
377
378 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
379 },
380
381 err => this.notifier.error(err.message)
382 )
383 }
384 },
385 {
386 label: this.i18n('Delete video'),
387 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
388 handler: async abuse => {
389 const res = await this.confirmService.confirm(
390 this.i18n('Do you really want to delete this video?'),
391 this.i18n('Delete')
392 )
393 if (res === false) return
394
395 this.videoService.removeVideo(abuse.video.id)
396 .subscribe(
397 () => {
398 this.notifier.success(this.i18n('Video deleted.'))
399
400 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
401 },
402
403 err => this.notifier.error(err.message)
404 )
405 }
406 }
407 ]
408 }
409
410 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
411 return [
412 {
413 label: this.i18n('Actions for the comment'),
414 isHeader: true,
415 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
416 },
417
418 {
419 label: this.i18n('Delete comment'),
420 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
421 handler: async abuse => {
422 const res = await this.confirmService.confirm(
423 this.i18n('Do you really want to delete this comment?'),
424 this.i18n('Delete')
425 )
426 if (res === false) return
427
428 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
429 .subscribe(
430 () => {
431 this.notifier.success(this.i18n('Comment deleted.'))
432
433 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
434 },
435
436 err => this.notifier.error(err.message)
437 )
438 }
439 }
440 ]
441 }
442
443 private muteAccountHelper (account: Account) {
444 this.blocklistService.blockAccountByInstance(account)
445 .subscribe(
446 () => {
447 this.notifier.success(
448 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
449 )
450
451 account.mutedByInstance = true
452 },
453
454 err => this.notifier.error(err.message)
455 )
456 }
457
458 private muteServerHelper (host: string) {
459 this.blocklistService.blockServerByInstance(host)
460 .subscribe(
461 () => {
462 this.notifier.success(
463 this.i18n('Server {{host}} muted by the instance.', { host: host })
464 )
465 },
466
467 err => this.notifier.error(err.message)
468 )
469 }
470 9
471 private toHtml (text: string) {
472 return this.markdownRenderer.textMarkdownToHTML(text)
473 }
474} 10}