1 import * as debug from 'debug'
2 import truncate from 'lodash-es/truncate'
3 import { SortMeta } from 'primeng/api'
4 import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
5 import { environment } from 'src/environments/environment'
6 import { Component, Input, OnInit, ViewChild } from '@angular/core'
7 import { DomSanitizer } from '@angular/platform-browser'
8 import { ActivatedRoute, 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 { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
14 import { AbuseState, AdminAbuse } from '@shared/models'
15 import { AdvancedInputFilter } from '../shared-forms'
16 import { AbuseMessageModalComponent } from './abuse-message-modal.component'
17 import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
18 import { ProcessedAbuse } from './processed-abuse.model'
20 const logger = debug('peertube:moderation:AbuseListTableComponent')
23 selector: 'my-abuse-list-table',
24 templateUrl: './abuse-list-table.component.html',
25 styleUrls: [ '../shared-moderation/moderation.scss', './abuse-list-table.component.scss' ]
27 export class AbuseListTableComponent extends RestTable implements OnInit {
28 @Input() viewType: 'admin' | 'user'
30 @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
31 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
33 abuses: ProcessedAbuse[] = []
35 sort: SortMeta = { field: 'createdAt', order: 1 }
36 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
38 abuseActions: DropdownAction<ProcessedAbuse>[][] = []
40 inputFilters: AdvancedInputFilter[] = [
42 queryParams: { search: 'state:pending' },
43 label: $localize`Unsolved reports`
46 queryParams: { search: 'state:accepted' },
47 label: $localize`Accepted reports`
50 queryParams: { search: 'state:rejected' },
51 label: $localize`Refused reports`
54 queryParams: { search: 'videoIs:blacklisted' },
55 label: $localize`Reports with blocked videos`
58 queryParams: { search: 'videoIs:deleted' },
59 label: $localize`Reports with deleted videos`
64 protected route: ActivatedRoute,
65 protected router: Router,
66 private notifier: Notifier,
67 private abuseService: AbuseService,
68 private blocklistService: BlocklistService,
69 private commentService: VideoCommentService,
70 private videoService: VideoService,
71 private videoBlocklistService: VideoBlockService,
72 private confirmService: ConfirmService,
73 private markdownRenderer: MarkdownService,
74 private sanitizer: DomSanitizer
81 this.buildInternalActions(),
83 this.buildFlaggedAccountActions(),
85 this.buildCommentActions(),
87 this.buildVideoActions(),
89 this.buildAccountActions()
96 return this.viewType === 'admin'
100 return 'AbuseListTableComponent'
103 openModerationCommentModal (abuse: AdminAbuse) {
104 this.moderationCommentModal.openModal(abuse)
107 onModerationCommentUpdated () {
111 isAbuseAccepted (abuse: AdminAbuse) {
112 return abuse.state.id === AbuseState.ACCEPTED
115 isAbuseRejected (abuse: AdminAbuse) {
116 return abuse.state.id === AbuseState.REJECTED
119 getVideoUrl (abuse: AdminAbuse) {
120 return Video.buildWatchUrl(abuse.video)
123 getCommentUrl (abuse: AdminAbuse) {
124 return Video.buildWatchUrl(abuse.comment.video) + ';threadId=' + abuse.comment.threadId
127 getAccountUrl (abuse: ProcessedAbuse) {
128 return '/a/' + abuse.flaggedAccount.nameWithHost
131 getVideoEmbed (abuse: AdminAbuse) {
132 return buildVideoOrPlaylistEmbed(
134 url: buildVideoEmbedLink(abuse.video, environment.originServerUrl),
137 startTime: abuse.video.startAt,
138 stopTime: abuse.video.endAt
144 async removeAbuse (abuse: AdminAbuse) {
145 const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`)
146 if (res === false) return
148 this.abuseService.removeAbuse(abuse)
151 this.notifier.success($localize`Abuse deleted.`)
155 error: err => this.notifier.error(err.message)
159 updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
160 this.abuseService.updateAbuse(abuse, { state })
162 next: () => this.reloadData(),
164 error: err => this.notifier.error(err.message)
168 onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
169 const abuse = this.abuses.find(a => a.id === event.abuseId)
172 console.error('Cannot find abuse %d.', event.abuseId)
176 abuse.countMessages = event.countMessages
179 openAbuseMessagesModal (abuse: AdminAbuse) {
180 this.abuseMessagesModal.openModal(abuse)
183 isLocalAbuse (abuse: AdminAbuse) {
184 if (this.viewType === 'user') return true
186 return Actor.IS_LOCAL(abuse.reporterAccount.host)
189 protected reloadData () {
190 logger('Loading data.')
193 pagination: this.pagination,
198 const observable = this.viewType === 'admin'
199 ? this.abuseService.getAdminAbuses(options)
200 : this.abuseService.getUserAbuses(options)
202 return observable.subscribe({
203 next: async resultList => {
204 this.totalRecords = resultList.total
208 for (const a of resultList.data) {
209 const abuse = a as ProcessedAbuse
211 abuse.reasonHtml = await this.toHtml(abuse.reason)
213 if (abuse.moderationComment) {
214 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
218 abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
220 if (abuse.video.channel?.ownerAccount) {
221 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
226 if (abuse.comment.deleted) {
227 abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment`
229 const truncated = truncate(abuse.comment.text, { length: 100 })
230 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true)
231 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true)
235 if (abuse.reporterAccount) {
236 abuse.reporterAccount = new Account(abuse.reporterAccount)
239 if (abuse.flaggedAccount) {
240 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
243 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
245 this.abuses.push(abuse)
249 error: err => this.notifier.error(err.message)
253 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
256 label: $localize`Internal actions`,
260 label: this.isAdminView()
261 ? $localize`Messages with reporter`
262 : $localize`Messages with moderators`,
263 handler: abuse => this.openAbuseMessagesModal(abuse),
264 isDisplayed: abuse => this.isLocalAbuse(abuse)
267 label: $localize`Update internal note`,
268 handler: abuse => this.openModerationCommentModal(abuse),
269 isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
272 label: $localize`Mark as accepted`,
273 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
274 isDisplayed: abuse => this.isAdminView() && !this.isAbuseAccepted(abuse)
277 label: $localize`Mark as rejected`,
278 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
279 isDisplayed: abuse => this.isAdminView() && !this.isAbuseRejected(abuse)
282 label: $localize`Add internal note`,
283 handler: abuse => this.openModerationCommentModal(abuse),
284 isDisplayed: abuse => this.isAdminView() && !abuse.moderationComment
287 label: $localize`Delete report`,
288 handler: abuse => this.isAdminView() && this.removeAbuse(abuse)
293 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
294 if (!this.isAdminView()) return []
298 label: $localize`Actions for the flagged account`,
300 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
304 label: $localize`Mute account`,
305 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
306 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
310 label: $localize`Mute server account`,
311 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
312 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
317 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
318 if (!this.isAdminView()) return []
322 label: $localize`Actions for the reporter`,
324 isDisplayed: abuse => !!abuse.reporterAccount
328 label: $localize`Mute reporter`,
329 isDisplayed: abuse => !!abuse.reporterAccount,
330 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
334 label: $localize`Mute server`,
335 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
336 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
341 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
342 if (!this.isAdminView()) return []
346 label: $localize`Actions for the video`,
348 isDisplayed: abuse => abuse.video && !abuse.video.deleted
351 label: $localize`Block video`,
352 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
354 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, abuse.video.channel.isLocal)
357 this.notifier.success($localize`Video blocked.`)
359 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
362 error: err => this.notifier.error(err.message)
367 label: $localize`Unblock video`,
368 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
370 this.videoBlocklistService.unblockVideo(abuse.video.id)
373 this.notifier.success($localize`Video unblocked.`)
375 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
378 error: err => this.notifier.error(err.message)
383 label: $localize`Delete video`,
384 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
385 handler: async abuse => {
386 const res = await this.confirmService.confirm(
387 $localize`Do you really want to delete this video?`,
390 if (res === false) return
392 this.videoService.removeVideo(abuse.video.id)
395 this.notifier.success($localize`Video deleted.`)
397 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
400 error: err => this.notifier.error(err.message)
407 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
408 if (!this.isAdminView()) return []
412 label: $localize`Actions for the comment`,
414 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
418 label: $localize`Delete comment`,
419 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
420 handler: async abuse => {
421 const res = await this.confirmService.confirm(
422 $localize`Do you really want to delete this comment?`,
425 if (res === false) return
427 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
430 this.notifier.success($localize`Comment deleted.`)
432 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
435 error: err => this.notifier.error(err.message)
442 private muteAccountHelper (account: Account) {
443 this.blocklistService.blockAccountByInstance(account)
446 this.notifier.success($localize`Account ${account.nameWithHost} muted by the instance.`)
447 account.mutedByInstance = true
450 error: err => this.notifier.error(err.message)
454 private muteServerHelper (host: string) {
455 this.blocklistService.blockServerByInstance(host)
458 this.notifier.success($localize`Server ${host} muted by the instance.`)
461 error: err => this.notifier.error(err.message)
465 private toHtml (text: string) {
466 return this.markdownRenderer.textMarkdownToHTML(text)