]>
Commit | Line | Data |
---|---|---|
cfde28ba C |
1 | import * as debug from 'debug' |
2 | import truncate from 'lodash-es/truncate' | |
f77eb73b | 3 | import { SortMeta } from 'primeng/api' |
1ebddadd | 4 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' |
67ed6552 C |
5 | import { environment } from 'src/environments/environment' |
6 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' | |
8ca56654 | 7 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' |
25a42e29 | 8 | import { ActivatedRoute, Params, Router } from '@angular/router' |
67ed6552 C |
9 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' |
10 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' | |
d95d1559 | 11 | import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' |
cfde28ba | 12 | import { VideoCommentService } from '@app/shared/shared-video-comment' |
67ed6552 | 13 | import { I18n } from '@ngx-translate/i18n-polyfill' |
d95d1559 | 14 | import { Abuse, AbuseState } from '@shared/models' |
67ed6552 | 15 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' |
19a3b914 | 16 | |
cfde28ba C |
17 | const logger = debug('peertube:moderation:AbuseListComponent') |
18 | ||
19 | // Don't use an abuse model because we need external services to compute some properties | |
20 | // And this model is only used in this component | |
d95d1559 | 21 | export type ProcessedAbuse = Abuse & { |
25a42e29 RK |
22 | moderationCommentHtml?: string, |
23 | reasonHtml?: string | |
8ca56654 | 24 | embedHtml?: SafeHtml |
25a42e29 | 25 | updatedAt?: Date |
d95d1559 | 26 | |
25a42e29 | 27 | // override bare server-side definitions with rich client-side definitions |
8ca56654 C |
28 | reporterAccount?: Account |
29 | flaggedAccount?: Account | |
30 | ||
31 | truncatedCommentHtml?: string | |
32 | commentHtml?: string | |
d95d1559 C |
33 | |
34 | video: Abuse['video'] & { | |
35 | channel: Abuse['video']['channel'] & { | |
25a42e29 RK |
36 | ownerAccount: Account |
37 | } | |
38 | } | |
39 | } | |
40 | ||
11ac88de | 41 | @Component({ |
d95d1559 C |
42 | selector: 'my-abuse-list', |
43 | templateUrl: './abuse-list.component.html', | |
44 | styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ] | |
11ac88de | 45 | }) |
d95d1559 | 46 | export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { |
f36da21e | 47 | @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent |
efc9e845 | 48 | |
d95d1559 | 49 | abuses: ProcessedAbuse[] = [] |
d592e0a9 | 50 | totalRecords = 0 |
ab998f7b | 51 | sort: SortMeta = { field: 'createdAt', order: 1 } |
d592e0a9 | 52 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
11ac88de | 53 | |
cfde28ba | 54 | abuseActions: DropdownAction<ProcessedAbuse>[][] = [] |
efc9e845 | 55 | |
df98563e | 56 | constructor ( |
f8b2c1b4 | 57 | private notifier: Notifier, |
d95d1559 | 58 | private abuseService: AbuseService, |
bb152476 | 59 | private blocklistService: BlocklistService, |
cfde28ba | 60 | private commentService: VideoCommentService, |
9b4241e3 | 61 | private videoService: VideoService, |
5baee5fc | 62 | private videoBlocklistService: VideoBlockService, |
efc9e845 | 63 | private confirmService: ConfirmService, |
1506307f | 64 | private i18n: I18n, |
d6af8146 | 65 | private markdownRenderer: MarkdownService, |
844db39e | 66 | private sanitizer: DomSanitizer, |
25a42e29 RK |
67 | private route: ActivatedRoute, |
68 | private router: Router | |
28798b5d | 69 | ) { |
d592e0a9 | 70 | super() |
efc9e845 | 71 | |
d95d1559 | 72 | this.abuseActions = [ |
cfde28ba | 73 | this.buildInternalActions(), |
9b4241e3 | 74 | |
cfde28ba | 75 | this.buildFlaggedAccountActions(), |
9b4241e3 | 76 | |
cfde28ba | 77 | this.buildCommentActions(), |
9b4241e3 | 78 | |
cfde28ba C |
79 | this.buildVideoActions(), |
80 | ||
81 | this.buildAccountActions() | |
efc9e845 | 82 | ] |
d592e0a9 C |
83 | } |
84 | ||
85 | ngOnInit () { | |
24b9417c | 86 | this.initialize() |
844db39e RK |
87 | |
88 | this.route.queryParams | |
36004aa7 | 89 | .subscribe(params => { |
d8b38291 C |
90 | this.search = params.search || '' |
91 | ||
cfde28ba C |
92 | logger('On URL change (search: %s).', this.search) |
93 | ||
d8b38291 | 94 | this.setTableFilter(this.search) |
36004aa7 RK |
95 | this.loadData() |
96 | }) | |
97 | } | |
98 | ||
99 | ngAfterViewInit () { | |
0251197e | 100 | if (this.search) this.setTableFilter(this.search) |
df98563e | 101 | } |
11ac88de | 102 | |
8e11a1b3 | 103 | getIdentifier () { |
d95d1559 | 104 | return 'AbuseListComponent' |
8e11a1b3 C |
105 | } |
106 | ||
d95d1559 C |
107 | openModerationCommentModal (abuse: Abuse) { |
108 | this.moderationCommentModal.openModal(abuse) | |
efc9e845 C |
109 | } |
110 | ||
111 | onModerationCommentUpdated () { | |
112 | this.loadData() | |
113 | } | |
114 | ||
25a42e29 RK |
115 | /* Table filter functions */ |
116 | onAbuseSearch (event: Event) { | |
117 | this.onSearch(event) | |
118 | this.setQueryParams((event.target as HTMLInputElement).value) | |
119 | } | |
120 | ||
121 | setQueryParams (search: string) { | |
122 | const queryParams: Params = {} | |
123 | if (search) Object.assign(queryParams, { search }) | |
d8b38291 | 124 | |
8ca56654 | 125 | this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams }) |
d592e0a9 C |
126 | } |
127 | ||
25a42e29 RK |
128 | resetTableFilter () { |
129 | this.setTableFilter('') | |
130 | this.setQueryParams('') | |
131 | this.resetSearch() | |
36004aa7 | 132 | } |
25a42e29 | 133 | /* END Table filter functions */ |
36004aa7 | 134 | |
d95d1559 C |
135 | isAbuseAccepted (abuse: Abuse) { |
136 | return abuse.state.id === AbuseState.ACCEPTED | |
efc9e845 C |
137 | } |
138 | ||
d95d1559 C |
139 | isAbuseRejected (abuse: Abuse) { |
140 | return abuse.state.id === AbuseState.REJECTED | |
efc9e845 C |
141 | } |
142 | ||
d95d1559 C |
143 | getVideoUrl (abuse: Abuse) { |
144 | return Video.buildClientUrl(abuse.video.uuid) | |
191764f3 C |
145 | } |
146 | ||
8ca56654 C |
147 | getCommentUrl (abuse: Abuse) { |
148 | return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId | |
149 | } | |
150 | ||
cfde28ba C |
151 | getAccountUrl (abuse: ProcessedAbuse) { |
152 | return '/accounts/' + abuse.flaggedAccount.nameWithHost | |
153 | } | |
154 | ||
d95d1559 | 155 | getVideoEmbed (abuse: Abuse) { |
1ebddadd RK |
156 | return buildVideoEmbed( |
157 | buildVideoLink({ | |
d95d1559 | 158 | baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, |
1ebddadd RK |
159 | title: false, |
160 | warningTitle: false, | |
d95d1559 C |
161 | startTime: abuse.startAt, |
162 | stopTime: abuse.endAt | |
1ebddadd RK |
163 | }) |
164 | ) | |
d6af8146 RK |
165 | } |
166 | ||
167 | switchToDefaultAvatar ($event: Event) { | |
168 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | |
169 | } | |
170 | ||
d95d1559 | 171 | async removeAbuse (abuse: Abuse) { |
198d764f | 172 | const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) |
efc9e845 C |
173 | if (res === false) return |
174 | ||
d95d1559 | 175 | this.abuseService.removeAbuse(abuse).subscribe( |
efc9e845 | 176 | () => { |
f8b2c1b4 | 177 | this.notifier.success(this.i18n('Abuse deleted.')) |
efc9e845 C |
178 | this.loadData() |
179 | }, | |
180 | ||
f8b2c1b4 | 181 | err => this.notifier.error(err.message) |
efc9e845 C |
182 | ) |
183 | } | |
184 | ||
d95d1559 C |
185 | updateAbuseState (abuse: Abuse, state: AbuseState) { |
186 | this.abuseService.updateAbuse(abuse, { state }) | |
efc9e845 C |
187 | .subscribe( |
188 | () => this.loadData(), | |
189 | ||
f8b2c1b4 | 190 | err => this.notifier.error(err.message) |
efc9e845 | 191 | ) |
efc9e845 C |
192 | } |
193 | ||
d592e0a9 | 194 | protected loadData () { |
cfde28ba C |
195 | logger('Load data.') |
196 | ||
d95d1559 | 197 | return this.abuseService.getAbuses({ |
844db39e | 198 | pagination: this.pagination, |
042daa70 | 199 | sort: this.sort, |
844db39e RK |
200 | search: this.search |
201 | }).subscribe( | |
202 | async resultList => { | |
203 | this.totalRecords = resultList.total | |
204 | ||
8ca56654 C |
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 | abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment) | |
212 | ||
213 | if (abuse.video) { | |
214 | abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)) | |
215 | ||
216 | if (abuse.video.channel?.ownerAccount) { | |
217 | abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) | |
218 | } | |
219 | } | |
220 | ||
221 | if (abuse.comment) { | |
222 | if (abuse.comment.deleted) { | |
223 | abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment') | |
224 | } else { | |
225 | const truncated = truncate(abuse.comment.text, { length: 100 }) | |
226 | abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true) | |
227 | abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true) | |
228 | } | |
229 | } | |
230 | ||
231 | if (abuse.reporterAccount) { | |
232 | abuse.reporterAccount = new Account(abuse.reporterAccount) | |
233 | } | |
234 | ||
235 | if (abuse.flaggedAccount) { | |
236 | abuse.flaggedAccount = new Account(abuse.flaggedAccount) | |
237 | } | |
25a42e29 | 238 | |
0db536f1 | 239 | if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt |
25a42e29 | 240 | |
8ca56654 | 241 | this.abuses.push(abuse) |
844db39e | 242 | } |
844db39e RK |
243 | }, |
244 | ||
245 | err => this.notifier.error(err.message) | |
246 | ) | |
11ac88de | 247 | } |
41d71344 | 248 | |
cfde28ba C |
249 | private buildInternalActions (): DropdownAction<ProcessedAbuse>[] { |
250 | return [ | |
251 | { | |
252 | label: this.i18n('Internal actions'), | |
253 | isHeader: true | |
254 | }, | |
255 | { | |
256 | label: this.i18n('Delete report'), | |
257 | handler: abuse => this.removeAbuse(abuse) | |
258 | }, | |
259 | { | |
260 | label: this.i18n('Add note'), | |
261 | handler: abuse => this.openModerationCommentModal(abuse), | |
262 | isDisplayed: abuse => !abuse.moderationComment | |
263 | }, | |
264 | { | |
265 | label: this.i18n('Update note'), | |
266 | handler: abuse => this.openModerationCommentModal(abuse), | |
267 | isDisplayed: abuse => !!abuse.moderationComment | |
268 | }, | |
269 | { | |
270 | label: this.i18n('Mark as accepted'), | |
271 | handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED), | |
272 | isDisplayed: abuse => !this.isAbuseAccepted(abuse) | |
273 | }, | |
274 | { | |
275 | label: this.i18n('Mark as rejected'), | |
276 | handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED), | |
277 | isDisplayed: abuse => !this.isAbuseRejected(abuse) | |
278 | } | |
279 | ] | |
280 | } | |
281 | ||
282 | private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] { | |
283 | return [ | |
284 | { | |
285 | label: this.i18n('Actions for the flagged account'), | |
286 | isHeader: true, | |
287 | isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video | |
288 | }, | |
289 | ||
290 | { | |
291 | label: this.i18n('Mute account'), | |
292 | isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video, | |
293 | handler: abuse => this.muteAccountHelper(abuse.flaggedAccount) | |
294 | }, | |
295 | ||
296 | { | |
297 | label: this.i18n('Mute server account'), | |
298 | isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video, | |
299 | handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host) | |
300 | } | |
301 | ] | |
302 | } | |
303 | ||
304 | private buildAccountActions (): DropdownAction<ProcessedAbuse>[] { | |
305 | return [ | |
306 | { | |
307 | label: this.i18n('Actions for the reporter'), | |
308 | isHeader: true, | |
309 | isDisplayed: abuse => !!abuse.reporterAccount | |
310 | }, | |
311 | ||
312 | { | |
313 | label: this.i18n('Mute reporter'), | |
314 | isDisplayed: abuse => !!abuse.reporterAccount, | |
315 | handler: abuse => this.muteAccountHelper(abuse.reporterAccount) | |
316 | }, | |
317 | ||
318 | { | |
319 | label: this.i18n('Mute server'), | |
320 | isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId, | |
321 | handler: abuse => this.muteServerHelper(abuse.reporterAccount.host) | |
322 | } | |
323 | ] | |
324 | } | |
325 | ||
326 | private buildVideoActions (): DropdownAction<ProcessedAbuse>[] { | |
327 | return [ | |
328 | { | |
329 | label: this.i18n('Actions for the video'), | |
330 | isHeader: true, | |
331 | isDisplayed: abuse => abuse.video && !abuse.video.deleted | |
332 | }, | |
333 | { | |
334 | label: this.i18n('Block video'), | |
335 | isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted, | |
336 | handler: abuse => { | |
337 | this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true) | |
338 | .subscribe( | |
339 | () => { | |
340 | this.notifier.success(this.i18n('Video blocked.')) | |
341 | ||
342 | this.updateAbuseState(abuse, AbuseState.ACCEPTED) | |
343 | }, | |
344 | ||
345 | err => this.notifier.error(err.message) | |
346 | ) | |
347 | } | |
348 | }, | |
349 | { | |
350 | label: this.i18n('Unblock video'), | |
351 | isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted, | |
352 | handler: abuse => { | |
353 | this.videoBlocklistService.unblockVideo(abuse.video.id) | |
354 | .subscribe( | |
355 | () => { | |
356 | this.notifier.success(this.i18n('Video unblocked.')) | |
357 | ||
358 | this.updateAbuseState(abuse, AbuseState.ACCEPTED) | |
359 | }, | |
360 | ||
361 | err => this.notifier.error(err.message) | |
362 | ) | |
363 | } | |
364 | }, | |
365 | { | |
366 | label: this.i18n('Delete video'), | |
367 | isDisplayed: abuse => abuse.video && !abuse.video.deleted, | |
368 | handler: async abuse => { | |
369 | const res = await this.confirmService.confirm( | |
370 | this.i18n('Do you really want to delete this video?'), | |
371 | this.i18n('Delete') | |
372 | ) | |
373 | if (res === false) return | |
374 | ||
375 | this.videoService.removeVideo(abuse.video.id) | |
376 | .subscribe( | |
377 | () => { | |
378 | this.notifier.success(this.i18n('Video deleted.')) | |
379 | ||
380 | this.updateAbuseState(abuse, AbuseState.ACCEPTED) | |
381 | }, | |
382 | ||
383 | err => this.notifier.error(err.message) | |
384 | ) | |
385 | } | |
386 | } | |
387 | ] | |
388 | } | |
389 | ||
390 | private buildCommentActions (): DropdownAction<ProcessedAbuse>[] { | |
391 | return [ | |
392 | { | |
393 | label: this.i18n('Actions for the comment'), | |
394 | isHeader: true, | |
395 | isDisplayed: abuse => abuse.comment && !abuse.comment.deleted | |
396 | }, | |
397 | ||
398 | { | |
399 | label: this.i18n('Delete comment'), | |
400 | isDisplayed: abuse => abuse.comment && !abuse.comment.deleted, | |
401 | handler: async abuse => { | |
402 | const res = await this.confirmService.confirm( | |
403 | this.i18n('Do you really want to delete this comment?'), | |
404 | this.i18n('Delete') | |
405 | ) | |
406 | if (res === false) return | |
407 | ||
408 | this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id) | |
409 | .subscribe( | |
410 | () => { | |
411 | this.notifier.success(this.i18n('Comment deleted.')) | |
412 | ||
413 | this.updateAbuseState(abuse, AbuseState.ACCEPTED) | |
414 | }, | |
415 | ||
416 | err => this.notifier.error(err.message) | |
417 | ) | |
418 | } | |
419 | } | |
420 | ] | |
421 | } | |
422 | ||
423 | private muteAccountHelper (account: Account) { | |
424 | this.blocklistService.blockAccountByInstance(account) | |
425 | .subscribe( | |
426 | () => { | |
427 | this.notifier.success( | |
428 | this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) | |
429 | ) | |
430 | ||
431 | account.mutedByInstance = true | |
432 | }, | |
433 | ||
434 | err => this.notifier.error(err.message) | |
435 | ) | |
436 | } | |
437 | ||
438 | private muteServerHelper (host: string) { | |
439 | this.blocklistService.blockServerByInstance(host) | |
440 | .subscribe( | |
441 | () => { | |
442 | this.notifier.success( | |
443 | this.i18n('Server {{host}} muted by the instance.', { host: host }) | |
444 | ) | |
445 | }, | |
446 | ||
447 | err => this.notifier.error(err.message) | |
448 | ) | |
449 | } | |
450 | ||
41d71344 C |
451 | private toHtml (text: string) { |
452 | return this.markdownRenderer.textMarkdownToHTML(text) | |
453 | } | |
11ac88de | 454 | } |