From d95d15598847c7f020aa056e7e6e0c02d2bbf732 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 1 Jul 2020 16:05:30 +0200 Subject: Use 3 tables to represent abuses --- client/src/app/+admin/admin.component.ts | 2 +- client/src/app/+admin/admin.module.ts | 10 +- .../abuse-list/abuse-details.component.html | 93 ++++++ .../abuse-list/abuse-details.component.ts | 52 ++++ .../abuse-list/abuse-list.component.html | 149 ++++++++++ .../abuse-list/abuse-list.component.scss | 23 ++ .../moderation/abuse-list/abuse-list.component.ts | 329 +++++++++++++++++++++ .../src/app/+admin/moderation/abuse-list/index.ts | 3 + .../moderation-comment-modal.component.html | 38 +++ .../moderation-comment-modal.component.scss | 6 + .../moderation-comment-modal.component.ts | 70 +++++ client/src/app/+admin/moderation/index.ts | 2 +- .../src/app/+admin/moderation/moderation.routes.ts | 15 +- .../+admin/moderation/video-abuse-list/index.ts | 2 - .../moderation-comment-modal.component.html | 38 --- .../moderation-comment-modal.component.scss | 6 - .../moderation-comment-modal.component.ts | 70 ----- .../video-abuse-details.component.html | 93 ------ .../video-abuse-details.component.ts | 52 ---- .../video-abuse-list.component.html | 149 ---------- .../video-abuse-list.component.scss | 23 -- .../video-abuse-list/video-abuse-list.component.ts | 328 -------------------- ...y-account-notification-preferences.component.ts | 2 +- client/src/app/menu/menu.component.ts | 4 +- .../form-validators/abuse-validators.service.ts | 30 ++ .../shared/shared-forms/form-validators/index.ts | 2 +- .../video-abuse-validators.service.ts | 30 -- .../app/shared/shared-forms/shared-form.module.ts | 4 +- .../app/shared/shared-main/account/actor.model.ts | 2 +- .../shared-main/users/user-notification.model.ts | 26 +- .../users/user-notifications.component.html | 8 +- .../app/shared/shared-moderation/abuse.service.ts | 98 ++++++ client/src/app/shared/shared-moderation/index.ts | 2 +- .../shared-moderation/shared-moderation.module.ts | 4 +- .../shared-moderation/video-abuse.service.ts | 98 ------ .../shared-moderation/video-report.component.ts | 29 +- 36 files changed, 958 insertions(+), 934 deletions(-) create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-details.component.html create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-list.component.html create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts create mode 100644 client/src/app/+admin/moderation/abuse-list/index.ts create mode 100644 client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html create mode 100644 client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss create mode 100644 client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/index.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts create mode 100644 client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts delete mode 100644 client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts create mode 100644 client/src/app/shared/shared-moderation/abuse.service.ts delete mode 100644 client/src/app/shared/shared-moderation/video-abuse.service.ts (limited to 'client') diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 6f340884f..1e137e63e 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts @@ -91,7 +91,7 @@ export class AdminComponent implements OnInit { } hasVideoAbusesRight () { - return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) + return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES) } hasVideoBlocklistRight () { diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 728227a84..c59bd2927 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -14,10 +14,10 @@ import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponen import { FollowingListComponent } from './follows/following-list/following-list.component' import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' -import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlockListComponent } from './moderation' +import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' import { ModerationComponent } from './moderation/moderation.component' -import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component' +import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component' import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' @@ -60,8 +60,10 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom ModerationComponent, VideoBlockListComponent, - VideoAbuseListComponent, - VideoAbuseDetailsComponent, + + AbuseListComponent, + AbuseDetailsComponent, + ModerationCommentModalComponent, InstanceServerBlocklistComponent, InstanceAccountBlocklistComponent, diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html new file mode 100644 index 000000000..d031ea8ed --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html @@ -0,0 +1,93 @@ +
+ + + + +
+
+
+ The video was deleted + The video was blocked +
+
+
+
+
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts new file mode 100644 index 000000000..8f87630b8 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts @@ -0,0 +1,52 @@ +import { Component, Input } from '@angular/core' +import { Actor } from '@app/shared/shared-main' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { AbusePredefinedReasonsString } from '@shared/models' +import { ProcessedAbuse } from './abuse-list.component' +import { durationToString } from '@app/helpers' + +@Component({ + selector: 'my-abuse-details', + templateUrl: './abuse-details.component.html', + styleUrls: [ '../moderation.component.scss' ] +}) +export class AbuseDetailsComponent { + @Input() abuse: ProcessedAbuse + + private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } + + constructor ( + private i18n: I18n + ) { + this.predefinedReasonsTranslations = { + violentOrRepulsive: this.i18n('Violent or Repulsive'), + hatefulOrAbusive: this.i18n('Hateful or Abusive'), + spamOrMisleading: this.i18n('Spam or Misleading'), + privacy: this.i18n('Privacy'), + rights: this.i18n('Rights'), + serverRules: this.i18n('Server rules'), + thumbnails: this.i18n('Thumbnails'), + captions: this.i18n('Captions') + } + } + + get startAt () { + return durationToString(this.abuse.startAt) + } + + get endAt () { + return durationToString(this.abuse.endAt) + } + + getPredefinedReasons () { + if (!this.abuse.predefinedReasons) return [] + return this.abuse.predefinedReasons.map(r => ({ + id: r, + label: this.predefinedReasonsTranslations[r] + })) + } + + switchToDefaultAvatar ($event: Event) { + ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() + } +} diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html new file mode 100644 index 000000000..167f32fe6 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html @@ -0,0 +1,149 @@ + + +
+
+
+ + + + Clear filters +
+
+
+
+ + + + + Reporter + Video + Created + State + + + + + + + + + + + + + + +
+ Avatar +
+ {{ abuse.reporterAccount.displayName }} + {{ abuse.reporterAccount.nameWithHost }} +
+
+
+ + + + +
+
+ + + {{ abuse.nth }}/{{ abuse.count }} + +
+
+
+ + + {{ abuse.video.name }} +
+
by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }}
+
+
+
+ + + +
+
+ Deleted +
+
+
+ {{ abuse.video.name }} + +
+
by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }}
+
+
+ + + {{ abuse.createdAt | date: 'short' }} + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ No video abuses found matching current filters. + No video abuses found. +
+ + +
+
+ + diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss new file mode 100644 index 000000000..8eee15b64 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss @@ -0,0 +1,23 @@ +@import 'mixins'; +@import 'miniature'; + +.video-details-date-updated { + font-size: 90%; + margin-top: .1rem; +} + +.video-details-links { + @include disable-default-a-behaviour; +} + +.video-abuse-states .glyphicon-comment { + margin-left: 0.5rem; +} + +.input-group { + @include peertube-input-group(300px); + + .dropdown-toggle::after { + margin-left: 0; + } +} diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts new file mode 100644 index 000000000..427ec4d5d --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts @@ -0,0 +1,329 @@ +import { SortMeta } from 'primeng/api' +import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' +import { environment } from 'src/environments/environment' +import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' +import { DomSanitizer } from '@angular/platform-browser' +import { ActivatedRoute, Params, Router } from '@angular/router' +import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' +import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' +import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { Abuse, AbuseState } from '@shared/models' +import { ModerationCommentModalComponent } from './moderation-comment-modal.component' + +export type ProcessedAbuse = Abuse & { + moderationCommentHtml?: string, + reasonHtml?: string + embedHtml?: string + updatedAt?: Date + + // override bare server-side definitions with rich client-side definitions + reporterAccount: Account + + video: Abuse['video'] & { + channel: Abuse['video']['channel'] & { + ownerAccount: Account + } + } +} + +@Component({ + selector: 'my-abuse-list', + templateUrl: './abuse-list.component.html', + styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ] +}) +export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { + @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent + + abuses: ProcessedAbuse[] = [] + totalRecords = 0 + sort: SortMeta = { field: 'createdAt', order: 1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + abuseActions: DropdownAction[][] = [] + + constructor ( + private notifier: Notifier, + private abuseService: AbuseService, + private blocklistService: BlocklistService, + private videoService: VideoService, + private videoBlocklistService: VideoBlockService, + private confirmService: ConfirmService, + private i18n: I18n, + private markdownRenderer: MarkdownService, + private sanitizer: DomSanitizer, + private route: ActivatedRoute, + private router: Router + ) { + super() + + this.abuseActions = [ + [ + { + label: this.i18n('Internal actions'), + isHeader: true + }, + { + label: this.i18n('Delete report'), + handler: abuse => this.removeAbuse(abuse) + }, + { + label: this.i18n('Add note'), + handler: abuse => this.openModerationCommentModal(abuse), + isDisplayed: abuse => !abuse.moderationComment + }, + { + label: this.i18n('Update note'), + handler: abuse => this.openModerationCommentModal(abuse), + isDisplayed: abuse => !!abuse.moderationComment + }, + { + label: this.i18n('Mark as accepted'), + handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED), + isDisplayed: abuse => !this.isAbuseAccepted(abuse) + }, + { + label: this.i18n('Mark as rejected'), + handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED), + isDisplayed: abuse => !this.isAbuseRejected(abuse) + } + ], + [ + { + label: this.i18n('Actions for the video'), + isHeader: true, + isDisplayed: abuse => !abuse.video.deleted + }, + { + label: this.i18n('Block video'), + isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted, + handler: abuse => { + this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true) + .subscribe( + () => { + this.notifier.success(this.i18n('Video blocked.')) + + this.updateAbuseState(abuse, AbuseState.ACCEPTED) + }, + + err => this.notifier.error(err.message) + ) + } + }, + { + label: this.i18n('Unblock video'), + isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted, + handler: abuse => { + this.videoBlocklistService.unblockVideo(abuse.video.id) + .subscribe( + () => { + this.notifier.success(this.i18n('Video unblocked.')) + + this.updateAbuseState(abuse, AbuseState.ACCEPTED) + }, + + err => this.notifier.error(err.message) + ) + } + }, + { + label: this.i18n('Delete video'), + isDisplayed: abuse => !abuse.video.deleted, + handler: async abuse => { + const res = await this.confirmService.confirm( + this.i18n('Do you really want to delete this video?'), + this.i18n('Delete') + ) + if (res === false) return + + this.videoService.removeVideo(abuse.video.id) + .subscribe( + () => { + this.notifier.success(this.i18n('Video deleted.')) + + this.updateAbuseState(abuse, AbuseState.ACCEPTED) + }, + + err => this.notifier.error(err.message) + ) + } + } + ], + [ + { + label: this.i18n('Actions for the reporter'), + isHeader: true + }, + { + label: this.i18n('Mute reporter'), + handler: async abuse => { + const account = abuse.reporterAccount as Account + + this.blocklistService.blockAccountByInstance(account) + .subscribe( + () => { + this.notifier.success( + this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) + ) + + account.mutedByInstance = true + }, + + err => this.notifier.error(err.message) + ) + } + }, + { + label: this.i18n('Mute server'), + isDisplayed: abuse => !abuse.reporterAccount.userId, + handler: async abuse => { + this.blocklistService.blockServerByInstance(abuse.reporterAccount.host) + .subscribe( + () => { + this.notifier.success( + this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host }) + ) + }, + + err => this.notifier.error(err.message) + ) + } + } + ] + ] + } + + ngOnInit () { + this.initialize() + + this.route.queryParams + .subscribe(params => { + this.search = params.search || '' + + this.setTableFilter(this.search) + this.loadData() + }) + } + + ngAfterViewInit () { + if (this.search) this.setTableFilter(this.search) + } + + getIdentifier () { + return 'AbuseListComponent' + } + + openModerationCommentModal (abuse: Abuse) { + this.moderationCommentModal.openModal(abuse) + } + + onModerationCommentUpdated () { + this.loadData() + } + + /* Table filter functions */ + onAbuseSearch (event: Event) { + this.onSearch(event) + this.setQueryParams((event.target as HTMLInputElement).value) + } + + setQueryParams (search: string) { + const queryParams: Params = {} + if (search) Object.assign(queryParams, { search }) + + this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams }) + } + + resetTableFilter () { + this.setTableFilter('') + this.setQueryParams('') + this.resetSearch() + } + /* END Table filter functions */ + + isAbuseAccepted (abuse: Abuse) { + return abuse.state.id === AbuseState.ACCEPTED + } + + isAbuseRejected (abuse: Abuse) { + return abuse.state.id === AbuseState.REJECTED + } + + getVideoUrl (abuse: Abuse) { + return Video.buildClientUrl(abuse.video.uuid) + } + + getVideoEmbed (abuse: Abuse) { + return buildVideoEmbed( + buildVideoLink({ + baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, + title: false, + warningTitle: false, + startTime: abuse.startAt, + stopTime: abuse.endAt + }) + ) + } + + switchToDefaultAvatar ($event: Event) { + ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() + } + + async removeAbuse (abuse: Abuse) { + const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) + if (res === false) return + + this.abuseService.removeAbuse(abuse).subscribe( + () => { + this.notifier.success(this.i18n('Abuse deleted.')) + this.loadData() + }, + + err => this.notifier.error(err.message) + ) + } + + updateAbuseState (abuse: Abuse, state: AbuseState) { + this.abuseService.updateAbuse(abuse, { state }) + .subscribe( + () => this.loadData(), + + err => this.notifier.error(err.message) + ) + } + + protected loadData () { + return this.abuseService.getAbuses({ + pagination: this.pagination, + sort: this.sort, + search: this.search + }).subscribe( + async resultList => { + this.totalRecords = resultList.total + const abuses = [] + + for (const abuse of resultList.data) { + Object.assign(abuse, { + reasonHtml: await this.toHtml(abuse.reason), + moderationCommentHtml: await this.toHtml(abuse.moderationComment), + embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)), + reporterAccount: new Account(abuse.reporterAccount) + }) + + if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) + if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt + + abuses.push(abuse as ProcessedAbuse) + } + + this.abuses = abuses + }, + + err => this.notifier.error(err.message) + ) + } + + private toHtml (text: string) { + return this.markdownRenderer.textMarkdownToHTML(text) + } +} diff --git a/client/src/app/+admin/moderation/abuse-list/index.ts b/client/src/app/+admin/moderation/abuse-list/index.ts new file mode 100644 index 000000000..c6037dab4 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/index.ts @@ -0,0 +1,3 @@ +export * from './abuse-details.component' +export * from './abuse-list.component' +export * from './moderation-comment-modal.component' diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html new file mode 100644 index 000000000..8082e93f4 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html @@ -0,0 +1,38 @@ + + + + + + diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss new file mode 100644 index 000000000..afcdb9a16 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss @@ -0,0 +1,6 @@ +@import 'variables'; +@import 'mixins'; + +textarea { + @include peertube-textarea(100%, 100px); +} diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts new file mode 100644 index 000000000..23738f9cd --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts @@ -0,0 +1,70 @@ +import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' +import { Notifier } from '@app/core' +import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms' +import { AbuseService } from '@app/shared/shared-moderation' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { Abuse } from '@shared/models' + +@Component({ + selector: 'my-moderation-comment-modal', + templateUrl: './moderation-comment-modal.component.html', + styleUrls: [ './moderation-comment-modal.component.scss' ] +}) +export class ModerationCommentModalComponent extends FormReactive implements OnInit { + @ViewChild('modal', { static: true }) modal: NgbModal + @Output() commentUpdated = new EventEmitter() + + private abuseToComment: Abuse + private openedModal: NgbModalRef + + constructor ( + protected formValidatorService: FormValidatorService, + private modalService: NgbModal, + private notifier: Notifier, + private abuseService: AbuseService, + private abuseValidatorsService: AbuseValidatorsService, + private i18n: I18n + ) { + super() + } + + ngOnInit () { + this.buildForm({ + moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT + }) + } + + openModal (abuseToComment: Abuse) { + this.abuseToComment = abuseToComment + this.openedModal = this.modalService.open(this.modal, { centered: true }) + + this.form.patchValue({ + moderationComment: this.abuseToComment.moderationComment + }) + } + + hide () { + this.abuseToComment = undefined + this.openedModal.close() + this.form.reset() + } + + async banUser () { + const moderationComment: string = this.form.value[ 'moderationComment' ] + + this.abuseService.updateAbuse(this.abuseToComment, { moderationComment }) + .subscribe( + () => { + this.notifier.success(this.i18n('Comment updated.')) + + this.commentUpdated.emit(moderationComment) + this.hide() + }, + + err => this.notifier.error(err.message) + ) + } + +} diff --git a/client/src/app/+admin/moderation/index.ts b/client/src/app/+admin/moderation/index.ts index 16249236c..53e4bc991 100644 --- a/client/src/app/+admin/moderation/index.ts +++ b/client/src/app/+admin/moderation/index.ts @@ -1,5 +1,5 @@ +export * from './abuse-list' export * from './instance-blocklist' -export * from './video-abuse-list' export * from './video-block-list' export * from './moderation.component' export * from './moderation.routes' diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index cd837bcb9..1e207e5e8 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts @@ -1,7 +1,7 @@ import { Routes } from '@angular/router' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' import { ModerationComponent } from '@app/+admin/moderation/moderation.component' -import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' +import { AbuseListComponent } from '@app/+admin/moderation/abuse-list' import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' import { UserRightGuard } from '@app/core' import { UserRight } from '@shared/models' @@ -13,20 +13,25 @@ export const ModerationRoutes: Routes = [ children: [ { path: '', - redirectTo: 'video-abuses/list', + redirectTo: 'abuses/list', pathMatch: 'full' }, { path: 'video-abuses', - redirectTo: 'video-abuses/list', + redirectTo: 'abuses/list', pathMatch: 'full' }, { path: 'video-abuses/list', - component: VideoAbuseListComponent, + redirectTo: 'abuses/list', + pathMatch: 'full' + }, + { + path: 'abuses/list', + component: AbuseListComponent, canActivate: [ UserRightGuard ], data: { - userRight: UserRight.MANAGE_VIDEO_ABUSES, + userRight: UserRight.MANAGE_ABUSES, meta: { title: 'Video reports' } diff --git a/client/src/app/+admin/moderation/video-abuse-list/index.ts b/client/src/app/+admin/moderation/video-abuse-list/index.ts deleted file mode 100644 index da7176e52..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './video-abuse-list.component' -export * from './moderation-comment-modal.component' diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html deleted file mode 100644 index 8082e93f4..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss deleted file mode 100644 index afcdb9a16..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import 'variables'; -@import 'mixins'; - -textarea { - @include peertube-textarea(100%, 100px); -} diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts deleted file mode 100644 index 3cd763ca4..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' -import { Notifier } from '@app/core' -import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms' -import { VideoAbuseService } from '@app/shared/shared-moderation' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoAbuse } from '@shared/models' - -@Component({ - selector: 'my-moderation-comment-modal', - templateUrl: './moderation-comment-modal.component.html', - styleUrls: [ './moderation-comment-modal.component.scss' ] -}) -export class ModerationCommentModalComponent extends FormReactive implements OnInit { - @ViewChild('modal', { static: true }) modal: NgbModal - @Output() commentUpdated = new EventEmitter() - - private abuseToComment: VideoAbuse - private openedModal: NgbModalRef - - constructor ( - protected formValidatorService: FormValidatorService, - private modalService: NgbModal, - private notifier: Notifier, - private videoAbuseService: VideoAbuseService, - private videoAbuseValidatorsService: VideoAbuseValidatorsService, - private i18n: I18n - ) { - super() - } - - ngOnInit () { - this.buildForm({ - moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_MODERATION_COMMENT - }) - } - - openModal (abuseToComment: VideoAbuse) { - this.abuseToComment = abuseToComment - this.openedModal = this.modalService.open(this.modal, { centered: true }) - - this.form.patchValue({ - moderationComment: this.abuseToComment.moderationComment - }) - } - - hide () { - this.abuseToComment = undefined - this.openedModal.close() - this.form.reset() - } - - async banUser () { - const moderationComment: string = this.form.value[ 'moderationComment' ] - - this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) - .subscribe( - () => { - this.notifier.success(this.i18n('Comment updated.')) - - this.commentUpdated.emit(moderationComment) - this.hide() - }, - - err => this.notifier.error(err.message) - ) - } - -} diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html deleted file mode 100644 index ec808cdb8..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html +++ /dev/null @@ -1,93 +0,0 @@ - diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts deleted file mode 100644 index 5db2887fa..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, Input } from '@angular/core' -import { Actor } from '@app/shared/shared-main' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model' -import { ProcessedVideoAbuse } from './video-abuse-list.component' -import { durationToString } from '@app/helpers' - -@Component({ - selector: 'my-video-abuse-details', - templateUrl: './video-abuse-details.component.html', - styleUrls: [ '../moderation.component.scss' ] -}) -export class VideoAbuseDetailsComponent { - @Input() videoAbuse: ProcessedVideoAbuse - - private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string } - - constructor ( - private i18n: I18n - ) { - this.predefinedReasonsTranslations = { - violentOrRepulsive: this.i18n('Violent or Repulsive'), - hatefulOrAbusive: this.i18n('Hateful or Abusive'), - spamOrMisleading: this.i18n('Spam or Misleading'), - privacy: this.i18n('Privacy'), - rights: this.i18n('Rights'), - serverRules: this.i18n('Server rules'), - thumbnails: this.i18n('Thumbnails'), - captions: this.i18n('Captions') - } - } - - get startAt () { - return durationToString(this.videoAbuse.startAt) - } - - get endAt () { - return durationToString(this.videoAbuse.endAt) - } - - getPredefinedReasons () { - if (!this.videoAbuse.predefinedReasons) return [] - return this.videoAbuse.predefinedReasons.map(r => ({ - id: r, - label: this.predefinedReasonsTranslations[r] - })) - } - - switchToDefaultAvatar ($event: Event) { - ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() - } -} diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html deleted file mode 100644 index 64641b28a..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ /dev/null @@ -1,149 +0,0 @@ - - -
-
-
- - - - Clear filters -
-
-
-
- - - - - Reporter - Video - Created - State - - - - - - - - - - - - - - -
- Avatar -
- {{ videoAbuse.reporterAccount.displayName }} - {{ videoAbuse.reporterAccount.nameWithHost }} -
-
-
- - - - -
-
- - - {{ videoAbuse.nth }}/{{ videoAbuse.count }} - -
-
-
- - - {{ videoAbuse.video.name }} -
-
by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }}
-
-
-
- - - -
-
- Deleted -
-
-
- {{ videoAbuse.video.name }} - -
-
by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }}
-
-
- - - {{ videoAbuse.createdAt | date: 'short' }} - - - - - - - - - - - -
- - - - - - - - - - - - -
- No video abuses found matching current filters. - No video abuses found. -
- - -
-
- - diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss deleted file mode 100644 index 8eee15b64..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss +++ /dev/null @@ -1,23 +0,0 @@ -@import 'mixins'; -@import 'miniature'; - -.video-details-date-updated { - font-size: 90%; - margin-top: .1rem; -} - -.video-details-links { - @include disable-default-a-behaviour; -} - -.video-abuse-states .glyphicon-comment { - margin-left: 0.5rem; -} - -.input-group { - @include peertube-input-group(300px); - - .dropdown-toggle::after { - margin-left: 0; - } -} diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts deleted file mode 100644 index 409dd42c7..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { SortMeta } from 'primeng/api' -import { filter } from 'rxjs/operators' -import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' -import { environment } from 'src/environments/environment' -import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' -import { DomSanitizer } from '@angular/platform-browser' -import { ActivatedRoute, Params, Router } from '@angular/router' -import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' -import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' -import { BlocklistService, VideoAbuseService, VideoBlockService } from '@app/shared/shared-moderation' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoAbuse, VideoAbuseState } from '@shared/models' -import { ModerationCommentModalComponent } from './moderation-comment-modal.component' - -export type ProcessedVideoAbuse = VideoAbuse & { - moderationCommentHtml?: string, - reasonHtml?: string - embedHtml?: string - updatedAt?: Date - // override bare server-side definitions with rich client-side definitions - reporterAccount: Account - video: VideoAbuse['video'] & { - channel: VideoAbuse['video']['channel'] & { - ownerAccount: Account - } - } -} - -@Component({ - selector: 'my-video-abuse-list', - templateUrl: './video-abuse-list.component.html', - styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ] -}) -export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit { - @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent - - videoAbuses: ProcessedVideoAbuse[] = [] - totalRecords = 0 - sort: SortMeta = { field: 'createdAt', order: 1 } - pagination: RestPagination = { count: this.rowsPerPage, start: 0 } - - videoAbuseActions: DropdownAction[][] = [] - - constructor ( - private notifier: Notifier, - private videoAbuseService: VideoAbuseService, - private blocklistService: BlocklistService, - private videoService: VideoService, - private videoBlocklistService: VideoBlockService, - private confirmService: ConfirmService, - private i18n: I18n, - private markdownRenderer: MarkdownService, - private sanitizer: DomSanitizer, - private route: ActivatedRoute, - private router: Router - ) { - super() - - this.videoAbuseActions = [ - [ - { - label: this.i18n('Internal actions'), - isHeader: true - }, - { - label: this.i18n('Delete report'), - handler: videoAbuse => this.removeVideoAbuse(videoAbuse) - }, - { - label: this.i18n('Add note'), - handler: videoAbuse => this.openModerationCommentModal(videoAbuse), - isDisplayed: videoAbuse => !videoAbuse.moderationComment - }, - { - label: this.i18n('Update note'), - handler: videoAbuse => this.openModerationCommentModal(videoAbuse), - isDisplayed: videoAbuse => !!videoAbuse.moderationComment - }, - { - label: this.i18n('Mark as accepted'), - handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED), - isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse) - }, - { - label: this.i18n('Mark as rejected'), - handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED), - isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse) - } - ], - [ - { - label: this.i18n('Actions for the video'), - isHeader: true, - isDisplayed: videoAbuse => !videoAbuse.video.deleted - }, - { - label: this.i18n('Block video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted, - handler: videoAbuse => { - this.videoBlocklistService.blockVideo(videoAbuse.video.id, undefined, true) - .subscribe( - () => { - this.notifier.success(this.i18n('Video blocked.')) - - this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) - }, - - err => this.notifier.error(err.message) - ) - } - }, - { - label: this.i18n('Unblock video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted, - handler: videoAbuse => { - this.videoBlocklistService.unblockVideo(videoAbuse.video.id) - .subscribe( - () => { - this.notifier.success(this.i18n('Video unblocked.')) - - this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) - }, - - err => this.notifier.error(err.message) - ) - } - }, - { - label: this.i18n('Delete video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted, - handler: async videoAbuse => { - const res = await this.confirmService.confirm( - this.i18n('Do you really want to delete this video?'), - this.i18n('Delete') - ) - if (res === false) return - - this.videoService.removeVideo(videoAbuse.video.id) - .subscribe( - () => { - this.notifier.success(this.i18n('Video deleted.')) - - this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) - }, - - err => this.notifier.error(err.message) - ) - } - } - ], - [ - { - label: this.i18n('Actions for the reporter'), - isHeader: true - }, - { - label: this.i18n('Mute reporter'), - handler: async videoAbuse => { - const account = videoAbuse.reporterAccount as Account - - this.blocklistService.blockAccountByInstance(account) - .subscribe( - () => { - this.notifier.success( - this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) - ) - - account.mutedByInstance = true - }, - - err => this.notifier.error(err.message) - ) - } - }, - { - label: this.i18n('Mute server'), - isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId, - handler: async videoAbuse => { - this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host) - .subscribe( - () => { - this.notifier.success( - this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host }) - ) - }, - - err => this.notifier.error(err.message) - ) - } - } - ] - ] - } - - ngOnInit () { - this.initialize() - - this.route.queryParams - .subscribe(params => { - this.search = params.search || '' - - this.setTableFilter(this.search) - this.loadData() - }) - } - - ngAfterViewInit () { - if (this.search) this.setTableFilter(this.search) - } - - getIdentifier () { - return 'VideoAbuseListComponent' - } - - openModerationCommentModal (videoAbuse: VideoAbuse) { - this.moderationCommentModal.openModal(videoAbuse) - } - - onModerationCommentUpdated () { - this.loadData() - } - - /* Table filter functions */ - onAbuseSearch (event: Event) { - this.onSearch(event) - this.setQueryParams((event.target as HTMLInputElement).value) - } - - setQueryParams (search: string) { - const queryParams: Params = {} - if (search) Object.assign(queryParams, { search }) - - this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams }) - } - - resetTableFilter () { - this.setTableFilter('') - this.setQueryParams('') - this.resetSearch() - } - /* END Table filter functions */ - - isVideoAbuseAccepted (videoAbuse: VideoAbuse) { - return videoAbuse.state.id === VideoAbuseState.ACCEPTED - } - - isVideoAbuseRejected (videoAbuse: VideoAbuse) { - return videoAbuse.state.id === VideoAbuseState.REJECTED - } - - getVideoUrl (videoAbuse: VideoAbuse) { - return Video.buildClientUrl(videoAbuse.video.uuid) - } - - getVideoEmbed (videoAbuse: VideoAbuse) { - return buildVideoEmbed( - buildVideoLink({ - baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`, - title: false, - warningTitle: false, - startTime: videoAbuse.startAt, - stopTime: videoAbuse.endAt - }) - ) - } - - switchToDefaultAvatar ($event: Event) { - ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() - } - - async removeVideoAbuse (videoAbuse: VideoAbuse) { - const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) - if (res === false) return - - this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( - () => { - this.notifier.success(this.i18n('Abuse deleted.')) - this.loadData() - }, - - err => this.notifier.error(err.message) - ) - } - - updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) { - this.videoAbuseService.updateVideoAbuse(videoAbuse, { state }) - .subscribe( - () => this.loadData(), - - err => this.notifier.error(err.message) - ) - } - - protected loadData () { - return this.videoAbuseService.getVideoAbuses({ - pagination: this.pagination, - sort: this.sort, - search: this.search - }).subscribe( - async resultList => { - this.totalRecords = resultList.total - const videoAbuses = [] - - for (const abuse of resultList.data) { - Object.assign(abuse, { - reasonHtml: await this.toHtml(abuse.reason), - moderationCommentHtml: await this.toHtml(abuse.moderationComment), - embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)), - reporterAccount: new Account(abuse.reporterAccount) - }) - - if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) - if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt - - videoAbuses.push(abuse as ProcessedVideoAbuse) - } - - this.videoAbuses = videoAbuses - }, - - err => this.notifier.error(err.message) - ) - } - - private toHtml (text: string) { - return this.markdownRenderer.textMarkdownToHTML(text) - } -} diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts index cfa514b26..adc18b587 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts @@ -47,7 +47,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] this.rightNotifications = { - videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES, + videoAbuseAsModerator: UserRight.MANAGE_ABUSES, videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, newUserRegistration: UserRight.MANAGE_USERS, newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 2dbe695c9..0ea251f1c 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts @@ -28,7 +28,7 @@ export class MenuComponent implements OnInit { private routesPerRight: { [ role in UserRight ]?: string } = { [UserRight.MANAGE_USERS]: '/admin/users', [UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends', - [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/moderation/video-abuses', + [UserRight.MANAGE_ABUSES]: '/admin/moderation/abuses', [UserRight.MANAGE_VIDEO_BLACKLIST]: '/admin/moderation/video-blocks', [UserRight.MANAGE_JOBS]: '/admin/jobs', [UserRight.MANAGE_CONFIGURATION]: '/admin/config' @@ -126,7 +126,7 @@ export class MenuComponent implements OnInit { const adminRights = [ UserRight.MANAGE_USERS, UserRight.MANAGE_SERVER_FOLLOW, - UserRight.MANAGE_VIDEO_ABUSES, + UserRight.MANAGE_ABUSES, UserRight.MANAGE_VIDEO_BLACKLIST, UserRight.MANAGE_JOBS, UserRight.MANAGE_CONFIGURATION diff --git a/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts new file mode 100644 index 000000000..739115e19 --- /dev/null +++ b/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts @@ -0,0 +1,30 @@ +import { I18n } from '@ngx-translate/i18n-polyfill' +import { Validators } from '@angular/forms' +import { Injectable } from '@angular/core' +import { BuildFormValidator } from './form-validator.service' + +@Injectable() +export class AbuseValidatorsService { + readonly ABUSE_REASON: BuildFormValidator + readonly ABUSE_MODERATION_COMMENT: BuildFormValidator + + constructor (private i18n: I18n) { + this.ABUSE_REASON = { + VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], + MESSAGES: { + 'required': this.i18n('Report reason is required.'), + 'minlength': this.i18n('Report reason must be at least 2 characters long.'), + 'maxlength': this.i18n('Report reason cannot be more than 3000 characters long.') + } + } + + this.ABUSE_MODERATION_COMMENT = { + VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], + MESSAGES: { + 'required': this.i18n('Moderation comment is required.'), + 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'), + 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.') + } + } + } +} diff --git a/client/src/app/shared/shared-forms/form-validators/index.ts b/client/src/app/shared/shared-forms/form-validators/index.ts index 8b71841a9..b06a326ff 100644 --- a/client/src/app/shared/shared-forms/form-validators/index.ts +++ b/client/src/app/shared/shared-forms/form-validators/index.ts @@ -1,3 +1,4 @@ +export * from './abuse-validators.service' export * from './batch-domains-validators.service' export * from './custom-config-validators.service' export * from './form-validator.service' @@ -6,7 +7,6 @@ export * from './instance-validators.service' export * from './login-validators.service' export * from './reset-password-validators.service' export * from './user-validators.service' -export * from './video-abuse-validators.service' export * from './video-accept-ownership-validators.service' export * from './video-block-validators.service' export * from './video-captions-validators.service' diff --git a/client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts deleted file mode 100644 index aae56d607..000000000 --- a/client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { I18n } from '@ngx-translate/i18n-polyfill' -import { Validators } from '@angular/forms' -import { Injectable } from '@angular/core' -import { BuildFormValidator } from './form-validator.service' - -@Injectable() -export class VideoAbuseValidatorsService { - readonly VIDEO_ABUSE_REASON: BuildFormValidator - readonly VIDEO_ABUSE_MODERATION_COMMENT: BuildFormValidator - - constructor (private i18n: I18n) { - this.VIDEO_ABUSE_REASON = { - VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], - MESSAGES: { - 'required': this.i18n('Report reason is required.'), - 'minlength': this.i18n('Report reason must be at least 2 characters long.'), - 'maxlength': this.i18n('Report reason cannot be more than 3000 characters long.') - } - } - - this.VIDEO_ABUSE_MODERATION_COMMENT = { - VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], - MESSAGES: { - 'required': this.i18n('Moderation comment is required.'), - 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'), - 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.') - } - } - } -} diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts index e82fa97d4..ba33704cf 100644 --- a/client/src/app/shared/shared-forms/shared-form.module.ts +++ b/client/src/app/shared/shared-forms/shared-form.module.ts @@ -11,7 +11,7 @@ import { LoginValidatorsService, ResetPasswordValidatorsService, UserValidatorsService, - VideoAbuseValidatorsService, + AbuseValidatorsService, VideoAcceptOwnershipValidatorsService, VideoBlockValidatorsService, VideoCaptionsValidatorsService, @@ -69,7 +69,7 @@ import { TimestampInputComponent } from './timestamp-input.component' LoginValidatorsService, ResetPasswordValidatorsService, UserValidatorsService, - VideoAbuseValidatorsService, + AbuseValidatorsService, VideoAcceptOwnershipValidatorsService, VideoBlockValidatorsService, VideoCaptionsValidatorsService, diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 5fc7989dd..0fa161ce6 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts @@ -14,7 +14,7 @@ export abstract class Actor implements ActorServer { avatarUrl: string - static GET_ACTOR_AVATAR_URL (actor: { avatar?: Avatar }) { + static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { if (actor?.avatar?.url) return actor.avatar.url if (actor && actor.avatar) { diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index de25d3ab9..389a242fd 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts @@ -25,9 +25,20 @@ export class UserNotification implements UserNotificationServer { video: VideoInfo } - videoAbuse?: { + abuse?: { id: number - video: VideoInfo + + video?: VideoInfo + + comment?: { + threadId: number + + video: { + uuid: string + } + } + + account?: ActorInfo } videoBlacklist?: { @@ -55,7 +66,7 @@ export class UserNotification implements UserNotificationServer { // Additional fields videoUrl?: string commentUrl?: any[] - videoAbuseUrl?: string + abuseUrl?: string videoAutoBlacklistUrl?: string accountUrl?: string videoImportIdentifier?: string @@ -78,7 +89,7 @@ export class UserNotification implements UserNotificationServer { this.comment = hash.comment if (this.comment) this.setAvatarUrl(this.comment.account) - this.videoAbuse = hash.videoAbuse + this.abuse = hash.abuse this.videoBlacklist = hash.videoBlacklist @@ -108,8 +119,9 @@ export class UserNotification implements UserNotificationServer { break case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS: - this.videoAbuseUrl = '/admin/moderation/video-abuses/list' - this.videoUrl = this.buildVideoUrl(this.videoAbuse.video) + this.abuseUrl = '/admin/moderation/abuses/list' + + if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) break case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: @@ -178,7 +190,7 @@ export class UserNotification implements UserNotificationServer { return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName } - private setAvatarUrl (actor: { avatarUrl?: string, avatar?: Avatar }) { + private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) { actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) } } diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index d5be1470e..8d31eab0d 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html @@ -19,7 +19,7 @@ - +
The notification concerns a video now unavailable
@@ -46,7 +46,7 @@ @@ -65,7 +65,7 @@ - + @@ -73,7 +73,7 @@ - +
The notification concerns a comment now unavailable
diff --git a/client/src/app/shared/shared-moderation/abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts new file mode 100644 index 000000000..f45018d5c --- /dev/null +++ b/client/src/app/shared/shared-moderation/abuse.service.ts @@ -0,0 +1,98 @@ +import { omit } from 'lodash-es' +import { SortMeta } from 'primeng/api' +import { Observable } from 'rxjs' +import { catchError, map } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService } from '@app/core' +import { AbuseUpdate, ResultList, Abuse, AbuseCreate, AbuseState } from '@shared/models' +import { environment } from '../../../environments/environment' + +@Injectable() +export class AbuseService { + private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses' + + constructor ( + private authHttp: HttpClient, + private restService: RestService, + private restExtractor: RestExtractor + ) {} + + getAbuses (options: { + pagination: RestPagination, + sort: SortMeta, + search?: string + }): Observable> { + const { pagination, sort, search } = options + const url = AbuseService.BASE_ABUSE_URL + 'abuse' + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (search) { + const filters = this.restService.parseQueryStringFilter(search, { + id: { prefix: '#' }, + state: { + prefix: 'state:', + handler: v => { + if (v === 'accepted') return AbuseState.ACCEPTED + if (v === 'pending') return AbuseState.PENDING + if (v === 'rejected') return AbuseState.REJECTED + + return undefined + } + }, + videoIs: { + prefix: 'videoIs:', + handler: v => { + if (v === 'deleted') return v + if (v === 'blacklisted') return v + + return undefined + } + }, + searchReporter: { prefix: 'reporter:' }, + searchReportee: { prefix: 'reportee:' }, + predefinedReason: { prefix: 'tag:' } + }) + + params = this.restService.addObjectParams(params, filters) + } + + return this.authHttp.get>(url, { params }) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } + + reportVideo (parameters: AbuseCreate) { + const url = AbuseService.BASE_ABUSE_URL + + const body = omit(parameters, [ 'id' ]) + + return this.authHttp.post(url, body) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) { + const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + + return this.authHttp.put(url, abuseUpdate) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + removeAbuse (abuse: Abuse) { + const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + + return this.authHttp.delete(url) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + }} diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts index 8e74254f6..d6c4a10be 100644 --- a/client/src/app/shared/shared-moderation/index.ts +++ b/client/src/app/shared/shared-moderation/index.ts @@ -1,3 +1,4 @@ +export * from './abuse.service' export * from './account-block.model' export * from './account-blocklist.component' export * from './batch-domains-modal.component' @@ -6,7 +7,6 @@ export * from './bulk.service' export * from './server-blocklist.component' export * from './user-ban-modal.component' export * from './user-moderation-dropdown.component' -export * from './video-abuse.service' export * from './video-block.component' export * from './video-block.service' export * from './video-report.component' diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts index f7e64dfa3..742193e58 100644 --- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts +++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts @@ -8,7 +8,7 @@ import { BlocklistService } from './blocklist.service' import { BulkService } from './bulk.service' import { UserBanModalComponent } from './user-ban-modal.component' import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' -import { VideoAbuseService } from './video-abuse.service' +import { AbuseService } from './abuse.service' import { VideoBlockComponent } from './video-block.component' import { VideoBlockService } from './video-block.service' import { VideoReportComponent } from './video-report.component' @@ -39,7 +39,7 @@ import { VideoReportComponent } from './video-report.component' providers: [ BlocklistService, BulkService, - VideoAbuseService, + AbuseService, VideoBlockService ] }) diff --git a/client/src/app/shared/shared-moderation/video-abuse.service.ts b/client/src/app/shared/shared-moderation/video-abuse.service.ts deleted file mode 100644 index 44dea44a5..000000000 --- a/client/src/app/shared/shared-moderation/video-abuse.service.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { omit } from 'lodash-es' -import { SortMeta } from 'primeng/api' -import { Observable } from 'rxjs' -import { catchError, map } from 'rxjs/operators' -import { HttpClient, HttpParams } from '@angular/common/http' -import { Injectable } from '@angular/core' -import { RestExtractor, RestPagination, RestService } from '@app/core' -import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '@shared/models' -import { environment } from '../../../environments/environment' - -@Injectable() -export class VideoAbuseService { - private static BASE_VIDEO_ABUSE_URL = environment.apiUrl + '/api/v1/videos/' - - constructor ( - private authHttp: HttpClient, - private restService: RestService, - private restExtractor: RestExtractor - ) {} - - getVideoAbuses (options: { - pagination: RestPagination, - sort: SortMeta, - search?: string - }): Observable> { - const { pagination, sort, search } = options - const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse' - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - if (search) { - const filters = this.restService.parseQueryStringFilter(search, { - id: { prefix: '#' }, - state: { - prefix: 'state:', - handler: v => { - if (v === 'accepted') return VideoAbuseState.ACCEPTED - if (v === 'pending') return VideoAbuseState.PENDING - if (v === 'rejected') return VideoAbuseState.REJECTED - - return undefined - } - }, - videoIs: { - prefix: 'videoIs:', - handler: v => { - if (v === 'deleted') return v - if (v === 'blacklisted') return v - - return undefined - } - }, - searchReporter: { prefix: 'reporter:' }, - searchReportee: { prefix: 'reportee:' }, - predefinedReason: { prefix: 'tag:' } - }) - - params = this.restService.addObjectParams(params, filters) - } - - return this.authHttp.get>(url, { params }) - .pipe( - catchError(res => this.restExtractor.handleError(res)) - ) - } - - reportVideo (parameters: { id: number } & VideoAbuseCreate) { - const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse' - - const body = omit(parameters, [ 'id' ]) - - return this.authHttp.post(url, body) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(res => this.restExtractor.handleError(res)) - ) - } - - updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) { - const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id - - return this.authHttp.put(url, abuseUpdate) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(res => this.restExtractor.handleError(res)) - ) - } - - removeVideoAbuse (videoAbuse: VideoAbuse) { - const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id - - return this.authHttp.delete(url) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(res => this.restExtractor.handleError(res)) - ) - }} diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/video-report.component.ts index 11c805636..b8d9f8d27 100644 --- a/client/src/app/shared/shared-moderation/video-report.component.ts +++ b/client/src/app/shared/shared-moderation/video-report.component.ts @@ -3,13 +3,13 @@ import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' import { Component, Input, OnInit, ViewChild } from '@angular/core' import { DomSanitizer, SafeHtml } from '@angular/platform-browser' import { Notifier } from '@app/core' -import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms' +import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' import { I18n } from '@ngx-translate/i18n-polyfill' -import { videoAbusePredefinedReasonsMap, VideoAbusePredefinedReasonsString } from '@shared/models/videos/abuse/video-abuse-reason.model' +import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models' import { Video } from '../shared-main' -import { VideoAbuseService } from './video-abuse.service' +import { AbuseService } from './abuse.service' @Component({ selector: 'my-video-report', @@ -22,7 +22,7 @@ export class VideoReportComponent extends FormReactive implements OnInit { @ViewChild('modal', { static: true }) modal: NgbModal error: string = null - predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] + predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] embedHtml: SafeHtml private openedModal: NgbModalRef @@ -30,8 +30,8 @@ export class VideoReportComponent extends FormReactive implements OnInit { constructor ( protected formValidatorService: FormValidatorService, private modalService: NgbModal, - private videoAbuseValidatorsService: VideoAbuseValidatorsService, - private videoAbuseService: VideoAbuseService, + private abuseValidatorsService: AbuseValidatorsService, + private abuseService: AbuseService, private notifier: Notifier, private sanitizer: DomSanitizer, private i18n: I18n @@ -69,8 +69,8 @@ export class VideoReportComponent extends FormReactive implements OnInit { ngOnInit () { this.buildForm({ - reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON, - predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null), + reason: this.abuseValidatorsService.ABUSE_REASON, + predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null), timestamp: { hasStart: null, startAt: null, @@ -136,15 +136,18 @@ export class VideoReportComponent extends FormReactive implements OnInit { report () { const reason = this.form.get('reason').value - const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[] + const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[] const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value - this.videoAbuseService.reportVideo({ - id: this.video.id, + this.abuseService.reportVideo({ + accountId: this.video.account.id, reason, predefinedReasons, - startAt: hasStart && startAt ? startAt : undefined, - endAt: hasEnd && endAt ? endAt : undefined + video: { + id: this.video.id, + startAt: hasStart && startAt ? startAt : undefined, + endAt: hasEnd && endAt ? endAt : undefined + } }).subscribe( () => { this.notifier.success(this.i18n('Video reported.')) -- cgit v1.2.3