]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
Fix client lint
[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 } 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'
19
20 const logger = debug('peertube:moderation:AbuseListTableComponent')
21
22 @Component({
23 selector: 'my-abuse-list-table',
24 templateUrl: './abuse-list-table.component.html',
25 styleUrls: [ '../shared-moderation/moderation.scss', './abuse-list-table.component.scss' ]
26 })
27 export class AbuseListTableComponent extends RestTable implements OnInit {
28 @Input() viewType: 'admin' | 'user'
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 inputFilters: AdvancedInputFilter[] = [
41 {
42 queryParams: { 'search': 'state:pending' },
43 label: $localize`Unsolved reports`
44 },
45 {
46 queryParams: { 'search': 'state:accepted' },
47 label: $localize`Accepted reports`
48 },
49 {
50 queryParams: { 'search': 'state:rejected' },
51 label: $localize`Refused reports`
52 },
53 {
54 queryParams: { 'search': 'videoIs:blacklisted' },
55 label: $localize`Reports with blocked videos`
56 },
57 {
58 queryParams: { 'search': 'videoIs:deleted' },
59 label: $localize`Reports with deleted videos`
60 }
61 ]
62
63 constructor (
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
75 ) {
76 super()
77 }
78
79 ngOnInit () {
80 this.abuseActions = [
81 this.buildInternalActions(),
82
83 this.buildFlaggedAccountActions(),
84
85 this.buildCommentActions(),
86
87 this.buildVideoActions(),
88
89 this.buildAccountActions()
90 ]
91
92 this.initialize()
93 }
94
95 isAdminView () {
96 return this.viewType === 'admin'
97 }
98
99 getIdentifier () {
100 return 'AbuseListTableComponent'
101 }
102
103 openModerationCommentModal (abuse: AdminAbuse) {
104 this.moderationCommentModal.openModal(abuse)
105 }
106
107 onModerationCommentUpdated () {
108 this.reloadData()
109 }
110
111 isAbuseAccepted (abuse: AdminAbuse) {
112 return abuse.state.id === AbuseState.ACCEPTED
113 }
114
115 isAbuseRejected (abuse: AdminAbuse) {
116 return abuse.state.id === AbuseState.REJECTED
117 }
118
119 getVideoUrl (abuse: AdminAbuse) {
120 return Video.buildWatchUrl(abuse.video)
121 }
122
123 getCommentUrl (abuse: AdminAbuse) {
124 return Video.buildWatchUrl(abuse.comment.video) + ';threadId=' + abuse.comment.threadId
125 }
126
127 getAccountUrl (abuse: ProcessedAbuse) {
128 return '/a/' + abuse.flaggedAccount.nameWithHost
129 }
130
131 getVideoEmbed (abuse: AdminAbuse) {
132 return buildVideoOrPlaylistEmbed(
133 decorateVideoLink({
134 url: buildVideoEmbedLink(abuse.video, environment.originServerUrl),
135 title: false,
136 warningTitle: false,
137 startTime: abuse.video.startAt,
138 stopTime: abuse.video.endAt
139 }),
140 abuse.video.name
141 )
142 }
143
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
147
148 this.abuseService.removeAbuse(abuse)
149 .subscribe({
150 next: () => {
151 this.notifier.success($localize`Abuse deleted.`)
152 this.reloadData()
153 },
154
155 error: err => this.notifier.error(err.message)
156 })
157 }
158
159 updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
160 this.abuseService.updateAbuse(abuse, { state })
161 .subscribe({
162 next: () => this.reloadData(),
163
164 error: err => this.notifier.error(err.message)
165 })
166 }
167
168 onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
169 const abuse = this.abuses.find(a => a.id === event.abuseId)
170
171 if (!abuse) {
172 console.error('Cannot find abuse %d.', event.abuseId)
173 return
174 }
175
176 abuse.countMessages = event.countMessages
177 }
178
179 openAbuseMessagesModal (abuse: AdminAbuse) {
180 this.abuseMessagesModal.openModal(abuse)
181 }
182
183 isLocalAbuse (abuse: AdminAbuse) {
184 if (this.viewType === 'user') return true
185
186 return Actor.IS_LOCAL(abuse.reporterAccount.host)
187 }
188
189 protected reloadData () {
190 logger('Loading data.')
191
192 const options = {
193 pagination: this.pagination,
194 sort: this.sort,
195 search: this.search
196 }
197
198 const observable = this.viewType === 'admin'
199 ? this.abuseService.getAdminAbuses(options)
200 : this.abuseService.getUserAbuses(options)
201
202 return observable.subscribe({
203 next: async resultList => {
204 this.totalRecords = resultList.total
205
206 this.abuses = []
207
208 for (const a of resultList.data) {
209 const abuse = a as ProcessedAbuse
210
211 abuse.reasonHtml = await this.toHtml(abuse.reason)
212
213 if (abuse.moderationComment) {
214 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
215 }
216
217 if (abuse.video) {
218 abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
219
220 if (abuse.video.channel?.ownerAccount) {
221 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
222 }
223 }
224
225 if (abuse.comment) {
226 if (abuse.comment.deleted) {
227 abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment`
228 } else {
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)
232 }
233 }
234
235 if (abuse.reporterAccount) {
236 abuse.reporterAccount = new Account(abuse.reporterAccount)
237 }
238
239 if (abuse.flaggedAccount) {
240 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
241 }
242
243 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
244
245 this.abuses.push(abuse)
246 }
247 },
248
249 error: err => this.notifier.error(err.message)
250 })
251 }
252
253 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
254 return [
255 {
256 label: $localize`Internal actions`,
257 isHeader: true
258 },
259 {
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)
265 },
266 {
267 label: $localize`Update internal note`,
268 handler: abuse => this.openModerationCommentModal(abuse),
269 isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
270 },
271 {
272 label: $localize`Mark as accepted`,
273 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
274 isDisplayed: abuse => this.isAdminView() && !this.isAbuseAccepted(abuse)
275 },
276 {
277 label: $localize`Mark as rejected`,
278 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
279 isDisplayed: abuse => this.isAdminView() && !this.isAbuseRejected(abuse)
280 },
281 {
282 label: $localize`Add internal note`,
283 handler: abuse => this.openModerationCommentModal(abuse),
284 isDisplayed: abuse => this.isAdminView() && !abuse.moderationComment
285 },
286 {
287 label: $localize`Delete report`,
288 handler: abuse => this.isAdminView() && this.removeAbuse(abuse)
289 }
290 ]
291 }
292
293 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
294 if (!this.isAdminView()) return []
295
296 return [
297 {
298 label: $localize`Actions for the flagged account`,
299 isHeader: true,
300 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
301 },
302
303 {
304 label: $localize`Mute account`,
305 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
306 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
307 },
308
309 {
310 label: $localize`Mute server account`,
311 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
312 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
313 }
314 ]
315 }
316
317 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
318 if (!this.isAdminView()) return []
319
320 return [
321 {
322 label: $localize`Actions for the reporter`,
323 isHeader: true,
324 isDisplayed: abuse => !!abuse.reporterAccount
325 },
326
327 {
328 label: $localize`Mute reporter`,
329 isDisplayed: abuse => !!abuse.reporterAccount,
330 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
331 },
332
333 {
334 label: $localize`Mute server`,
335 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
336 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
337 }
338 ]
339 }
340
341 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
342 if (!this.isAdminView()) return []
343
344 return [
345 {
346 label: $localize`Actions for the video`,
347 isHeader: true,
348 isDisplayed: abuse => abuse.video && !abuse.video.deleted
349 },
350 {
351 label: $localize`Block video`,
352 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
353 handler: abuse => {
354 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, abuse.video.channel.isLocal)
355 .subscribe({
356 next: () => {
357 this.notifier.success($localize`Video blocked.`)
358
359 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
360 },
361
362 error: err => this.notifier.error(err.message)
363 })
364 }
365 },
366 {
367 label: $localize`Unblock video`,
368 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
369 handler: abuse => {
370 this.videoBlocklistService.unblockVideo(abuse.video.id)
371 .subscribe({
372 next: () => {
373 this.notifier.success($localize`Video unblocked.`)
374
375 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
376 },
377
378 error: err => this.notifier.error(err.message)
379 })
380 }
381 },
382 {
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?`,
388 $localize`Delete`
389 )
390 if (res === false) return
391
392 this.videoService.removeVideo(abuse.video.id)
393 .subscribe({
394 next: () => {
395 this.notifier.success($localize`Video deleted.`)
396
397 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
398 },
399
400 error: err => this.notifier.error(err.message)
401 })
402 }
403 }
404 ]
405 }
406
407 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
408 if (!this.isAdminView()) return []
409
410 return [
411 {
412 label: $localize`Actions for the comment`,
413 isHeader: true,
414 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
415 },
416
417 {
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?`,
423 $localize`Delete`
424 )
425 if (res === false) return
426
427 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
428 .subscribe({
429 next: () => {
430 this.notifier.success($localize`Comment deleted.`)
431
432 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
433 },
434
435 error: err => this.notifier.error(err.message)
436 })
437 }
438 }
439 ]
440 }
441
442 private muteAccountHelper (account: Account) {
443 this.blocklistService.blockAccountByInstance(account)
444 .subscribe({
445 next: () => {
446 this.notifier.success($localize`Account ${account.nameWithHost} muted by the instance.`)
447 account.mutedByInstance = true
448 },
449
450 error: err => this.notifier.error(err.message)
451 })
452 }
453
454 private muteServerHelper (host: string) {
455 this.blocklistService.blockServerByInstance(host)
456 .subscribe({
457 next: () => {
458 this.notifier.success($localize`Server ${host} muted by the instance.`)
459 },
460
461 error: err => this.notifier.error(err.message)
462 })
463 }
464
465 private toHtml (text: string) {
466 return this.markdownRenderer.textMarkdownToHTML(text)
467 }
468 }