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