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