1 import * as debug from 'debug'
2 import truncate from 'lodash-es/truncate'
3 import { SortMeta } from 'primeng/api'
4 import { Component, Input, OnInit, ViewChild } from '@angular/core'
5 import { DomSanitizer } from '@angular/platform-browser'
6 import { ActivatedRoute, Router } from '@angular/router'
7 import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
8 import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9 import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
10 import { VideoCommentService } from '@app/shared/shared-video-comment'
11 import { logger } from '@root-helpers/logger'
12 import { AbuseState, AdminAbuse } from '@shared/models'
13 import { AdvancedInputFilter } from '../shared-forms'
14 import { AbuseMessageModalComponent } from './abuse-message-modal.component'
15 import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
16 import { ProcessedAbuse } from './processed-abuse.model'
18 const debugLogger = debug('peertube:moderation:AbuseListTableComponent')
21 selector: 'my-abuse-list-table',
22 templateUrl: './abuse-list-table.component.html',
23 styleUrls: [ '../shared-moderation/moderation.scss', './abuse-list-table.component.scss' ]
25 export class AbuseListTableComponent extends RestTable implements OnInit {
26 @Input() viewType: 'admin' | 'user'
28 @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
29 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
31 abuses: ProcessedAbuse[] = []
33 sort: SortMeta = { field: 'createdAt', order: 1 }
34 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
36 abuseActions: DropdownAction<ProcessedAbuse>[][] = []
38 inputFilters: AdvancedInputFilter[] = [
40 title: $localize`Advanced filters`,
43 value: 'state:pending',
44 label: $localize`Unsolved reports`
47 value: 'state:accepted',
48 label: $localize`Accepted reports`
51 value: 'state:rejected',
52 label: $localize`Refused reports`
55 value: 'videoIs:blacklisted',
56 label: $localize`Reports with blocked videos`
59 value: 'videoIs:deleted',
60 label: $localize`Reports with deleted videos`
67 protected route: ActivatedRoute,
68 protected router: Router,
69 private notifier: Notifier,
70 private abuseService: AbuseService,
71 private blocklistService: BlocklistService,
72 private commentService: VideoCommentService,
73 private videoService: VideoService,
74 private videoBlocklistService: VideoBlockService,
75 private confirmService: ConfirmService,
76 private markdownRenderer: MarkdownService,
77 private sanitizer: DomSanitizer
84 this.buildInternalActions(),
86 this.buildFlaggedAccountActions(),
88 this.buildCommentActions(),
90 this.buildVideoActions(),
92 this.buildAccountActions()
99 return this.viewType === 'admin'
103 return 'AbuseListTableComponent'
106 openModerationCommentModal (abuse: AdminAbuse) {
107 this.moderationCommentModal.openModal(abuse)
110 onModerationCommentUpdated () {
114 isAbuseAccepted (abuse: AdminAbuse) {
115 return abuse.state.id === AbuseState.ACCEPTED
118 isAbuseRejected (abuse: AdminAbuse) {
119 return abuse.state.id === AbuseState.REJECTED
122 getVideoUrl (abuse: AdminAbuse) {
123 return Video.buildWatchUrl(abuse.video)
126 getCommentUrl (abuse: AdminAbuse) {
127 return Video.buildWatchUrl(abuse.comment.video) + ';threadId=' + abuse.comment.threadId
130 getAccountUrl (abuse: ProcessedAbuse) {
131 return '/a/' + abuse.flaggedAccount.nameWithHost
134 async removeAbuse (abuse: AdminAbuse) {
135 const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`)
136 if (res === false) return
138 this.abuseService.removeAbuse(abuse)
141 this.notifier.success($localize`Abuse deleted.`)
145 error: err => this.notifier.error(err.message)
149 updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
150 this.abuseService.updateAbuse(abuse, { state })
152 next: () => this.reloadData(),
154 error: err => this.notifier.error(err.message)
158 onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
159 const abuse = this.abuses.find(a => a.id === event.abuseId)
162 logger.error(`Cannot find abuse ${event.abuseId}`)
166 abuse.countMessages = event.countMessages
169 openAbuseMessagesModal (abuse: AdminAbuse) {
170 this.abuseMessagesModal.openModal(abuse)
173 isLocalAbuse (abuse: AdminAbuse) {
174 if (this.viewType === 'user') return true
175 if (!abuse.reporterAccount) return false
177 return Actor.IS_LOCAL(abuse.reporterAccount.host)
180 protected reloadData () {
181 debugLogger('Loading data.')
184 pagination: this.pagination,
189 const observable = this.viewType === 'admin'
190 ? this.abuseService.getAdminAbuses(options)
191 : this.abuseService.getUserAbuses(options)
193 return observable.subscribe({
194 next: async resultList => {
195 this.totalRecords = resultList.total
199 for (const a of resultList.data) {
200 const abuse = a as ProcessedAbuse
202 abuse.reasonHtml = await this.toHtml(abuse.reason)
204 if (abuse.moderationComment) {
205 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
209 if (abuse.video.channel?.ownerAccount) {
210 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
215 if (abuse.comment.deleted) {
216 abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment`
218 const truncated = truncate(abuse.comment.text, { length: 100 })
219 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true)
220 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true)
224 if (abuse.reporterAccount) {
225 abuse.reporterAccount = new Account(abuse.reporterAccount)
228 if (abuse.flaggedAccount) {
229 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
232 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
234 this.abuses.push(abuse)
238 error: err => this.notifier.error(err.message)
242 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
245 label: $localize`Internal actions`,
249 label: this.isAdminView()
250 ? $localize`Messages with reporter`
251 : $localize`Messages with moderators`,
252 handler: abuse => this.openAbuseMessagesModal(abuse),
253 isDisplayed: abuse => this.isLocalAbuse(abuse)
256 label: $localize`Update internal note`,
257 handler: abuse => this.openModerationCommentModal(abuse),
258 isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
261 label: $localize`Mark as accepted`,
262 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
263 isDisplayed: abuse => this.isAdminView() && !this.isAbuseAccepted(abuse)
266 label: $localize`Mark as rejected`,
267 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
268 isDisplayed: abuse => this.isAdminView() && !this.isAbuseRejected(abuse)
271 label: $localize`Add internal note`,
272 handler: abuse => this.openModerationCommentModal(abuse),
273 isDisplayed: abuse => this.isAdminView() && !abuse.moderationComment
276 label: $localize`Delete report`,
277 handler: abuse => this.isAdminView() && this.removeAbuse(abuse)
282 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
283 if (!this.isAdminView()) return []
287 label: $localize`Actions for the flagged account`,
289 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
293 label: $localize`Mute account`,
294 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
295 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
299 label: $localize`Mute server account`,
300 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
301 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
306 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
307 if (!this.isAdminView()) return []
311 label: $localize`Actions for the reporter`,
313 isDisplayed: abuse => !!abuse.reporterAccount
317 label: $localize`Mute reporter`,
318 isDisplayed: abuse => !!abuse.reporterAccount,
319 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
323 label: $localize`Mute server`,
324 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
325 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
330 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
331 if (!this.isAdminView()) return []
335 label: $localize`Actions for the video`,
337 isDisplayed: abuse => abuse.video && !abuse.video.deleted
340 label: $localize`Block video`,
341 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
343 this.videoBlocklistService.blockVideo([ { videoId: abuse.video.id, unfederate: abuse.video.channel.isLocal } ])
346 this.notifier.success($localize`Video blocked.`)
348 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
351 error: err => this.notifier.error(err.message)
356 label: $localize`Unblock video`,
357 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
359 this.videoBlocklistService.unblockVideo(abuse.video.id)
362 this.notifier.success($localize`Video unblocked.`)
364 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
367 error: err => this.notifier.error(err.message)
372 label: $localize`Delete video`,
373 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
374 handler: async abuse => {
375 const res = await this.confirmService.confirm(
376 $localize`Do you really want to delete this video?`,
379 if (res === false) return
381 this.videoService.removeVideo(abuse.video.id)
384 this.notifier.success($localize`Video deleted.`)
386 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
389 error: err => this.notifier.error(err.message)
396 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
397 if (!this.isAdminView()) return []
401 label: $localize`Actions for the comment`,
403 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
407 label: $localize`Delete comment`,
408 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
409 handler: async abuse => {
410 const res = await this.confirmService.confirm(
411 $localize`Do you really want to delete this comment?`,
414 if (res === false) return
416 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
419 this.notifier.success($localize`Comment deleted.`)
421 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
424 error: err => this.notifier.error(err.message)
431 private muteAccountHelper (account: Account) {
432 this.blocklistService.blockAccountByInstance(account)
435 this.notifier.success($localize`Account ${account.nameWithHost} muted by the instance.`)
436 account.mutedByInstance = true
439 error: err => this.notifier.error(err.message)
443 private muteServerHelper (host: string) {
444 this.blocklistService.blockServerByInstance(host)
447 this.notifier.success($localize`Server ${host} muted by the instance.`)
450 error: err => this.notifier.error(err.message)
454 private toHtml (text: string) {
455 return this.markdownRenderer.textMarkdownToHTML(text)