1 import * as debug from 'debug'
2 import truncate from 'lodash-es/truncate'
3 import { SortMeta } from 'primeng/api'
4 import { buildVideoEmbed, 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'
19 const logger = debug('peertube:moderation:AbuseListTableComponent')
22 selector: 'my-abuse-list-table',
23 templateUrl: './abuse-list-table.component.html',
24 styleUrls: [ '../shared-moderation/moderation.scss', './abuse-list-table.component.scss' ]
26 export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit {
27 @Input() viewType: 'admin' | 'user'
28 @Input() baseRoute: string
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>[][] = []
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,
49 private markdownRenderer: MarkdownService,
50 private sanitizer: DomSanitizer,
51 private route: ActivatedRoute,
52 private router: Router
59 this.buildInternalActions(),
61 this.buildFlaggedAccountActions(),
63 this.buildCommentActions(),
65 this.buildVideoActions(),
67 this.buildAccountActions()
72 this.route.queryParams
73 .subscribe(params => {
74 this.search = params.search || ''
76 logger('On URL change (search: %s).', this.search)
78 this.setTableFilter(this.search)
84 if (this.search) this.setTableFilter(this.search)
88 return this.viewType === 'admin'
92 return 'AbuseListTableComponent'
95 openModerationCommentModal (abuse: AdminAbuse) {
96 this.moderationCommentModal.openModal(abuse)
99 onModerationCommentUpdated () {
103 /* Table filter functions */
104 onAbuseSearch (event: Event) {
106 this.setQueryParams((event.target as HTMLInputElement).value)
109 setQueryParams (search: string) {
110 const queryParams: Params = {}
111 if (search) Object.assign(queryParams, { search })
113 this.router.navigate([ this.baseRoute ], { queryParams })
116 resetTableFilter () {
117 this.setTableFilter('')
118 this.setQueryParams('')
121 /* END Table filter functions */
123 isAbuseAccepted (abuse: AdminAbuse) {
124 return abuse.state.id === AbuseState.ACCEPTED
127 isAbuseRejected (abuse: AdminAbuse) {
128 return abuse.state.id === AbuseState.REJECTED
131 getVideoUrl (abuse: AdminAbuse) {
132 return Video.buildClientUrl(abuse.video.uuid)
135 getCommentUrl (abuse: AdminAbuse) {
136 return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
139 getAccountUrl (abuse: ProcessedAbuse) {
140 return '/accounts/' + abuse.flaggedAccount.nameWithHost
143 getVideoEmbed (abuse: AdminAbuse) {
144 return buildVideoEmbed(
146 baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
149 startTime: abuse.startAt,
150 stopTime: abuse.endAt
155 switchToDefaultAvatar ($event: Event) {
156 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
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
163 this.abuseService.removeAbuse(abuse).subscribe(
165 this.notifier.success(this.i18n('Abuse deleted.'))
169 err => this.notifier.error(err.message)
173 updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
174 this.abuseService.updateAbuse(abuse, { state })
176 () => this.loadData(),
178 err => this.notifier.error(err.message)
182 onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
183 const abuse = this.abuses.find(a => a.id === event.abuseId)
186 console.error('Cannot find abuse %d.', event.abuseId)
190 abuse.countMessages = event.countMessages
193 openAbuseMessagesModal (abuse: AdminAbuse) {
194 this.abuseMessagesModal.openModal(abuse)
197 isLocalAbuse (abuse: AdminAbuse) {
198 if (this.viewType === 'user') return true
200 return Actor.IS_LOCAL(abuse.reporterAccount.host)
203 protected loadData () {
204 logger('Loading data.')
207 pagination: this.pagination,
212 const observable = this.viewType === 'admin'
213 ? this.abuseService.getAdminAbuses(options)
214 : this.abuseService.getUserAbuses(options)
216 return observable.subscribe(
217 async resultList => {
218 this.totalRecords = resultList.total
222 for (const a of resultList.data) {
223 const abuse = a as ProcessedAbuse
225 abuse.reasonHtml = await this.toHtml(abuse.reason)
227 if (abuse.moderationComment) {
228 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
232 abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
234 if (abuse.video.channel?.ownerAccount) {
235 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
240 if (abuse.comment.deleted) {
241 abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment')
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)
249 if (abuse.reporterAccount) {
250 abuse.reporterAccount = new Account(abuse.reporterAccount)
253 if (abuse.flaggedAccount) {
254 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
257 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
259 this.abuses.push(abuse)
263 err => this.notifier.error(err.message)
267 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
270 label: this.i18n('Internal actions'),
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)
281 label: this.i18n('Update internal note'),
282 handler: abuse => this.openModerationCommentModal(abuse),
283 isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
286 label: this.i18n('Mark as accepted'),
287 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
288 isDisplayed: abuse => this.isAdminView() && !this.isAbuseAccepted(abuse)
291 label: this.i18n('Mark as rejected'),
292 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
293 isDisplayed: abuse => this.isAdminView() && !this.isAbuseRejected(abuse)
296 label: this.i18n('Add internal note'),
297 handler: abuse => this.openModerationCommentModal(abuse),
298 isDisplayed: abuse => this.isAdminView() && !abuse.moderationComment
301 label: this.i18n('Delete report'),
302 handler: abuse => this.isAdminView() && this.removeAbuse(abuse)
307 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
308 if (!this.isAdminView()) return []
312 label: this.i18n('Actions for the flagged account'),
314 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
318 label: this.i18n('Mute account'),
319 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
320 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
324 label: this.i18n('Mute server account'),
325 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
326 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
331 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
332 if (!this.isAdminView()) return []
336 label: this.i18n('Actions for the reporter'),
338 isDisplayed: abuse => !!abuse.reporterAccount
342 label: this.i18n('Mute reporter'),
343 isDisplayed: abuse => !!abuse.reporterAccount,
344 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
348 label: this.i18n('Mute server'),
349 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
350 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
355 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
356 if (!this.isAdminView()) return []
360 label: this.i18n('Actions for the video'),
362 isDisplayed: abuse => abuse.video && !abuse.video.deleted
365 label: this.i18n('Block video'),
366 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
368 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
371 this.notifier.success(this.i18n('Video blocked.'))
373 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
376 err => this.notifier.error(err.message)
381 label: this.i18n('Unblock video'),
382 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
384 this.videoBlocklistService.unblockVideo(abuse.video.id)
387 this.notifier.success(this.i18n('Video unblocked.'))
389 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
392 err => this.notifier.error(err.message)
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?'),
404 if (res === false) return
406 this.videoService.removeVideo(abuse.video.id)
409 this.notifier.success(this.i18n('Video deleted.'))
411 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
414 err => this.notifier.error(err.message)
421 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
422 if (!this.isAdminView()) return []
426 label: this.i18n('Actions for the comment'),
428 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
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?'),
439 if (res === false) return
441 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
444 this.notifier.success(this.i18n('Comment deleted.'))
446 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
449 err => this.notifier.error(err.message)
456 private muteAccountHelper (account: Account) {
457 this.blocklistService.blockAccountByInstance(account)
460 this.notifier.success(
461 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
464 account.mutedByInstance = true
467 err => this.notifier.error(err.message)
471 private muteServerHelper (host: string) {
472 this.blocklistService.blockServerByInstance(host)
475 this.notifier.success(
476 this.i18n('Server {{host}} muted by the instance.', { host: host })
480 err => this.notifier.error(err.message)
484 private toHtml (text: string) {
485 return this.markdownRenderer.textMarkdownToHTML(text)