]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
Refactor video links builders
[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).subscribe(
149 () => {
150 this.notifier.success($localize`Abuse deleted.`)
151 this.reloadData()
152 },
153
154 err => this.notifier.error(err.message)
155 )
156 }
157
158 updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
159 this.abuseService.updateAbuse(abuse, { state })
160 .subscribe(
161 () => this.reloadData(),
162
163 err => this.notifier.error(err.message)
164 )
165 }
166
167 onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
168 const abuse = this.abuses.find(a => a.id === event.abuseId)
169
170 if (!abuse) {
171 console.error('Cannot find abuse %d.', event.abuseId)
172 return
173 }
174
175 abuse.countMessages = event.countMessages
176 }
177
178 openAbuseMessagesModal (abuse: AdminAbuse) {
179 this.abuseMessagesModal.openModal(abuse)
180 }
181
182 isLocalAbuse (abuse: AdminAbuse) {
183 if (this.viewType === 'user') return true
184
185 return Actor.IS_LOCAL(abuse.reporterAccount.host)
186 }
187
188 protected reloadData () {
189 logger('Loading data.')
190
191 const options = {
192 pagination: this.pagination,
193 sort: this.sort,
194 search: this.search
195 }
196
197 const observable = this.viewType === 'admin'
198 ? this.abuseService.getAdminAbuses(options)
199 : this.abuseService.getUserAbuses(options)
200
201 return observable.subscribe(
202 async resultList => {
203 this.totalRecords = resultList.total
204
205 this.abuses = []
206
207 for (const a of resultList.data) {
208 const abuse = a as ProcessedAbuse
209
210 abuse.reasonHtml = await this.toHtml(abuse.reason)
211
212 if (abuse.moderationComment) {
213 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
214 }
215
216 if (abuse.video) {
217 abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
218
219 if (abuse.video.channel?.ownerAccount) {
220 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
221 }
222 }
223
224 if (abuse.comment) {
225 if (abuse.comment.deleted) {
226 abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment`
227 } else {
228 const truncated = truncate(abuse.comment.text, { length: 100 })
229 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true)
230 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true)
231 }
232 }
233
234 if (abuse.reporterAccount) {
235 abuse.reporterAccount = new Account(abuse.reporterAccount)
236 }
237
238 if (abuse.flaggedAccount) {
239 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
240 }
241
242 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
243
244 this.abuses.push(abuse)
245 }
246 },
247
248 err => this.notifier.error(err.message)
249 )
250 }
251
252 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
253 return [
254 {
255 label: $localize`Internal actions`,
256 isHeader: true
257 },
258 {
259 label: this.isAdminView()
260 ? $localize`Messages with reporter`
261 : $localize`Messages with moderators`,
262 handler: abuse => this.openAbuseMessagesModal(abuse),
263 isDisplayed: abuse => this.isLocalAbuse(abuse)
264 },
265 {
266 label: $localize`Update internal note`,
267 handler: abuse => this.openModerationCommentModal(abuse),
268 isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
269 },
270 {
271 label: $localize`Mark as accepted`,
272 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
273 isDisplayed: abuse => this.isAdminView() && !this.isAbuseAccepted(abuse)
274 },
275 {
276 label: $localize`Mark as rejected`,
277 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
278 isDisplayed: abuse => this.isAdminView() && !this.isAbuseRejected(abuse)
279 },
280 {
281 label: $localize`Add internal note`,
282 handler: abuse => this.openModerationCommentModal(abuse),
283 isDisplayed: abuse => this.isAdminView() && !abuse.moderationComment
284 },
285 {
286 label: $localize`Delete report`,
287 handler: abuse => this.isAdminView() && this.removeAbuse(abuse)
288 }
289 ]
290 }
291
292 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
293 if (!this.isAdminView()) return []
294
295 return [
296 {
297 label: $localize`Actions for the flagged account`,
298 isHeader: true,
299 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
300 },
301
302 {
303 label: $localize`Mute account`,
304 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
305 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
306 },
307
308 {
309 label: $localize`Mute server account`,
310 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
311 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
312 }
313 ]
314 }
315
316 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
317 if (!this.isAdminView()) return []
318
319 return [
320 {
321 label: $localize`Actions for the reporter`,
322 isHeader: true,
323 isDisplayed: abuse => !!abuse.reporterAccount
324 },
325
326 {
327 label: $localize`Mute reporter`,
328 isDisplayed: abuse => !!abuse.reporterAccount,
329 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
330 },
331
332 {
333 label: $localize`Mute server`,
334 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
335 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
336 }
337 ]
338 }
339
340 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
341 if (!this.isAdminView()) return []
342
343 return [
344 {
345 label: $localize`Actions for the video`,
346 isHeader: true,
347 isDisplayed: abuse => abuse.video && !abuse.video.deleted
348 },
349 {
350 label: $localize`Block video`,
351 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
352 handler: abuse => {
353 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, abuse.video.channel.isLocal)
354 .subscribe(
355 () => {
356 this.notifier.success($localize`Video blocked.`)
357
358 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
359 },
360
361 err => this.notifier.error(err.message)
362 )
363 }
364 },
365 {
366 label: $localize`Unblock video`,
367 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
368 handler: abuse => {
369 this.videoBlocklistService.unblockVideo(abuse.video.id)
370 .subscribe(
371 () => {
372 this.notifier.success($localize`Video unblocked.`)
373
374 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
375 },
376
377 err => this.notifier.error(err.message)
378 )
379 }
380 },
381 {
382 label: $localize`Delete video`,
383 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
384 handler: async abuse => {
385 const res = await this.confirmService.confirm(
386 $localize`Do you really want to delete this video?`,
387 $localize`Delete`
388 )
389 if (res === false) return
390
391 this.videoService.removeVideo(abuse.video.id)
392 .subscribe(
393 () => {
394 this.notifier.success($localize`Video deleted.`)
395
396 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
397 },
398
399 err => this.notifier.error(err.message)
400 )
401 }
402 }
403 ]
404 }
405
406 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
407 if (!this.isAdminView()) return []
408
409 return [
410 {
411 label: $localize`Actions for the comment`,
412 isHeader: true,
413 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
414 },
415
416 {
417 label: $localize`Delete comment`,
418 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
419 handler: async abuse => {
420 const res = await this.confirmService.confirm(
421 $localize`Do you really want to delete this comment?`,
422 $localize`Delete`
423 )
424 if (res === false) return
425
426 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
427 .subscribe(
428 () => {
429 this.notifier.success($localize`Comment deleted.`)
430
431 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
432 },
433
434 err => this.notifier.error(err.message)
435 )
436 }
437 }
438 ]
439 }
440
441 private muteAccountHelper (account: Account) {
442 this.blocklistService.blockAccountByInstance(account)
443 .subscribe(
444 () => {
445 this.notifier.success($localize`Account ${account.nameWithHost} muted by the instance.`)
446 account.mutedByInstance = true
447 },
448
449 err => this.notifier.error(err.message)
450 )
451 }
452
453 private muteServerHelper (host: string) {
454 this.blocklistService.blockServerByInstance(host)
455 .subscribe(
456 () => {
457 this.notifier.success($localize`Server ${host} muted by the instance.`)
458 },
459
460 err => this.notifier.error(err.message)
461 )
462 }
463
464 private toHtml (text: string) {
465 return this.markdownRenderer.textMarkdownToHTML(text)
466 }
467 }