From d95d15598847c7f020aa056e7e6e0c02d2bbf732 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 1 Jul 2020 16:05:30 +0200 Subject: [PATCH] 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-details.component.ts} | 22 +- .../abuse-list.component.html} | 64 ++-- .../abuse-list.component.scss} | 0 .../abuse-list.component.ts} | 127 +++---- .../app/+admin/moderation/abuse-list/index.ts | 3 + .../moderation-comment-modal.component.html | 0 .../moderation-comment-modal.component.scss | 0 .../moderation-comment-modal.component.ts | 18 +- client/src/app/+admin/moderation/index.ts | 2 +- .../+admin/moderation/moderation.routes.ts | 15 +- .../moderation/video-abuse-list/index.ts | 2 - .../video-abuse-details.component.html | 93 ----- ...ount-notification-preferences.component.ts | 2 +- client/src/app/menu/menu.component.ts | 4 +- ...service.ts => abuse-validators.service.ts} | 10 +- .../shared-forms/form-validators/index.ts | 2 +- .../shared/shared-forms/shared-form.module.ts | 4 +- .../shared/shared-main/account/actor.model.ts | 2 +- .../users/user-notification.model.ts | 26 +- .../users/user-notifications.component.html | 8 +- ...ideo-abuse.service.ts => abuse.service.ts} | 32 +- .../src/app/shared/shared-moderation/index.ts | 2 +- .../shared-moderation.module.ts | 4 +- .../video-report.component.ts | 29 +- server/controllers/api/abuse.ts | 168 +++++++++ server/controllers/api/index.ts | 2 + server/controllers/api/videos/abuse.ts | 109 ++---- server/helpers/audit-logger.ts | 27 +- server/helpers/custom-validators/abuses.ts | 54 +++ .../custom-validators/activitypub/flag.ts | 4 +- .../helpers/custom-validators/video-abuses.ts | 56 --- .../{video-abuses.ts => abuses.ts} | 18 +- server/helpers/middlewares/index.ts | 2 +- server/initializers/constants.ts | 27 +- server/initializers/database.ts | 41 ++- .../migrations/0250-video-abuse-state.ts | 4 +- .../lib/activitypub/process/process-flag.ts | 117 ++++--- server/lib/activitypub/send/send-flag.ts | 31 +- server/lib/activitypub/url.ts | 10 +- server/lib/emailer.ts | 110 ++++-- server/lib/emails/account-abuse-new/html.pug | 14 + server/lib/emails/common/mixins.pug | 6 +- server/lib/emails/video-abuse-new/html.pug | 8 +- .../emails/video-comment-abuse-new/html.pug | 15 + server/lib/moderation.ts | 164 ++++++++- server/lib/notifier.ts | 43 ++- server/middlewares/validators/abuse.ts | 253 ++++++++++++++ server/middlewares/validators/index.ts | 1 + server/middlewares/validators/sort.ts | 6 +- server/middlewares/validators/videos/index.ts | 1 - .../validators/videos/video-abuses.ts | 135 -------- .../{video/video-abuse.ts => abuse/abuse.ts} | 327 +++++++++++------- server/models/abuse/video-abuse.ts | 63 ++++ server/models/abuse/video-comment-abuse.ts | 53 +++ server/models/account/account-blocklist.ts | 10 +- server/models/account/account.ts | 4 + server/models/account/user-notification.ts | 100 ++++-- server/models/account/user.ts | 4 +- server/models/server/server-blocklist.ts | 10 +- server/models/video/video.ts | 84 ++--- server/tests/api/check-params/video-abuses.ts | 9 +- server/tests/api/users/users.ts | 4 +- server/tests/api/videos/video-abuse.ts | 54 +-- server/types/models/index.ts | 1 + server/types/models/moderation/abuse.ts | 97 ++++++ server/types/models/moderation/index.ts | 1 + server/types/models/user/user-notification.ts | 32 +- server/types/models/video/index.ts | 1 - server/types/models/video/video-abuse.ts | 35 -- server/typings/express/index.d.ts | 4 +- shared/extra-utils/index.ts | 1 + shared/extra-utils/moderation/abuses.ts | 112 ++++++ .../extra-utils/users/user-notifications.ts | 6 +- shared/extra-utils/videos/video-abuses.ts | 18 +- shared/models/activitypub/activity.ts | 10 +- ...{video-abuse-object.ts => abuse-object.ts} | 4 +- .../activitypub/objects/common-objects.ts | 4 +- shared/models/activitypub/objects/index.ts | 4 +- shared/models/index.ts | 3 +- .../moderation/abuse/abuse-create.model.ts | 26 ++ .../models/moderation/abuse/abuse-filter.ts | 1 + .../moderation/abuse/abuse-reason.model.ts | 33 ++ .../abuse/abuse-state.model.ts} | 2 +- .../moderation/abuse/abuse-update.model.ts | 7 + .../moderation/abuse/abuse-video-is.type.ts | 1 + shared/models/moderation/abuse/abuse.model.ts | 53 +++ shared/models/moderation/abuse/index.ts | 6 + .../account-block.model.ts | 0 .../models/{blocklist => moderation}/index.ts | 1 + .../server-block.model.ts | 0 .../models/users/user-notification.model.ts | 15 +- shared/models/users/user-right.enum.ts | 2 +- shared/models/users/user-role.ts | 2 +- shared/models/videos/abuse/index.ts | 6 - .../videos/abuse/video-abuse-create.model.ts | 8 - .../videos/abuse/video-abuse-reason.model.ts | 33 -- .../videos/abuse/video-abuse-update.model.ts | 6 - .../videos/abuse/video-abuse-video-is.type.ts | 1 - .../models/videos/abuse/video-abuse.model.ts | 38 -- shared/models/videos/index.ts | 1 - 103 files changed, 2137 insertions(+), 1162 deletions(-) create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-details.component.html rename client/src/app/+admin/moderation/{video-abuse-list/video-abuse-details.component.ts => abuse-list/abuse-details.component.ts} (59%) rename client/src/app/+admin/moderation/{video-abuse-list/video-abuse-list.component.html => abuse-list/abuse-list.component.html} (52%) rename client/src/app/+admin/moderation/{video-abuse-list/video-abuse-list.component.scss => abuse-list/abuse-list.component.scss} (100%) rename client/src/app/+admin/moderation/{video-abuse-list/video-abuse-list.component.ts => abuse-list/abuse-list.component.ts} (64%) create mode 100644 client/src/app/+admin/moderation/abuse-list/index.ts rename client/src/app/+admin/moderation/{video-abuse-list => abuse-list}/moderation-comment-modal.component.html (100%) rename client/src/app/+admin/moderation/{video-abuse-list => abuse-list}/moderation-comment-modal.component.scss (100%) rename client/src/app/+admin/moderation/{video-abuse-list => abuse-list}/moderation-comment-modal.component.ts (73%) delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/index.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html rename client/src/app/shared/shared-forms/form-validators/{video-abuse-validators.service.ts => abuse-validators.service.ts} (81%) rename client/src/app/shared/shared-moderation/{video-abuse.service.ts => abuse.service.ts} (67%) create mode 100644 server/controllers/api/abuse.ts create mode 100644 server/helpers/custom-validators/abuses.ts delete mode 100644 server/helpers/custom-validators/video-abuses.ts rename server/helpers/middlewares/{video-abuses.ts => abuses.ts} (56%) create mode 100644 server/lib/emails/account-abuse-new/html.pug create mode 100644 server/lib/emails/video-comment-abuse-new/html.pug create mode 100644 server/middlewares/validators/abuse.ts delete mode 100644 server/middlewares/validators/videos/video-abuses.ts rename server/models/{video/video-abuse.ts => abuse/abuse.ts} (52%) create mode 100644 server/models/abuse/video-abuse.ts create mode 100644 server/models/abuse/video-comment-abuse.ts create mode 100644 server/types/models/moderation/abuse.ts create mode 100644 server/types/models/moderation/index.ts delete mode 100644 server/types/models/video/video-abuse.ts create mode 100644 shared/extra-utils/moderation/abuses.ts rename shared/models/activitypub/objects/{video-abuse-object.ts => abuse-object.ts} (84%) create mode 100644 shared/models/moderation/abuse/abuse-create.model.ts create mode 100644 shared/models/moderation/abuse/abuse-filter.ts create mode 100644 shared/models/moderation/abuse/abuse-reason.model.ts rename shared/models/{videos/abuse/video-abuse-state.model.ts => moderation/abuse/abuse-state.model.ts} (61%) create mode 100644 shared/models/moderation/abuse/abuse-update.model.ts create mode 100644 shared/models/moderation/abuse/abuse-video-is.type.ts create mode 100644 shared/models/moderation/abuse/abuse.model.ts create mode 100644 shared/models/moderation/abuse/index.ts rename shared/models/{blocklist => moderation}/account-block.model.ts (100%) rename shared/models/{blocklist => moderation}/index.ts (75%) rename shared/models/{blocklist => moderation}/server-block.model.ts (100%) delete mode 100644 shared/models/videos/abuse/index.ts delete mode 100644 shared/models/videos/abuse/video-abuse-create.model.ts delete mode 100644 shared/models/videos/abuse/video-abuse-reason.model.ts delete mode 100644 shared/models/videos/abuse/video-abuse-update.model.ts delete mode 100644 shared/models/videos/abuse/video-abuse-video-is.type.ts delete mode 100644 shared/models/videos/abuse/video-abuse.model.ts 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/video-abuse-list/video-abuse-details.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts similarity index 59% rename from client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts rename to client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts index 5db2887fa..8f87630b8 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts @@ -1,19 +1,19 @@ 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 { AbusePredefinedReasonsString } from '@shared/models' +import { ProcessedAbuse } from './abuse-list.component' import { durationToString } from '@app/helpers' @Component({ - selector: 'my-video-abuse-details', - templateUrl: './video-abuse-details.component.html', + selector: 'my-abuse-details', + templateUrl: './abuse-details.component.html', styleUrls: [ '../moderation.component.scss' ] }) -export class VideoAbuseDetailsComponent { - @Input() videoAbuse: ProcessedVideoAbuse +export class AbuseDetailsComponent { + @Input() abuse: ProcessedAbuse - private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string } + private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } constructor ( private i18n: I18n @@ -31,16 +31,16 @@ export class VideoAbuseDetailsComponent { } get startAt () { - return durationToString(this.videoAbuse.startAt) + return durationToString(this.abuse.startAt) } get endAt () { - return durationToString(this.videoAbuse.endAt) + return durationToString(this.abuse.endAt) } getPredefinedReasons () { - if (!this.videoAbuse.predefinedReasons) return [] - return this.videoAbuse.predefinedReasons.map(r => ({ + if (!this.abuse.predefinedReasons) return [] + return this.abuse.predefinedReasons.map(r => ({ id: r, label: this.predefinedReasonsTranslations[r] })) diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html similarity index 52% rename from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html rename to client/src/app/+admin/moderation/abuse-list/abuse-list.component.html index 64641b28a..167f32fe6 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html @@ -1,5 +1,5 @@ - Unsolved reports - Accepted reports - Refused reports - Reports with blocked videos - Reports with deleted videos + Unsolved reports + Accepted reports + Refused reports + Reports with blocked videos + Reports with deleted videos - + - + - +
Avatar
- {{ videoAbuse.reporterAccount.displayName }} - {{ videoAbuse.reporterAccount.nameWithHost }} + {{ abuse.reporterAccount.displayName }} + {{ abuse.reporterAccount.nameWithHost }}
- - + +
- + - {{ videoAbuse.nth }}/{{ videoAbuse.count }} + {{ abuse.nth }}/{{ abuse.count }}
- - - {{ videoAbuse.video.name }} + + + {{ abuse.video.name }}
-
by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }}
+
by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }}
- +
Deleted
- {{ videoAbuse.video.name }} + {{ abuse.video.name }}
-
by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }}
+
by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }}
- {{ videoAbuse.createdAt | date: 'short' }} + {{ abuse.createdAt | date: 'short' }} - - - - + + + +
- + - + diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss similarity index 100% rename from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss rename to client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts similarity index 64% rename from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts rename to client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts index 409dd42c7..427ec4d5d 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts @@ -1,5 +1,4 @@ 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' @@ -7,43 +6,45 @@ 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 { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoAbuse, VideoAbuseState } from '@shared/models' +import { Abuse, AbuseState } from '@shared/models' import { ModerationCommentModalComponent } from './moderation-comment-modal.component' -export type ProcessedVideoAbuse = VideoAbuse & { +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: VideoAbuse['video'] & { - channel: VideoAbuse['video']['channel'] & { + + video: Abuse['video'] & { + channel: Abuse['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' ] + selector: 'my-abuse-list', + templateUrl: './abuse-list.component.html', + styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ] }) -export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit { +export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent - videoAbuses: ProcessedVideoAbuse[] = [] + abuses: ProcessedAbuse[] = [] totalRecords = 0 sort: SortMeta = { field: 'createdAt', order: 1 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 } - videoAbuseActions: DropdownAction[][] = [] + abuseActions: DropdownAction[][] = [] constructor ( private notifier: Notifier, - private videoAbuseService: VideoAbuseService, + private abuseService: AbuseService, private blocklistService: BlocklistService, private videoService: VideoService, private videoBlocklistService: VideoBlockService, @@ -56,7 +57,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV ) { super() - this.videoAbuseActions = [ + this.abuseActions = [ [ { label: this.i18n('Internal actions'), @@ -64,45 +65,45 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV }, { label: this.i18n('Delete report'), - handler: videoAbuse => this.removeVideoAbuse(videoAbuse) + handler: abuse => this.removeAbuse(abuse) }, { label: this.i18n('Add note'), - handler: videoAbuse => this.openModerationCommentModal(videoAbuse), - isDisplayed: videoAbuse => !videoAbuse.moderationComment + handler: abuse => this.openModerationCommentModal(abuse), + isDisplayed: abuse => !abuse.moderationComment }, { label: this.i18n('Update note'), - handler: videoAbuse => this.openModerationCommentModal(videoAbuse), - isDisplayed: videoAbuse => !!videoAbuse.moderationComment + handler: abuse => this.openModerationCommentModal(abuse), + isDisplayed: abuse => !!abuse.moderationComment }, { label: this.i18n('Mark as accepted'), - handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED), - isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse) + handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED), + isDisplayed: abuse => !this.isAbuseAccepted(abuse) }, { label: this.i18n('Mark as rejected'), - handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED), - isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse) + handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED), + isDisplayed: abuse => !this.isAbuseRejected(abuse) } ], [ { label: this.i18n('Actions for the video'), isHeader: true, - isDisplayed: videoAbuse => !videoAbuse.video.deleted + isDisplayed: abuse => !abuse.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) + 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.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) + this.updateAbuseState(abuse, AbuseState.ACCEPTED) }, err => this.notifier.error(err.message) @@ -111,14 +112,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV }, { label: this.i18n('Unblock video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted, - handler: videoAbuse => { - this.videoBlocklistService.unblockVideo(videoAbuse.video.id) + 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.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) + this.updateAbuseState(abuse, AbuseState.ACCEPTED) }, err => this.notifier.error(err.message) @@ -127,20 +128,20 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV }, { label: this.i18n('Delete video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted, - handler: async videoAbuse => { + 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(videoAbuse.video.id) + this.videoService.removeVideo(abuse.video.id) .subscribe( () => { this.notifier.success(this.i18n('Video deleted.')) - this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) + this.updateAbuseState(abuse, AbuseState.ACCEPTED) }, err => this.notifier.error(err.message) @@ -155,8 +156,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV }, { label: this.i18n('Mute reporter'), - handler: async videoAbuse => { - const account = videoAbuse.reporterAccount as Account + handler: async abuse => { + const account = abuse.reporterAccount as Account this.blocklistService.blockAccountByInstance(account) .subscribe( @@ -174,13 +175,13 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV }, { label: this.i18n('Mute server'), - isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId, - handler: async videoAbuse => { - this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host) + 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: videoAbuse.reporterAccount.host }) + this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host }) ) }, @@ -209,11 +210,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV } getIdentifier () { - return 'VideoAbuseListComponent' + return 'AbuseListComponent' } - openModerationCommentModal (videoAbuse: VideoAbuse) { - this.moderationCommentModal.openModal(videoAbuse) + openModerationCommentModal (abuse: Abuse) { + this.moderationCommentModal.openModal(abuse) } onModerationCommentUpdated () { @@ -240,26 +241,26 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV } /* END Table filter functions */ - isVideoAbuseAccepted (videoAbuse: VideoAbuse) { - return videoAbuse.state.id === VideoAbuseState.ACCEPTED + isAbuseAccepted (abuse: Abuse) { + return abuse.state.id === AbuseState.ACCEPTED } - isVideoAbuseRejected (videoAbuse: VideoAbuse) { - return videoAbuse.state.id === VideoAbuseState.REJECTED + isAbuseRejected (abuse: Abuse) { + return abuse.state.id === AbuseState.REJECTED } - getVideoUrl (videoAbuse: VideoAbuse) { - return Video.buildClientUrl(videoAbuse.video.uuid) + getVideoUrl (abuse: Abuse) { + return Video.buildClientUrl(abuse.video.uuid) } - getVideoEmbed (videoAbuse: VideoAbuse) { + getVideoEmbed (abuse: Abuse) { return buildVideoEmbed( buildVideoLink({ - baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`, + baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, title: false, warningTitle: false, - startTime: videoAbuse.startAt, - stopTime: videoAbuse.endAt + startTime: abuse.startAt, + stopTime: abuse.endAt }) ) } @@ -268,11 +269,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() } - async removeVideoAbuse (videoAbuse: VideoAbuse) { + 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.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( + this.abuseService.removeAbuse(abuse).subscribe( () => { this.notifier.success(this.i18n('Abuse deleted.')) this.loadData() @@ -282,8 +283,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV ) } - updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) { - this.videoAbuseService.updateVideoAbuse(videoAbuse, { state }) + updateAbuseState (abuse: Abuse, state: AbuseState) { + this.abuseService.updateAbuse(abuse, { state }) .subscribe( () => this.loadData(), @@ -292,14 +293,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV } protected loadData () { - return this.videoAbuseService.getVideoAbuses({ + return this.abuseService.getAbuses({ pagination: this.pagination, sort: this.sort, search: this.search }).subscribe( async resultList => { this.totalRecords = resultList.total - const videoAbuses = [] + const abuses = [] for (const abuse of resultList.data) { Object.assign(abuse, { @@ -312,10 +313,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV 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) + abuses.push(abuse as ProcessedAbuse) } - this.videoAbuses = videoAbuses + this.abuses = abuses }, err => this.notifier.error(err.message) 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/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html similarity index 100% rename from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html rename to client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss similarity index 100% rename from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss rename to client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts similarity index 73% rename from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts rename to client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts index 3cd763ca4..23738f9cd 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts +++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts @@ -1,11 +1,11 @@ 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 { 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 { VideoAbuse } from '@shared/models' +import { Abuse } from '@shared/models' @Component({ selector: 'my-moderation-comment-modal', @@ -16,15 +16,15 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI @ViewChild('modal', { static: true }) modal: NgbModal @Output() commentUpdated = new EventEmitter() - private abuseToComment: VideoAbuse + private abuseToComment: Abuse private openedModal: NgbModalRef constructor ( protected formValidatorService: FormValidatorService, private modalService: NgbModal, private notifier: Notifier, - private videoAbuseService: VideoAbuseService, - private videoAbuseValidatorsService: VideoAbuseValidatorsService, + private abuseService: AbuseService, + private abuseValidatorsService: AbuseValidatorsService, private i18n: I18n ) { super() @@ -32,11 +32,11 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI ngOnInit () { this.buildForm({ - moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_MODERATION_COMMENT + moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT }) } - openModal (abuseToComment: VideoAbuse) { + openModal (abuseToComment: Abuse) { this.abuseToComment = abuseToComment this.openedModal = this.modalService.open(this.modal, { centered: true }) @@ -54,7 +54,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI async banUser () { const moderationComment: string = this.form.value[ 'moderationComment' ] - this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) + this.abuseService.updateAbuse(this.abuseToComment, { moderationComment }) .subscribe( () => { this.notifier.success(this.i18n('Comment updated.')) 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/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/+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/video-abuse-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts similarity index 81% rename from client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts rename to client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts index aae56d607..739115e19 100644 --- a/client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts +++ b/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts @@ -4,12 +4,12 @@ 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 +export class AbuseValidatorsService { + readonly ABUSE_REASON: BuildFormValidator + readonly ABUSE_MODERATION_COMMENT: BuildFormValidator constructor (private i18n: I18n) { - this.VIDEO_ABUSE_REASON = { + this.ABUSE_REASON = { VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], MESSAGES: { 'required': this.i18n('Report reason is required.'), @@ -18,7 +18,7 @@ export class VideoAbuseValidatorsService { } } - this.VIDEO_ABUSE_MODERATION_COMMENT = { + this.ABUSE_MODERATION_COMMENT = { VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], MESSAGES: { 'required': this.i18n('Moderation comment is required.'), 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/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/video-abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts similarity index 67% rename from client/src/app/shared/shared-moderation/video-abuse.service.ts rename to client/src/app/shared/shared-moderation/abuse.service.ts index 44dea44a5..f45018d5c 100644 --- a/client/src/app/shared/shared-moderation/video-abuse.service.ts +++ b/client/src/app/shared/shared-moderation/abuse.service.ts @@ -5,12 +5,12 @@ 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 { AbuseUpdate, ResultList, Abuse, AbuseCreate, AbuseState } from '@shared/models' import { environment } from '../../../environments/environment' @Injectable() -export class VideoAbuseService { - private static BASE_VIDEO_ABUSE_URL = environment.apiUrl + '/api/v1/videos/' +export class AbuseService { + private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses' constructor ( private authHttp: HttpClient, @@ -18,13 +18,13 @@ export class VideoAbuseService { private restExtractor: RestExtractor ) {} - getVideoAbuses (options: { + getAbuses (options: { pagination: RestPagination, sort: SortMeta, search?: string - }): Observable> { + }): Observable> { const { pagination, sort, search } = options - const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse' + const url = AbuseService.BASE_ABUSE_URL + 'abuse' let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) @@ -35,9 +35,9 @@ export class VideoAbuseService { state: { prefix: 'state:', handler: v => { - if (v === 'accepted') return VideoAbuseState.ACCEPTED - if (v === 'pending') return VideoAbuseState.PENDING - if (v === 'rejected') return VideoAbuseState.REJECTED + if (v === 'accepted') return AbuseState.ACCEPTED + if (v === 'pending') return AbuseState.PENDING + if (v === 'rejected') return AbuseState.REJECTED return undefined } @@ -59,14 +59,14 @@ export class VideoAbuseService { params = this.restService.addObjectParams(params, filters) } - return this.authHttp.get>(url, { params }) + 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' + reportVideo (parameters: AbuseCreate) { + const url = AbuseService.BASE_ABUSE_URL const body = omit(parameters, [ 'id' ]) @@ -77,8 +77,8 @@ export class VideoAbuseService { ) } - updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) { - const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id + updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) { + const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id return this.authHttp.put(url, abuseUpdate) .pipe( @@ -87,8 +87,8 @@ export class VideoAbuseService { ) } - removeVideoAbuse (videoAbuse: VideoAbuse) { - const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id + removeAbuse (abuse: Abuse) { + const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id return this.authHttp.delete(url) .pipe( 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-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.')) diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts new file mode 100644 index 000000000..ee046cb3a --- /dev/null +++ b/server/controllers/api/abuse.ts @@ -0,0 +1,168 @@ +import * as express from 'express' +import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation' +import { AbuseModel } from '@server/models/abuse/abuse' +import { getServerActor } from '@server/models/application/application' +import { AbuseCreate, abusePredefinedReasonsMap, AbuseState, UserRight } from '../../../shared' +import { getFormattedObjects } from '../../helpers/utils' +import { sequelizeTypescript } from '../../initializers/database' +import { + abuseGetValidator, + abuseListValidator, + abuseReportValidator, + abusesSortValidator, + abuseUpdateValidator, + asyncMiddleware, + asyncRetryTransactionMiddleware, + authenticate, + ensureUserHasRight, + paginationValidator, + setDefaultPagination, + setDefaultSort +} from '../../middlewares' +import { AccountModel } from '../../models/account/account' + +const abuseRouter = express.Router() + +abuseRouter.get('/abuse', + authenticate, + ensureUserHasRight(UserRight.MANAGE_ABUSES), + paginationValidator, + abusesSortValidator, + setDefaultSort, + setDefaultPagination, + abuseListValidator, + asyncMiddleware(listAbuses) +) +abuseRouter.put('/:videoId/abuse/:id', + authenticate, + ensureUserHasRight(UserRight.MANAGE_ABUSES), + asyncMiddleware(abuseUpdateValidator), + asyncRetryTransactionMiddleware(updateAbuse) +) +abuseRouter.post('/:videoId/abuse', + authenticate, + asyncMiddleware(abuseReportValidator), + asyncRetryTransactionMiddleware(reportAbuse) +) +abuseRouter.delete('/:videoId/abuse/:id', + authenticate, + ensureUserHasRight(UserRight.MANAGE_ABUSES), + asyncMiddleware(abuseGetValidator), + asyncRetryTransactionMiddleware(deleteAbuse) +) + +// --------------------------------------------------------------------------- + +export { + abuseRouter, + + // FIXME: deprecated in 2.3. Remove these exports + listAbuses, + updateAbuse, + deleteAbuse, + reportAbuse +} + +// --------------------------------------------------------------------------- + +async function listAbuses (req: express.Request, res: express.Response) { + const user = res.locals.oauth.token.user + const serverActor = await getServerActor() + + const resultList = await AbuseModel.listForApi({ + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + id: req.query.id, + filter: 'video', + predefinedReason: req.query.predefinedReason, + search: req.query.search, + state: req.query.state, + videoIs: req.query.videoIs, + searchReporter: req.query.searchReporter, + searchReportee: req.query.searchReportee, + searchVideo: req.query.searchVideo, + searchVideoChannel: req.query.searchVideoChannel, + serverAccountId: serverActor.Account.id, + user + }) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + +async function updateAbuse (req: express.Request, res: express.Response) { + const abuse = res.locals.abuse + + if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment + if (req.body.state !== undefined) abuse.state = req.body.state + + await sequelizeTypescript.transaction(t => { + return abuse.save({ transaction: t }) + }) + + // Do not send the delete to other instances, we updated OUR copy of this video abuse + + return res.type('json').status(204).end() +} + +async function deleteAbuse (req: express.Request, res: express.Response) { + const abuse = res.locals.abuse + + await sequelizeTypescript.transaction(t => { + return abuse.destroy({ transaction: t }) + }) + + // Do not send the delete to other instances, we delete OUR copy of this video abuse + + return res.type('json').status(204).end() +} + +async function reportAbuse (req: express.Request, res: express.Response) { + const videoInstance = res.locals.videoAll + const commentInstance = res.locals.videoCommentFull + const accountInstance = res.locals.account + + const body: AbuseCreate = req.body + + const { id } = await sequelizeTypescript.transaction(async t => { + const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) + const predefinedReasons = body.predefinedReasons?.map(r => abusePredefinedReasonsMap[r]) + + const baseAbuse = { + reporterAccountId: reporterAccount.id, + reason: body.reason, + state: AbuseState.PENDING, + predefinedReasons + } + + if (body.video) { + return createVideoAbuse({ + baseAbuse, + videoInstance, + reporterAccount, + transaction: t, + startAt: body.video.startAt, + endAt: body.video.endAt + }) + } + + if (body.comment) { + return createVideoCommentAbuse({ + baseAbuse, + commentInstance, + reporterAccount, + transaction: t + }) + } + + // Account report + return createAccountAbuse({ + baseAbuse, + accountInstance, + reporterAccount, + transaction: t + }) + }) + + return res.json({ abuse: { id } }) +} diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index c334a26b4..eda9e04d1 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts @@ -3,6 +3,7 @@ import * as express from 'express' import * as RateLimit from 'express-rate-limit' import { badRequest } from '../../helpers/express-utils' import { CONFIG } from '../../initializers/config' +import { abuseRouter } from './abuse' import { accountsRouter } from './accounts' import { bulkRouter } from './bulk' import { configRouter } from './config' @@ -32,6 +33,7 @@ const apiRateLimiter = RateLimit({ apiRouter.use(apiRateLimiter) apiRouter.use('/server', serverRouter) +apiRouter.use('/abuses', abuseRouter) apiRouter.use('/bulk', bulkRouter) apiRouter.use('/oauth-clients', oauthClientsRouter) apiRouter.use('/config', configRouter) diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index ab2074459..b92a66360 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts @@ -1,9 +1,10 @@ import * as express from 'express' -import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared' -import { logger } from '../../../helpers/logger' +import { AbuseModel } from '@server/models/abuse/abuse' +import { getServerActor } from '@server/models/application/application' +import { AbuseCreate, UserRight, VideoAbuseCreate } from '../../../../shared' import { getFormattedObjects } from '../../../helpers/utils' -import { sequelizeTypescript } from '../../../initializers/database' import { + abusesSortValidator, asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, @@ -12,28 +13,21 @@ import { setDefaultPagination, setDefaultSort, videoAbuseGetValidator, + videoAbuseListValidator, videoAbuseReportValidator, - videoAbusesSortValidator, - videoAbuseUpdateValidator, - videoAbuseListValidator + videoAbuseUpdateValidator } from '../../../middlewares' -import { AccountModel } from '../../../models/account/account' -import { VideoAbuseModel } from '../../../models/video/video-abuse' -import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' -import { Notifier } from '../../../lib/notifier' -import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag' -import { MVideoAbuseAccountVideo } from '../../../types/models/video' -import { getServerActor } from '@server/models/application/application' -import { MAccountDefault } from '@server/types/models' +import { deleteAbuse, reportAbuse, updateAbuse } from '../abuse' + +// FIXME: deprecated in 2.3. Remove this controller -const auditLogger = auditLoggerFactory('abuse') const abuseVideoRouter = express.Router() abuseVideoRouter.get('/abuse', authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), + ensureUserHasRight(UserRight.MANAGE_ABUSES), paginationValidator, - videoAbusesSortValidator, + abusesSortValidator, setDefaultSort, setDefaultPagination, videoAbuseListValidator, @@ -41,7 +35,7 @@ abuseVideoRouter.get('/abuse', ) abuseVideoRouter.put('/:videoId/abuse/:id', authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), + ensureUserHasRight(UserRight.MANAGE_ABUSES), asyncMiddleware(videoAbuseUpdateValidator), asyncRetryTransactionMiddleware(updateVideoAbuse) ) @@ -52,7 +46,7 @@ abuseVideoRouter.post('/:videoId/abuse', ) abuseVideoRouter.delete('/:videoId/abuse/:id', authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), + ensureUserHasRight(UserRight.MANAGE_ABUSES), asyncMiddleware(videoAbuseGetValidator), asyncRetryTransactionMiddleware(deleteVideoAbuse) ) @@ -69,11 +63,12 @@ async function listVideoAbuses (req: express.Request, res: express.Response) { const user = res.locals.oauth.token.user const serverActor = await getServerActor() - const resultList = await VideoAbuseModel.listForApi({ + const resultList = await AbuseModel.listForApi({ start: req.query.start, count: req.query.count, sort: req.query.sort, id: req.query.id, + filter: 'video', predefinedReason: req.query.predefinedReason, search: req.query.search, state: req.query.state, @@ -90,74 +85,28 @@ async function listVideoAbuses (req: express.Request, res: express.Response) { } async function updateVideoAbuse (req: express.Request, res: express.Response) { - const videoAbuse = res.locals.videoAbuse - - if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment - if (req.body.state !== undefined) videoAbuse.state = req.body.state - - await sequelizeTypescript.transaction(t => { - return videoAbuse.save({ transaction: t }) - }) - - // Do not send the delete to other instances, we updated OUR copy of this video abuse - - return res.type('json').status(204).end() + return updateAbuse(req, res) } async function deleteVideoAbuse (req: express.Request, res: express.Response) { - const videoAbuse = res.locals.videoAbuse - - await sequelizeTypescript.transaction(t => { - return videoAbuse.destroy({ transaction: t }) - }) - - // Do not send the delete to other instances, we delete OUR copy of this video abuse - - return res.type('json').status(204).end() + return deleteAbuse(req, res) } async function reportVideoAbuse (req: express.Request, res: express.Response) { - const videoInstance = res.locals.videoAll - const body: VideoAbuseCreate = req.body - let reporterAccount: MAccountDefault - let videoAbuseJSON: VideoAbuse - - const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { - reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) - const predefinedReasons = body.predefinedReasons?.map(r => videoAbusePredefinedReasonsMap[r]) - - const abuseToCreate = { - reporterAccountId: reporterAccount.id, - reason: body.reason, - videoId: videoInstance.id, - state: VideoAbuseState.PENDING, - predefinedReasons, - startAt: body.startAt, - endAt: body.endAt - } - - const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) - videoAbuseInstance.Video = videoInstance - videoAbuseInstance.Account = reporterAccount - - // We send the video abuse to the origin server - if (videoInstance.isOwned() === false) { - await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t) - } + const oldBody = req.body as VideoAbuseCreate - videoAbuseJSON = videoAbuseInstance.toFormattedJSON() - auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON)) + req.body = { + accountId: res.locals.videoAll.VideoChannel.accountId, - return videoAbuseInstance - }) + reason: oldBody.reason, + predefinedReasons: oldBody.predefinedReasons, - Notifier.Instance.notifyOnNewVideoAbuse({ - videoAbuse: videoAbuseJSON, - videoAbuseInstance, - reporter: reporterAccount.Actor.getIdentifier() - }) - - logger.info('Abuse report for video "%s" created.', videoInstance.name) + video: { + id: res.locals.videoAll.id, + startAt: oldBody.startAt, + endAt: oldBody.endAt + } + } as AbuseCreate - return res.json({ videoAbuse: videoAbuseJSON }).end() + return reportAbuse(req, res) } diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index 0bbfbc753..954b0b69d 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts @@ -1,15 +1,15 @@ -import * as path from 'path' -import * as express from 'express' import { diff } from 'deep-object-diff' -import { chain } from 'lodash' +import * as express from 'express' import * as flatten from 'flat' +import { chain } from 'lodash' +import * as path from 'path' import * as winston from 'winston' -import { jsonLoggerFormat, labelFormatter } from './logger' -import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared' -import { VideoComment } from '../../shared/models/videos/video-comment.model' +import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' +import { Abuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared' import { CustomConfig } from '../../shared/models/server/custom-config.model' +import { VideoComment } from '../../shared/models/videos/video-comment.model' import { CONFIG } from '../initializers/config' -import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' +import { jsonLoggerFormat, labelFormatter } from './logger' function getAuditIdFromRes (res: express.Response) { return res.locals.oauth.token.User.username @@ -212,18 +212,15 @@ class VideoChannelAuditView extends EntityAuditView { } } -const videoAbuseKeysToKeep = [ +const abuseKeysToKeep = [ 'id', 'reason', 'reporterAccount', - 'video-id', - 'video-name', - 'video-uuid', 'createdAt' ] -class VideoAbuseAuditView extends EntityAuditView { - constructor (private readonly videoAbuse: VideoAbuse) { - super(videoAbuseKeysToKeep, 'abuse', videoAbuse) +class AbuseAuditView extends EntityAuditView { + constructor (private readonly abuse: Abuse) { + super(abuseKeysToKeep, 'abuse', abuse) } } @@ -274,6 +271,6 @@ export { CommentAuditView, UserAuditView, VideoAuditView, - VideoAbuseAuditView, + AbuseAuditView, CustomConfigAuditView } diff --git a/server/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts new file mode 100644 index 000000000..a6a895c65 --- /dev/null +++ b/server/helpers/custom-validators/abuses.ts @@ -0,0 +1,54 @@ +import validator from 'validator' +import { abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs } from '@shared/models' +import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants' +import { exists, isArray } from './misc' + +const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES + +function isAbuseReasonValid (value: string) { + return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) +} + +function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) { + return exists(value) && value in abusePredefinedReasonsMap +} + +function isAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) { + return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap) +} + +function isAbuseTimestampValid (value: number) { + return value === null || (exists(value) && validator.isInt('' + value, { min: 0 })) +} + +function isAbuseTimestampCoherent (endAt: number, { req }) { + return exists(req.body.startAt) && endAt > req.body.startAt +} + +function isAbuseModerationCommentValid (value: string) { + return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) +} + +function isAbuseStateValid (value: string) { + return exists(value) && ABUSE_STATES[value] !== undefined +} + +function isAbuseVideoIsValid (value: AbuseVideoIs) { + return exists(value) && ( + value === 'deleted' || + value === 'blacklisted' + ) +} + +// --------------------------------------------------------------------------- + +export { + isAbuseReasonValid, + isAbusePredefinedReasonValid, + isAbusePredefinedReasonsValid, + isAbuseTimestampValid, + isAbuseTimestampCoherent, + isAbuseModerationCommentValid, + isAbuseStateValid, + isAbuseVideoIsValid +} diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts index 6452e297c..dc90b3667 100644 --- a/server/helpers/custom-validators/activitypub/flag.ts +++ b/server/helpers/custom-validators/activitypub/flag.ts @@ -1,9 +1,9 @@ import { isActivityPubUrlValid } from './misc' -import { isVideoAbuseReasonValid } from '../video-abuses' +import { isAbuseReasonValid } from '../abuses' function isFlagActivityValid (activity: any) { return activity.type === 'Flag' && - isVideoAbuseReasonValid(activity.content) && + isAbuseReasonValid(activity.content) && isActivityPubUrlValid(activity.object) } diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts deleted file mode 100644 index 0c2c34268..000000000 --- a/server/helpers/custom-validators/video-abuses.ts +++ /dev/null @@ -1,56 +0,0 @@ -import validator from 'validator' - -import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' -import { exists, isArray } from './misc' -import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' -import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model' - -const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES - -function isVideoAbuseReasonValid (value: string) { - return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) -} - -function isVideoAbusePredefinedReasonValid (value: VideoAbusePredefinedReasonsString) { - return exists(value) && value in videoAbusePredefinedReasonsMap -} - -function isVideoAbusePredefinedReasonsValid (value: VideoAbusePredefinedReasonsString[]) { - return exists(value) && isArray(value) && value.every(v => v in videoAbusePredefinedReasonsMap) -} - -function isVideoAbuseTimestampValid (value: number) { - return value === null || (exists(value) && validator.isInt('' + value, { min: 0 })) -} - -function isVideoAbuseTimestampCoherent (endAt: number, { req }) { - return exists(req.body.startAt) && endAt > req.body.startAt -} - -function isVideoAbuseModerationCommentValid (value: string) { - return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) -} - -function isVideoAbuseStateValid (value: string) { - return exists(value) && VIDEO_ABUSE_STATES[value] !== undefined -} - -function isAbuseVideoIsValid (value: VideoAbuseVideoIs) { - return exists(value) && ( - value === 'deleted' || - value === 'blacklisted' - ) -} - -// --------------------------------------------------------------------------- - -export { - isVideoAbuseReasonValid, - isVideoAbusePredefinedReasonValid, - isVideoAbusePredefinedReasonsValid, - isVideoAbuseTimestampValid, - isVideoAbuseTimestampCoherent, - isVideoAbuseModerationCommentValid, - isVideoAbuseStateValid, - isAbuseVideoIsValid -} diff --git a/server/helpers/middlewares/video-abuses.ts b/server/helpers/middlewares/abuses.ts similarity index 56% rename from server/helpers/middlewares/video-abuses.ts rename to server/helpers/middlewares/abuses.ts index 97a5724b6..3906f6760 100644 --- a/server/helpers/middlewares/video-abuses.ts +++ b/server/helpers/middlewares/abuses.ts @@ -1,19 +1,20 @@ import { Response } from 'express' -import { VideoAbuseModel } from '../../models/video/video-abuse' +import { AbuseModel } from '../../models/abuse/abuse' import { fetchVideo } from '../video' +// FIXME: deprecated in 2.3. Remove this function async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) { const abuseId = parseInt(abuseIdArg + '', 10) - let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID) + let abuse = await AbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID) - if (!videoAbuse) { + if (!abuse) { const userId = res.locals.oauth?.token.User.id const video = await fetchVideo(videoUUID, 'all', userId) - if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id) + if (video) abuse = await AbuseModel.loadByIdAndVideoId(abuseId, video.id) } - if (videoAbuse === null) { + if (abuse === null) { res.status(404) .json({ error: 'Video abuse not found' }) .end() @@ -21,12 +22,17 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri return false } - res.locals.videoAbuse = videoAbuse + res.locals.abuse = abuse return true } +async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) { + +} + // --------------------------------------------------------------------------- export { + doesAbuseExist, doesVideoAbuseExist } diff --git a/server/helpers/middlewares/index.ts b/server/helpers/middlewares/index.ts index f91aeaa12..f57f3ad31 100644 --- a/server/helpers/middlewares/index.ts +++ b/server/helpers/middlewares/index.ts @@ -1,5 +1,5 @@ +export * from './abuses' export * from './accounts' -export * from './video-abuses' export * from './video-blacklists' export * from './video-captions' export * from './video-channels' diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index e730e3c84..8f86bbbef 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -1,9 +1,17 @@ import { join } from 'path' import { randomBytes } from 'crypto' -import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models' import { ActivityPubActorType } from '../../shared/models/activitypub' import { FollowState } from '../../shared/models/actors' -import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' +import { + AbuseState, + VideoImportState, + VideoPrivacy, + VideoTranscodingFPS, + JobType, + VideoRateType, + VideoResolution, + VideoState +} from '../../shared/models' // Do not use barrels, remain constants as independent as possible import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils' import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' @@ -51,7 +59,6 @@ const SORTABLE_COLUMNS = { USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], ACCOUNTS: [ 'createdAt' ], JOBS: [ 'createdAt' ], - VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ], VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], VIDEO_IMPORTS: [ 'createdAt' ], VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], @@ -66,6 +73,8 @@ const SORTABLE_COLUMNS = { VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], + ABUSES: [ 'id', 'createdAt', 'state' ], + ACCOUNTS_BLOCKLIST: [ 'createdAt' ], SERVERS_BLOCKLIST: [ 'createdAt' ], @@ -193,7 +202,7 @@ const CONSTRAINTS_FIELDS = { VIDEO_LANGUAGES: { max: 500 }, // Array length BLOCKED_REASON: { min: 3, max: 250 } // Length }, - VIDEO_ABUSES: { + ABUSES: { REASON: { min: 2, max: 3000 }, // Length MODERATION_COMMENT: { min: 2, max: 3000 } // Length }, @@ -378,10 +387,10 @@ const VIDEO_IMPORT_STATES = { [VideoImportState.REJECTED]: 'Rejected' } -const VIDEO_ABUSE_STATES = { - [VideoAbuseState.PENDING]: 'Pending', - [VideoAbuseState.REJECTED]: 'Rejected', - [VideoAbuseState.ACCEPTED]: 'Accepted' +const ABUSE_STATES = { + [AbuseState.PENDING]: 'Pending', + [AbuseState.REJECTED]: 'Rejected', + [AbuseState.ACCEPTED]: 'Accepted' } const VIDEO_PLAYLIST_PRIVACIES = { @@ -778,7 +787,7 @@ export { VIDEO_RATE_TYPES, VIDEO_TRANSCODING_FPS, FFMPEG_NICE, - VIDEO_ABUSE_STATES, + ABUSE_STATES, VIDEO_CHANNELS, LRU_CACHE, JOB_REQUEST_TIMEOUT, diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 633d4f956..0775f1fad 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -1,44 +1,45 @@ +import { QueryTypes, Transaction } from 'sequelize' import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' +import { AbuseModel } from '@server/models/abuse/abuse' +import { VideoAbuseModel } from '@server/models/abuse/video-abuse' +import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' import { isTestInstance } from '../helpers/core-utils' import { logger } from '../helpers/logger' - import { AccountModel } from '../models/account/account' +import { AccountBlocklistModel } from '../models/account/account-blocklist' import { AccountVideoRateModel } from '../models/account/account-video-rate' import { UserModel } from '../models/account/user' +import { UserNotificationModel } from '../models/account/user-notification' +import { UserNotificationSettingModel } from '../models/account/user-notification-setting' +import { UserVideoHistoryModel } from '../models/account/user-video-history' import { ActorModel } from '../models/activitypub/actor' import { ActorFollowModel } from '../models/activitypub/actor-follow' import { ApplicationModel } from '../models/application/application' import { AvatarModel } from '../models/avatar/avatar' import { OAuthClientModel } from '../models/oauth/oauth-client' import { OAuthTokenModel } from '../models/oauth/oauth-token' +import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' +import { PluginModel } from '../models/server/plugin' import { ServerModel } from '../models/server/server' +import { ServerBlocklistModel } from '../models/server/server-blocklist' +import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' import { TagModel } from '../models/video/tag' +import { ThumbnailModel } from '../models/video/thumbnail' import { VideoModel } from '../models/video/video' -import { VideoAbuseModel } from '../models/video/video-abuse' import { VideoBlacklistModel } from '../models/video/video-blacklist' +import { VideoCaptionModel } from '../models/video/video-caption' +import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' import { VideoChannelModel } from '../models/video/video-channel' import { VideoCommentModel } from '../models/video/video-comment' import { VideoFileModel } from '../models/video/video-file' -import { VideoShareModel } from '../models/video/video-share' -import { VideoTagModel } from '../models/video/video-tag' -import { CONFIG } from './config' -import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' -import { VideoCaptionModel } from '../models/video/video-caption' import { VideoImportModel } from '../models/video/video-import' -import { VideoViewModel } from '../models/video/video-view' -import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' -import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' -import { UserVideoHistoryModel } from '../models/account/user-video-history' -import { AccountBlocklistModel } from '../models/account/account-blocklist' -import { ServerBlocklistModel } from '../models/server/server-blocklist' -import { UserNotificationModel } from '../models/account/user-notification' -import { UserNotificationSettingModel } from '../models/account/user-notification-setting' -import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' import { VideoPlaylistModel } from '../models/video/video-playlist' import { VideoPlaylistElementModel } from '../models/video/video-playlist-element' -import { ThumbnailModel } from '../models/video/thumbnail' -import { PluginModel } from '../models/server/plugin' -import { QueryTypes, Transaction } from 'sequelize' +import { VideoShareModel } from '../models/video/video-share' +import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' +import { VideoTagModel } from '../models/video/video-tag' +import { VideoViewModel } from '../models/video/video-view' +import { CONFIG } from './config' require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string @@ -86,6 +87,8 @@ async function initDatabaseModels (silent: boolean) { TagModel, AccountVideoRateModel, UserModel, + AbuseModel, + VideoCommentAbuseModel, VideoAbuseModel, VideoModel, VideoChangeOwnershipModel, diff --git a/server/initializers/migrations/0250-video-abuse-state.ts b/server/initializers/migrations/0250-video-abuse-state.ts index 50de25182..e4993c393 100644 --- a/server/initializers/migrations/0250-video-abuse-state.ts +++ b/server/initializers/migrations/0250-video-abuse-state.ts @@ -1,5 +1,5 @@ import * as Sequelize from 'sequelize' -import { VideoAbuseState } from '../../../shared/models/videos' +import { AbuseState } from '../../../shared/models' async function up (utils: { transaction: Sequelize.Transaction @@ -16,7 +16,7 @@ async function up (utils: { } { - const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING + const query = 'UPDATE "videoAbuse" SET "state" = ' + AbuseState.PENDING await utils.sequelize.query(query) } diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts index 1d7132a3a..6350cee12 100644 --- a/server/lib/activitypub/process/process-flag.ts +++ b/server/lib/activitypub/process/process-flag.ts @@ -1,24 +1,19 @@ -import { - ActivityCreate, - ActivityFlag, - VideoAbuseState, - videoAbusePredefinedReasonsMap -} from '../../../../shared' -import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' +import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation' +import { AccountModel } from '@server/models/account/account' +import { VideoModel } from '@server/models/video/video' +import { VideoCommentModel } from '@server/models/video/video-comment' +import { AbuseObject, abusePredefinedReasonsMap, AbuseState, ActivityCreate, ActivityFlag } from '../../../../shared' +import { getAPId } from '../../../helpers/activitypub' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' -import { VideoAbuseModel } from '../../../models/video/video-abuse' -import { getOrCreateVideoAndAccountAndChannel } from '../videos' -import { Notifier } from '../../notifier' -import { getAPId } from '../../../helpers/activitypub' import { APProcessorOptions } from '../../../types/activitypub-processor.model' -import { MActorSignature, MVideoAbuseAccountVideo } from '../../../types/models' -import { AccountModel } from '@server/models/account/account' +import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' async function processFlagActivity (options: APProcessorOptions) { const { activity, byActor } = options - return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor) + + return retryTransactionWrapper(processCreateAbuse, activity, byActor) } // --------------------------------------------------------------------------- @@ -29,55 +24,79 @@ export { // --------------------------------------------------------------------------- -async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { - const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject) +async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { + const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject) const account = byActor.Account - if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url) + if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) + + const reporterAccount = await AccountModel.load(account.id) const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ] + const tags = Array.isArray(flag.tag) ? flag.tag : [] + const predefinedReasons = tags.map(tag => abusePredefinedReasonsMap[tag.name]) + .filter(v => !isNaN(v)) + + const startAt = flag.startAt + const endAt = flag.endAt + for (const object of objects) { try { - logger.debug('Reporting remote abuse for video %s.', getAPId(object)) - - const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) - const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t)) - const tags = Array.isArray(flag.tag) ? flag.tag : [] - const predefinedReasons = tags.map(tag => videoAbusePredefinedReasonsMap[tag.name]) - .filter(v => !isNaN(v)) - const startAt = flag.startAt - const endAt = flag.endAt - - const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { - const videoAbuseData = { - reporterAccountId: account.id, - reason: flag.content, - videoId: video.id, - state: VideoAbuseState.PENDING, - predefinedReasons, - startAt, - endAt - } + const uri = getAPId(object) - const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) - videoAbuseInstance.Video = video - videoAbuseInstance.Account = reporterAccount + logger.debug('Reporting remote abuse for object %s.', uri) - logger.info('Remote abuse for video uuid %s created', flag.object) + await sequelizeTypescript.transaction(async t => { - return videoAbuseInstance - }) + const video = await VideoModel.loadByUrlAndPopulateAccount(uri) + let videoComment: MCommentOwnerVideo + let flaggedAccount: MAccountDefault + + if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri) + if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri) + + if (!video && !videoComment && !flaggedAccount) { + logger.warn('Cannot flag unknown entity %s.', object) + return + } + + const baseAbuse = { + reporterAccountId: reporterAccount.id, + reason: flag.content, + state: AbuseState.PENDING, + predefinedReasons + } - const videoAbuseJSON = videoAbuseInstance.toFormattedJSON() + if (video) { + return createVideoAbuse({ + baseAbuse, + startAt, + endAt, + reporterAccount, + transaction: t, + videoInstance: video + }) + } + + if (videoComment) { + return createVideoCommentAbuse({ + baseAbuse, + reporterAccount, + transaction: t, + commentInstance: videoComment + }) + } - Notifier.Instance.notifyOnNewVideoAbuse({ - videoAbuse: videoAbuseJSON, - videoAbuseInstance, - reporter: reporterAccount.Actor.getIdentifier() + return await createAccountAbuse({ + baseAbuse, + reporterAccount, + transaction: t, + accountInstance: flaggedAccount + }) }) } catch (err) { - logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) + logger.debug('Cannot process report of %s', getAPId(object), { err }) } } } diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts index 3a1fe0812..821637ec8 100644 --- a/server/lib/activitypub/send/send-flag.ts +++ b/server/lib/activitypub/send/send-flag.ts @@ -1,32 +1,31 @@ -import { getVideoAbuseActivityPubUrl } from '../url' -import { unicastTo } from './utils' -import { logger } from '../../../helpers/logger' +import { Transaction } from 'sequelize' import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' +import { logger } from '../../../helpers/logger' +import { MAbuseAP, MAccountLight, MActor } from '../../../types/models' import { audiencify, getAudience } from '../audience' -import { Transaction } from 'sequelize' -import { MActor, MVideoFullLight } from '../../../types/models' -import { MVideoAbuseVideo } from '../../../types/models/video' +import { getAbuseActivityPubUrl } from '../url' +import { unicastTo } from './utils' -function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { - if (!video.VideoChannel.Account.Actor.serverId) return // Local user +function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) { + if (!flaggedAccount.Actor.serverId) return // Local user - const url = getVideoAbuseActivityPubUrl(videoAbuse) + const url = getAbuseActivityPubUrl(abuse) - logger.info('Creating job to send video abuse %s.', url) + logger.info('Creating job to send abuse %s.', url) // Custom audience, we only send the abuse to the origin instance - const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } - const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience) + const audience = { to: [ flaggedAccount.Actor.url ], cc: [] } + const flagActivity = buildFlagActivity(url, byActor, abuse, audience) - t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.getSharedInbox())) + t.afterCommit(() => unicastTo(flagActivity, byActor, flaggedAccount.Actor.getSharedInbox())) } -function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag { +function buildFlagActivity (url: string, byActor: MActor, abuse: MAbuseAP, audience: ActivityAudience): ActivityFlag { if (!audience) audience = getAudience(byActor) const activity = Object.assign( { id: url, actor: byActor.url }, - videoAbuse.toActivityPubObject() + abuse.toActivityPubObject() ) return audiencify(activity, audience) @@ -35,5 +34,5 @@ function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbus // --------------------------------------------------------------------------- export { - sendVideoAbuse + sendAbuse } diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 7f98751a1..b54e038a4 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts @@ -5,10 +5,10 @@ import { MActorId, MActorUrl, MCommentId, - MVideoAbuseId, MVideoId, MVideoUrl, - MVideoUUID + MVideoUUID, + MAbuseId } from '../../types/models' import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist' import { MVideoFileVideoUUID } from '../../types/models/video/video-file' @@ -48,8 +48,8 @@ function getAccountActivityPubUrl (accountName: string) { return WEBSERVER.URL + '/accounts/' + accountName } -function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) { - return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id +function getAbuseActivityPubUrl (abuse: MAbuseId) { + return WEBSERVER.URL + '/admin/abuses/' + abuse.id } function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) { @@ -118,7 +118,7 @@ export { getVideoCacheStreamingPlaylistActivityPubUrl, getVideoChannelActivityPubUrl, getAccountActivityPubUrl, - getVideoAbuseActivityPubUrl, + getAbuseActivityPubUrl, getActorFollowActivityPubUrl, getActorFollowAcceptActivityPubUrl, getVideoAnnounceActivityPubUrl, diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index c08732b48..e821aea5f 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -1,26 +1,20 @@ +import { readFileSync } from 'fs-extra' +import { merge } from 'lodash' import { createTransport, Transporter } from 'nodemailer' +import { join } from 'path' +import { VideoChannelModel } from '@server/models/video/video-channel' +import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' +import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' +import { Abuse, EmailPayload } from '@shared/models' +import { SendEmailOptions } from '../../shared/models/server/emailer.model' import { isTestInstance, root } from '../helpers/core-utils' import { bunyanLogger, logger } from '../helpers/logger' import { CONFIG, isEmailEnabled } from '../initializers/config' -import { JobQueue } from './job-queue' -import { readFileSync } from 'fs-extra' import { WEBSERVER } from '../initializers/constants' -import { - MCommentOwnerVideo, - MVideo, - MVideoAbuseVideo, - MVideoAccountLight, - MVideoBlacklistLightVideo, - MVideoBlacklistVideo -} from '../types/models/video' -import { MActorFollowActors, MActorFollowFull, MUser } from '../types/models' -import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' -import { EmailPayload } from '@shared/models' -import { join } from 'path' -import { VideoAbuse } from '../../shared/models/videos' -import { SendEmailOptions } from '../../shared/models/server/emailer.model' -import { merge } from 'lodash' -import { VideoChannelModel } from '@server/models/video/video-channel' +import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' +import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' +import { JobQueue } from './job-queue' + const Email = require('email-templates') class Emailer { @@ -288,28 +282,70 @@ class Emailer { return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) } - addVideoAbuseModeratorsNotification (to: string[], parameters: { - videoAbuse: VideoAbuse - videoAbuseInstance: MVideoAbuseVideo + addAbuseModeratorsNotification (to: string[], parameters: { + abuse: Abuse + abuseInstance: MAbuseFull reporter: string }) { - const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id - const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath() + const { abuse, abuseInstance, reporter } = parameters - const emailPayload: EmailPayload = { - template: 'video-abuse-new', - to, - subject: `New video abuse report from ${parameters.reporter}`, - locals: { - videoUrl, - videoAbuseUrl, - videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(), - videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(), - videoAbuse: parameters.videoAbuse, - reporter: parameters.reporter, - action: { - text: 'View report #' + parameters.videoAbuse.id, - url: videoAbuseUrl + const action = { + text: 'View report #' + abuse.id, + url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id + } + + let emailPayload: EmailPayload + + if (abuseInstance.VideoAbuse) { + const video = abuseInstance.VideoAbuse.Video + const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() + + emailPayload = { + template: 'video-abuse-new', + to, + subject: `New video abuse report from ${reporter}`, + locals: { + videoUrl, + isLocal: video.remote === false, + videoCreatedAt: new Date(video.createdAt).toLocaleString(), + videoPublishedAt: new Date(video.publishedAt).toLocaleString(), + videoName: video.name, + reason: abuse.reason, + videoChannel: video.VideoChannel, + action + } + } + } else if (abuseInstance.VideoCommentAbuse) { + const comment = abuseInstance.VideoCommentAbuse.VideoComment + const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId() + + emailPayload = { + template: 'comment-abuse-new', + to, + subject: `New comment abuse report from ${reporter}`, + locals: { + commentUrl, + isLocal: comment.isOwned(), + commentCreatedAt: new Date(comment.createdAt).toLocaleString(), + reason: abuse.reason, + flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(), + action + } + } + } else { + const account = abuseInstance.FlaggedAccount + const accountUrl = account.getClientUrl() + + emailPayload = { + template: 'account-abuse-new', + to, + subject: `New account abuse report from ${reporter}`, + locals: { + accountUrl, + accountDisplayName: account.getDisplayName(), + isLocal: account.isOwned(), + reason: abuse.reason, + action } } } diff --git a/server/lib/emails/account-abuse-new/html.pug b/server/lib/emails/account-abuse-new/html.pug new file mode 100644 index 000000000..06be8025b --- /dev/null +++ b/server/lib/emails/account-abuse-new/html.pug @@ -0,0 +1,14 @@ +extends ../common/greetings +include ../common/mixins.pug + +block title + | An account is pending moderation + +block content + p + | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}account " + a(href=accountUrl) #{accountDisplayName} + + p The reporter, #{reporter}, cited the following reason(s): + blockquote #{reason} + br(style="display: none;") diff --git a/server/lib/emails/common/mixins.pug b/server/lib/emails/common/mixins.pug index 76b805a24..831211864 100644 --- a/server/lib/emails/common/mixins.pug +++ b/server/lib/emails/common/mixins.pug @@ -1,3 +1,7 @@ mixin channel(channel) - var handle = `${channel.name}@${channel.host}` - | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] \ No newline at end of file + | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] + +mixin account(account) + - var handle = `${account.name}@${account.host}` + | #[a(href=`${WEBSERVER.URL}/accounts/${handle}` title=handle) #{account.displayName}] diff --git a/server/lib/emails/video-abuse-new/html.pug b/server/lib/emails/video-abuse-new/html.pug index 999c89d26..a1acdabdc 100644 --- a/server/lib/emails/video-abuse-new/html.pug +++ b/server/lib/emails/video-abuse-new/html.pug @@ -6,13 +6,13 @@ block title block content p - | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video " - a(href=videoUrl) #{videoAbuse.video.name} - | " by #[+channel(videoAbuse.video.channel)] + | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}video " + a(href=videoUrl) #{videoName} + | " by #[+channel(videoChannel)] if videoPublishedAt | , published the #{videoPublishedAt}. else | , uploaded the #{videoCreatedAt} but not yet published. p The reporter, #{reporter}, cited the following reason(s): - blockquote #{videoAbuse.reason} + blockquote #{reason} br(style="display: none;") diff --git a/server/lib/emails/video-comment-abuse-new/html.pug b/server/lib/emails/video-comment-abuse-new/html.pug new file mode 100644 index 000000000..170b79576 --- /dev/null +++ b/server/lib/emails/video-comment-abuse-new/html.pug @@ -0,0 +1,15 @@ +extends ../common/greetings +include ../common/mixins.pug + +block title + | A comment is pending moderation + +block content + p + | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}comment " + a(href=commentUrl) of #{flaggedAccount} + | created on #{commentCreatedAt} + + p The reporter, #{reporter}, cited the following reason(s): + blockquote #{reason} + br(style="display: none;") diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index 60d1b4053..4fc9cd747 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts @@ -1,15 +1,33 @@ -import { VideoModel } from '../models/video/video' -import { VideoCommentModel } from '../models/video/video-comment' -import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' +import { PathLike } from 'fs-extra' +import { Transaction } from 'sequelize/types' +import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger' +import { logger } from '@server/helpers/logger' +import { AbuseModel } from '@server/models/abuse/abuse' +import { VideoAbuseModel } from '@server/models/abuse/video-abuse' +import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' +import { VideoFileModel } from '@server/models/video/video-file' +import { FilteredModelAttributes } from '@server/types' +import { + MAbuseFull, + MAccountDefault, + MAccountLight, + MCommentAbuseAccountVideo, + MCommentOwnerVideo, + MUser, + MVideoAbuseVideoFull, + MVideoAccountLightBlacklistAllFiles +} from '@server/types/models' +import { ActivityCreate } from '../../shared/models/activitypub' +import { VideoTorrentObject } from '../../shared/models/activitypub/objects' +import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' import { VideoCreate, VideoImportCreate } from '../../shared/models/videos' +import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' import { UserModel } from '../models/account/user' -import { VideoTorrentObject } from '../../shared/models/activitypub/objects' -import { ActivityCreate } from '../../shared/models/activitypub' import { ActorModel } from '../models/activitypub/actor' -import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' -import { VideoFileModel } from '@server/models/video/video-file' -import { PathLike } from 'fs-extra' -import { MUser } from '@server/types/models' +import { VideoModel } from '../models/video/video' +import { VideoCommentModel } from '../models/video/video-comment' +import { sendAbuse } from './activitypub/send/send-flag' +import { Notifier } from './notifier' export type AcceptResult = { accepted: boolean @@ -73,6 +91,89 @@ function isPostImportVideoAccepted (object: { return { accepted: true } } +async function createVideoAbuse (options: { + baseAbuse: FilteredModelAttributes + videoInstance: MVideoAccountLightBlacklistAllFiles + startAt: number + endAt: number + transaction: Transaction + reporterAccount: MAccountDefault +}) { + const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount } = options + + const associateFun = async (abuseInstance: MAbuseFull) => { + const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({ + abuseId: abuseInstance.id, + videoId: videoInstance.id, + startAt: startAt, + endAt: endAt + }, { transaction }) + + videoAbuseInstance.Video = videoInstance + abuseInstance.VideoAbuse = videoAbuseInstance + + return { isOwned: videoInstance.isOwned() } + } + + return createAbuse({ + base: baseAbuse, + reporterAccount, + flaggedAccount: videoInstance.VideoChannel.Account, + transaction, + associateFun + }) +} + +function createVideoCommentAbuse (options: { + baseAbuse: FilteredModelAttributes + commentInstance: MCommentOwnerVideo + transaction: Transaction + reporterAccount: MAccountDefault +}) { + const { baseAbuse, commentInstance, transaction, reporterAccount } = options + + const associateFun = async (abuseInstance: MAbuseFull) => { + const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({ + abuseId: abuseInstance.id, + videoCommentId: commentInstance.id + }, { transaction }) + + commentAbuseInstance.VideoComment = commentInstance + abuseInstance.VideoCommentAbuse = commentAbuseInstance + + return { isOwned: commentInstance.isOwned() } + } + + return createAbuse({ + base: baseAbuse, + reporterAccount, + flaggedAccount: commentInstance.Account, + transaction, + associateFun + }) +} + +function createAccountAbuse (options: { + baseAbuse: FilteredModelAttributes + accountInstance: MAccountDefault + transaction: Transaction + reporterAccount: MAccountDefault +}) { + const { baseAbuse, accountInstance, transaction, reporterAccount } = options + + const associateFun = async () => { + return { isOwned: accountInstance.isOwned() } + } + + return createAbuse({ + base: baseAbuse, + reporterAccount, + flaggedAccount: accountInstance, + transaction, + associateFun + }) +} + export { isLocalVideoAccepted, isLocalVideoThreadAccepted, @@ -80,5 +181,48 @@ export { isRemoteVideoCommentAccepted, isLocalVideoCommentReplyAccepted, isPreImportVideoAccepted, - isPostImportVideoAccepted + isPostImportVideoAccepted, + + createAbuse, + createVideoAbuse, + createVideoCommentAbuse, + createAccountAbuse +} + +// --------------------------------------------------------------------------- + +async function createAbuse (options: { + base: FilteredModelAttributes + reporterAccount: MAccountDefault + flaggedAccount: MAccountLight + associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean} > + transaction: Transaction +}) { + const { base, reporterAccount, flaggedAccount, associateFun, transaction } = options + const auditLogger = auditLoggerFactory('abuse') + + const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id }) + const abuseInstance: MAbuseFull = await AbuseModel.create(abuseAttributes, { transaction }) + + abuseInstance.ReporterAccount = reporterAccount + abuseInstance.FlaggedAccount = flaggedAccount + + const { isOwned } = await associateFun(abuseInstance) + + if (isOwned === false) { + await sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction) + } + + const abuseJSON = abuseInstance.toFormattedJSON() + auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON)) + + Notifier.Instance.notifyOnNewAbuse({ + abuse: abuseJSON, + abuseInstance, + reporter: reporterAccount.Actor.getIdentifier() + }) + + logger.info('Abuse report %d created.', abuseInstance.id) + + return abuseJSON } diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 943a087d2..40cff66d2 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts @@ -8,23 +8,18 @@ import { MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models/user' +import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' import { MVideoImportVideo } from '@server/types/models/video/video-import' +import { Abuse } from '@shared/models' import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' -import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos' +import { VideoPrivacy, VideoState } from '../../shared/models/videos' import { logger } from '../helpers/logger' import { CONFIG } from '../initializers/config' import { AccountBlocklistModel } from '../models/account/account-blocklist' import { UserModel } from '../models/account/user' import { UserNotificationModel } from '../models/account/user-notification' -import { MAccountServer, MActorFollowFull } from '../types/models' -import { - MCommentOwnerVideo, - MVideoAbuseVideo, - MVideoAccountLight, - MVideoBlacklistLightVideo, - MVideoBlacklistVideo, - MVideoFullLight -} from '../types/models/video' +import { MAbuseFull, MAbuseVideo, MAccountServer, MActorFollowFull } from '../types/models' +import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' import { isBlockedByServerOrAccount } from './blocklist' import { Emailer } from './emailer' import { PeerTubeSocket } from './peertube-socket' @@ -78,9 +73,9 @@ class Notifier { .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) } - notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void { - this.notifyModeratorsOfNewVideoAbuse(parameters) - .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err })) + notifyOnNewAbuse (parameters: { abuse: Abuse, abuseInstance: MAbuseFull, reporter: string }): void { + this.notifyModeratorsOfNewAbuse(parameters) + .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err })) } notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { @@ -354,33 +349,37 @@ class Notifier { return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) } - private async notifyModeratorsOfNewVideoAbuse (parameters: { - videoAbuse: VideoAbuse - videoAbuseInstance: MVideoAbuseVideo + private async notifyModeratorsOfNewAbuse (parameters: { + abuse: Abuse + abuseInstance: MAbuseFull reporter: string }) { - const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) + const { abuse, abuseInstance } = parameters + + const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) if (moderators.length === 0) return - logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url) + const url = abuseInstance.VideoAbuse?.Video?.url || abuseInstance.VideoCommentAbuse?.VideoComment?.url + + logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url) function settingGetter (user: MUserWithNotificationSetting) { return user.NotificationSetting.videoAbuseAsModerator } async function notificationCreator (user: MUserWithNotificationSetting) { - const notification: UserNotificationModelForApi = await UserNotificationModel.create({ + const notification = await UserNotificationModel.create({ type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, userId: user.id, - videoAbuseId: parameters.videoAbuse.id + abuseId: abuse.id }) - notification.VideoAbuse = parameters.videoAbuseInstance + notification.Abuse = abuseInstance return notification } function emailSender (emails: string[]) { - return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters) + return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters) } return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts new file mode 100644 index 000000000..f098e2ff9 --- /dev/null +++ b/server/middlewares/validators/abuse.ts @@ -0,0 +1,253 @@ +import * as express from 'express' +import { body, param, query } from 'express-validator' +import { + isAbuseModerationCommentValid, + isAbusePredefinedReasonsValid, + isAbusePredefinedReasonValid, + isAbuseReasonValid, + isAbuseStateValid, + isAbuseTimestampCoherent, + isAbuseTimestampValid, + isAbuseVideoIsValid +} from '@server/helpers/custom-validators/abuses' +import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc' +import { logger } from '@server/helpers/logger' +import { doesAbuseExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares' +import { areValidationErrors } from './utils' + +const abuseReportValidator = [ + param('videoId') + .custom(isIdOrUUIDValid) + .not() + .isEmpty() + .withMessage('Should have a valid videoId'), + body('reason') + .custom(isAbuseReasonValid) + .withMessage('Should have a valid reason'), + body('predefinedReasons') + .optional() + .custom(isAbusePredefinedReasonsValid) + .withMessage('Should have a valid list of predefined reasons'), + body('startAt') + .optional() + .customSanitizer(toIntOrNull) + .custom(isAbuseTimestampValid) + .withMessage('Should have valid starting time value'), + body('endAt') + .optional() + .customSanitizer(toIntOrNull) + .custom(isAbuseTimestampValid) + .withMessage('Should have valid ending time value') + .bail() + .custom(isAbuseTimestampCoherent) + .withMessage('Should have a startAt timestamp beginning before endAt'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking abuseReport parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await doesVideoExist(req.params.videoId, res)) return + + // TODO: check comment or video (exlusive) + + return next() + } +] + +const abuseGetValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking abuseGetValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return + + return next() + } +] + +const abuseUpdateValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), + body('state') + .optional() + .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), + body('moderationComment') + .optional() + .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return + + return next() + } +] + +const abuseListValidator = [ + query('id') + .optional() + .custom(isIdValid).withMessage('Should have a valid id'), + query('predefinedReason') + .optional() + .custom(isAbusePredefinedReasonValid) + .withMessage('Should have a valid predefinedReason'), + query('search') + .optional() + .custom(exists).withMessage('Should have a valid search'), + query('state') + .optional() + .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), + query('videoIs') + .optional() + .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'), + query('searchReporter') + .optional() + .custom(exists).withMessage('Should have a valid reporter search'), + query('searchReportee') + .optional() + .custom(exists).withMessage('Should have a valid reportee search'), + query('searchVideo') + .optional() + .custom(exists).withMessage('Should have a valid video search'), + query('searchVideoChannel') + .optional() + .custom(exists).withMessage('Should have a valid video channel search'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking abuseListValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + + return next() + } +] + +// FIXME: deprecated in 2.3. Remove these validators + +const videoAbuseReportValidator = [ + param('videoId') + .custom(isIdOrUUIDValid) + .not() + .isEmpty() + .withMessage('Should have a valid videoId'), + body('reason') + .custom(isAbuseReasonValid) + .withMessage('Should have a valid reason'), + body('predefinedReasons') + .optional() + .custom(isAbusePredefinedReasonsValid) + .withMessage('Should have a valid list of predefined reasons'), + body('startAt') + .optional() + .customSanitizer(toIntOrNull) + .custom(isAbuseTimestampValid) + .withMessage('Should have valid starting time value'), + body('endAt') + .optional() + .customSanitizer(toIntOrNull) + .custom(isAbuseTimestampValid) + .withMessage('Should have valid ending time value') + .bail() + .custom(isAbuseTimestampCoherent) + .withMessage('Should have a startAt timestamp beginning before endAt'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await doesVideoExist(req.params.videoId, res)) return + + return next() + } +] + +const videoAbuseGetValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return + + return next() + } +] + +const videoAbuseUpdateValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), + body('state') + .optional() + .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), + body('moderationComment') + .optional() + .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return + + return next() + } +] + +const videoAbuseListValidator = [ + query('id') + .optional() + .custom(isIdValid).withMessage('Should have a valid id'), + query('predefinedReason') + .optional() + .custom(isAbusePredefinedReasonValid) + .withMessage('Should have a valid predefinedReason'), + query('search') + .optional() + .custom(exists).withMessage('Should have a valid search'), + query('state') + .optional() + .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), + query('videoIs') + .optional() + .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'), + query('searchReporter') + .optional() + .custom(exists).withMessage('Should have a valid reporter search'), + query('searchReportee') + .optional() + .custom(exists).withMessage('Should have a valid reportee search'), + query('searchVideo') + .optional() + .custom(exists).withMessage('Should have a valid video search'), + query('searchVideoChannel') + .optional() + .custom(exists).withMessage('Should have a valid video channel search'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + abuseListValidator, + abuseReportValidator, + abuseGetValidator, + abuseUpdateValidator, + videoAbuseReportValidator, + videoAbuseGetValidator, + videoAbuseUpdateValidator, + videoAbuseListValidator +} diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 65dd00335..4086d77aa 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts @@ -1,3 +1,4 @@ +export * from './abuse' export * from './account' export * from './blocklist' export * from './oembed' diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index b76dab722..29aba0436 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts @@ -5,7 +5,7 @@ import { checkSort, createSortableColumns } from './utils' const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS) const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) -const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) +const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES) const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) @@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) -const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) +const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) @@ -52,7 +52,7 @@ const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COL export { usersSortValidator, - videoAbusesSortValidator, + abusesSortValidator, videoChannelsSortValidator, videoImportsSortValidator, videosSearchSortValidator, diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index a0d585b93..1eabada0a 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts @@ -1,4 +1,3 @@ -export * from './video-abuses' export * from './video-blacklist' export * from './video-captions' export * from './video-channels' diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts deleted file mode 100644 index 5bbd1e3c6..000000000 --- a/server/middlewares/validators/videos/video-abuses.ts +++ /dev/null @@ -1,135 +0,0 @@ -import * as express from 'express' -import { body, param, query } from 'express-validator' -import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' -import { - isAbuseVideoIsValid, - isVideoAbuseModerationCommentValid, - isVideoAbuseReasonValid, - isVideoAbuseStateValid, - isVideoAbusePredefinedReasonsValid, - isVideoAbusePredefinedReasonValid, - isVideoAbuseTimestampValid, - isVideoAbuseTimestampCoherent -} from '../../../helpers/custom-validators/video-abuses' -import { logger } from '../../../helpers/logger' -import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares' -import { areValidationErrors } from '../utils' - -const videoAbuseReportValidator = [ - param('videoId') - .custom(isIdOrUUIDValid) - .not() - .isEmpty() - .withMessage('Should have a valid videoId'), - body('reason') - .custom(isVideoAbuseReasonValid) - .withMessage('Should have a valid reason'), - body('predefinedReasons') - .optional() - .custom(isVideoAbusePredefinedReasonsValid) - .withMessage('Should have a valid list of predefined reasons'), - body('startAt') - .optional() - .customSanitizer(toIntOrNull) - .custom(isVideoAbuseTimestampValid) - .withMessage('Should have valid starting time value'), - body('endAt') - .optional() - .customSanitizer(toIntOrNull) - .custom(isVideoAbuseTimestampValid) - .withMessage('Should have valid ending time value') - .bail() - .custom(isVideoAbuseTimestampCoherent) - .withMessage('Should have a startAt timestamp beginning before endAt'), - - async (req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) - - if (areValidationErrors(req, res)) return - if (!await doesVideoExist(req.params.videoId, res)) return - - return next() - } -] - -const videoAbuseGetValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), - param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), - - async (req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) - - if (areValidationErrors(req, res)) return - if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return - - return next() - } -] - -const videoAbuseUpdateValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), - param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), - body('state') - .optional() - .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'), - body('moderationComment') - .optional() - .custom(isVideoAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), - - async (req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) - - if (areValidationErrors(req, res)) return - if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return - - return next() - } -] - -const videoAbuseListValidator = [ - query('id') - .optional() - .custom(isIdValid).withMessage('Should have a valid id'), - query('predefinedReason') - .optional() - .custom(isVideoAbusePredefinedReasonValid) - .withMessage('Should have a valid predefinedReason'), - query('search') - .optional() - .custom(exists).withMessage('Should have a valid search'), - query('state') - .optional() - .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'), - query('videoIs') - .optional() - .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'), - query('searchReporter') - .optional() - .custom(exists).withMessage('Should have a valid reporter search'), - query('searchReportee') - .optional() - .custom(exists).withMessage('Should have a valid reportee search'), - query('searchVideo') - .optional() - .custom(exists).withMessage('Should have a valid video search'), - query('searchVideoChannel') - .optional() - .custom(exists).withMessage('Should have a valid video channel search'), - - (req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body }) - - if (areValidationErrors(req, res)) return - - return next() - } -] - -// --------------------------------------------------------------------------- - -export { - videoAbuseListValidator, - videoAbuseReportValidator, - videoAbuseGetValidator, - videoAbuseUpdateValidator -} diff --git a/server/models/video/video-abuse.ts b/server/models/abuse/abuse.ts similarity index 52% rename from server/models/video/video-abuse.ts rename to server/models/abuse/abuse.ts index 1319332f0..4f99f9c9b 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/abuse/abuse.ts @@ -1,5 +1,6 @@ import * as Bluebird from 'bluebird' -import { literal, Op } from 'sequelize' +import { invert } from 'lodash' +import { literal, Op, WhereOptions } from 'sequelize' import { AllowNull, BelongsTo, @@ -8,36 +9,35 @@ import { DataType, Default, ForeignKey, + HasOne, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' -import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' -import { - VideoAbuseState, - VideoDetails, - VideoAbusePredefinedReasons, - VideoAbusePredefinedReasonsString, - videoAbusePredefinedReasonsMap -} from '../../../shared' -import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' -import { VideoAbuse } from '../../../shared/models/videos' +import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' import { - isVideoAbuseModerationCommentValid, - isVideoAbuseReasonValid, - isVideoAbuseStateValid -} from '../../helpers/custom-validators/video-abuses' -import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' -import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models' -import { AccountModel } from '../account/account' + Abuse, + AbuseObject, + AbusePredefinedReasons, + abusePredefinedReasonsMap, + AbusePredefinedReasonsString, + AbuseState, + AbuseVideoIs, + VideoAbuse +} from '@shared/models' +import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter' +import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants' +import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' +import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' -import { ThumbnailModel } from './thumbnail' -import { VideoModel } from './video' -import { VideoBlacklistModel } from './video-blacklist' -import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' -import { invert } from 'lodash' +import { ThumbnailModel } from '../video/thumbnail' +import { VideoModel } from '../video/video' +import { VideoBlacklistModel } from '../video/video-blacklist' +import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' +import { VideoAbuseModel } from './video-abuse' +import { VideoCommentAbuseModel } from './video-comment-abuse' export enum ScopeNames { FOR_API = 'FOR_API' @@ -49,20 +49,26 @@ export enum ScopeNames { search?: string searchReporter?: string searchReportee?: string + + // video releated searchVideo?: string searchVideoChannel?: string + videoIs?: AbuseVideoIs // filters id?: number predefinedReasonId?: number + filter?: AbuseFilter - state?: VideoAbuseState - videoIs?: VideoAbuseVideoIs + state?: AbuseState // accountIds serverAccountId: number userAccountId: number }) => { + const onlyBlacklisted = options.videoIs === 'blacklisted' + const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel) + const where = { reporterAccountId: { [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') @@ -70,33 +76,36 @@ export enum ScopeNames { } if (options.search) { + const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%') + Object.assign(where, { [Op.or]: [ { [Op.and]: [ - { videoId: { [Op.not]: null } }, - searchAttribute(options.search, '$Video.name$') + { '$VideoAbuse.videoId$': { [Op.not]: null } }, + searchAttribute(options.search, '$VideoAbuse.Video.name$') ] }, { [Op.and]: [ - { videoId: { [Op.not]: null } }, - searchAttribute(options.search, '$Video.VideoChannel.name$') + { '$VideoAbuse.videoId$': { [Op.not]: null } }, + searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$') ] }, { [Op.and]: [ - { deletedVideo: { [Op.not]: null } }, - { deletedVideo: searchAttribute(options.search, 'name') } + { '$VideoAbuse.deletedVideo$': { [Op.not]: null } }, + literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`) ] }, { [Op.and]: [ - { deletedVideo: { [Op.not]: null } }, - { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } } + { '$VideoAbuse.deletedVideo$': { [Op.not]: null } }, + literal(`"VideoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE ${escapedSearch}`) ] }, - searchAttribute(options.search, '$Account.name$') + searchAttribute(options.search, '$ReporterAccount.name$'), + searchAttribute(options.search, '$FlaggedAccount.name$') ] }) } @@ -106,7 +115,7 @@ export enum ScopeNames { if (options.videoIs === 'deleted') { Object.assign(where, { - deletedVideo: { + '$VideoAbuse.deletedVideo$': { [Op.not]: null } }) @@ -120,8 +129,6 @@ export enum ScopeNames { }) } - const onlyBlacklisted = options.videoIs === 'blacklisted' - return { attributes: { include: [ @@ -131,7 +138,7 @@ export enum ScopeNames { '(' + 'SELECT count(*) ' + 'FROM "videoAbuse" ' + - 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' + + 'WHERE "videoId" = "VideoAbuse"."videoId" ' + ')' ), 'countReportsForVideo' @@ -146,7 +153,7 @@ export enum ScopeNames { 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + 'FROM "videoAbuse" ' + ') t ' + - 'WHERE t.id = "VideoAbuseModel".id ' + + 'WHERE t.id = "VideoAbuse".id' + ')' ), 'nthReportForVideo' @@ -159,7 +166,7 @@ export enum ScopeNames { 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + - 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' + + 'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' + ')' ), 'countReportsForReporter__video' @@ -169,7 +176,7 @@ export enum ScopeNames { '(' + 'SELECT count(DISTINCT "videoAbuse"."id") ' + 'FROM "videoAbuse" ' + - `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` + + `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` + ')' ), 'countReportsForReporter__deletedVideo' @@ -182,8 +189,8 @@ export enum ScopeNames { 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 'INNER JOIN "account" ON ' + - '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' + - `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + + '"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' + + `OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + ')' ), 'countReportsForReportee__video' @@ -193,9 +200,9 @@ export enum ScopeNames { '(' + 'SELECT count(DISTINCT "videoAbuse"."id") ' + 'FROM "videoAbuse" ' + - `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` + + `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuse->Video->VideoChannel"."accountId" ` + `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` + - `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + + `CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + ')' ), 'countReportsForReportee__deletedVideo' @@ -204,32 +211,47 @@ export enum ScopeNames { }, include: [ { - model: AccountModel, + model: AccountModel.scope(AccountScopeNames.SUMMARY), + as: 'ReporterAccount', required: true, where: searchAttribute(options.searchReporter, 'name') }, { - model: VideoModel, - required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel), - where: searchAttribute(options.searchVideo, 'name'), + model: AccountModel.scope(AccountScopeNames.SUMMARY), + as: 'FlaggedAccount', + required: true, + where: searchAttribute(options.searchReportee, 'name') + }, + { + model: VideoAbuseModel, + required: options.filter === 'video' || !!options.videoIs || videoRequired, include: [ { - model: ThumbnailModel - }, - { - model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), - where: searchAttribute(options.searchVideoChannel, 'name'), + model: VideoModel, + required: videoRequired, + where: searchAttribute(options.searchVideo, 'name'), include: [ { - model: AccountModel, - where: searchAttribute(options.searchReportee, 'name') + model: ThumbnailModel + }, + { + model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: false } as SummaryOptions ] }), + where: searchAttribute(options.searchVideoChannel, 'name'), + required: true, + include: [ + { + model: AccountModel.scope(AccountScopeNames.SUMMARY), + required: true, + where: searchAttribute(options.searchReportee, 'name') + } + ] + }, + { + attributes: [ 'id', 'reason', 'unfederated' ], + model: VideoBlacklistModel, + required: onlyBlacklisted } ] - }, - { - attributes: [ 'id', 'reason', 'unfederated' ], - model: VideoBlacklistModel, - required: onlyBlacklisted } ] } @@ -239,55 +261,40 @@ export enum ScopeNames { } })) @Table({ - tableName: 'videoAbuse', + tableName: 'abuse', indexes: [ { - fields: [ 'videoId' ] + fields: [ 'reporterAccountId' ] }, { - fields: [ 'reporterAccountId' ] + fields: [ 'flaggedAccountId' ] } ] }) -export class VideoAbuseModel extends Model { +export class AbuseModel extends Model { @AllowNull(false) @Default(null) - @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) + @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max)) reason: string @AllowNull(false) @Default(null) - @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state')) + @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) @Column - state: VideoAbuseState + state: AbuseState @AllowNull(true) @Default(null) - @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true)) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) + @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max)) moderationComment: string - @AllowNull(true) - @Default(null) - @Column(DataType.JSONB) - deletedVideo: VideoDetails - @AllowNull(true) @Default(null) @Column(DataType.ARRAY(DataType.INTEGER)) - predefinedReasons: VideoAbusePredefinedReasons[] - - @AllowNull(true) - @Default(null) - @Column - startAt: number - - @AllowNull(true) - @Default(null) - @Column - endAt: number + predefinedReasons: AbusePredefinedReasons[] @CreatedAt createdAt: Date @@ -301,36 +308,65 @@ export class VideoAbuseModel extends Model { @BelongsTo(() => AccountModel, { foreignKey: { + name: 'reporterAccountId', allowNull: true }, + as: 'ReporterAccount', onDelete: 'set null' }) - Account: AccountModel + ReporterAccount: AccountModel - @ForeignKey(() => VideoModel) + @ForeignKey(() => AccountModel) @Column - videoId: number + flaggedAccountId: number - @BelongsTo(() => VideoModel, { + @BelongsTo(() => AccountModel, { foreignKey: { + name: 'flaggedAccountId', allowNull: true }, + as: 'FlaggedAccount', onDelete: 'set null' }) - Video: VideoModel + FlaggedAccount: AccountModel + + @HasOne(() => VideoCommentAbuseModel, { + foreignKey: { + name: 'abuseId', + allowNull: false + }, + onDelete: 'cascade' + }) + VideoCommentAbuse: VideoCommentAbuseModel - static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird { - const videoAttributes = {} - if (videoId) videoAttributes['videoId'] = videoId - if (uuid) videoAttributes['deletedVideo'] = { uuid } + @HasOne(() => VideoAbuseModel, { + foreignKey: { + name: 'abuseId', + allowNull: false + }, + onDelete: 'cascade' + }) + VideoAbuse: VideoAbuseModel + + static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird { + const videoWhere: WhereOptions = {} + + if (videoId) videoWhere.videoId = videoId + if (uuid) videoWhere.deletedVideo = { uuid } const query = { + include: [ + { + model: VideoAbuseModel, + required: true, + where: videoWhere + } + ], where: { - id, - ...videoAttributes + id } } - return VideoAbuseModel.findOne(query) + return AbuseModel.findOne(query) } static listForApi (parameters: { @@ -338,13 +374,15 @@ export class VideoAbuseModel extends Model { count: number sort: string + filter?: AbuseFilter + serverAccountId: number user?: MUserAccountId id?: number - predefinedReason?: VideoAbusePredefinedReasonsString - state?: VideoAbuseState - videoIs?: VideoAbuseVideoIs + predefinedReason?: AbusePredefinedReasonsString + state?: AbuseState + videoIs?: AbuseVideoIs search?: string searchReporter?: string @@ -364,24 +402,26 @@ export class VideoAbuseModel extends Model { predefinedReason, searchReportee, searchVideo, + filter, searchVideoChannel, searchReporter, id } = parameters const userAccountId = user ? user.Account.id : undefined - const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined + const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined const query = { offset: start, limit: count, order: getSort(sort), - col: 'VideoAbuseModel.id', + col: 'AbuseModel.id', distinct: true } const filters = { id, + filter, predefinedReasonId, search, state, @@ -394,7 +434,7 @@ export class VideoAbuseModel extends Model { userAccountId } - return VideoAbuseModel + return AbuseModel .scope([ { method: [ ScopeNames.FOR_API, filters ] } ]) @@ -404,8 +444,8 @@ export class VideoAbuseModel extends Model { }) } - toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { - const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) + toFormattedJSON (this: MAbuseFormattable): Abuse { + const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) const countReportsForVideo = this.get('countReportsForVideo') as number const nthReportForVideo = this.get('nthReportForVideo') as number const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number @@ -413,51 +453,70 @@ export class VideoAbuseModel extends Model { const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number - const video = this.Video - ? this.Video - : this.deletedVideo + let video: VideoAbuse + + if (this.VideoAbuse) { + const abuseModel = this.VideoAbuse + const entity = abuseModel.Video || abuseModel.deletedVideo + + video = { + id: entity.id, + uuid: entity.uuid, + name: entity.name, + nsfw: entity.nsfw, + + startAt: abuseModel.startAt, + endAt: abuseModel.endAt, + + deleted: !abuseModel.Video, + blacklisted: abuseModel.Video?.isBlacklisted() || false, + thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), + channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel + } + } return { id: this.id, reason: this.reason, predefinedReasons, - reporterAccount: this.Account.toFormattedJSON(), + + reporterAccount: this.ReporterAccount.toFormattedJSON(), + state: { id: this.state, - label: VideoAbuseModel.getStateLabel(this.state) + label: AbuseModel.getStateLabel(this.state) }, + moderationComment: this.moderationComment, - video: { - id: video.id, - uuid: video.uuid, - name: video.name, - nsfw: video.nsfw, - deleted: !this.Video, - blacklisted: this.Video?.isBlacklisted() || false, - thumbnailPath: this.Video?.getMiniatureStaticPath(), - channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel - }, + + video, + comment: null, + createdAt: this.createdAt, updatedAt: this.updatedAt, - startAt: this.startAt, - endAt: this.endAt, count: countReportsForVideo || 0, nth: nthReportForVideo || 0, countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), - countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0) + countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0), + + // FIXME: deprecated in 2.3, remove this + startAt: null, + endAt: null } } - toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject { - const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) + toActivityPubObject (this: MAbuseAP): AbuseObject { + const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) + + const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url - const startAt = this.startAt - const endAt = this.endAt + const startAt = this.VideoAbuse?.startAt + const endAt = this.VideoAbuse?.endAt return { type: 'Flag' as 'Flag', content: this.reason, - object: this.Video.url, + object, tag: predefinedReasons.map(r => ({ type: 'Hashtag' as 'Hashtag', name: r @@ -468,12 +527,12 @@ export class VideoAbuseModel extends Model { } private static getStateLabel (id: number) { - return VIDEO_ABUSE_STATES[id] || 'Unknown' + return ABUSE_STATES[id] || 'Unknown' } - private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] { + private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] { return (predefinedReasons || []) - .filter(r => r in VideoAbusePredefinedReasons) - .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString) + .filter(r => r in AbusePredefinedReasons) + .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString) } } diff --git a/server/models/abuse/video-abuse.ts b/server/models/abuse/video-abuse.ts new file mode 100644 index 000000000..d92bcf19f --- /dev/null +++ b/server/models/abuse/video-abuse.ts @@ -0,0 +1,63 @@ +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { VideoDetails } from '@shared/models' +import { VideoModel } from '../video/video' +import { AbuseModel } from './abuse' + +@Table({ + tableName: 'videoAbuse', + indexes: [ + { + fields: [ 'abuseId' ] + }, + { + fields: [ 'videoId' ] + } + ] +}) +export class VideoAbuseModel extends Model { + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AllowNull(true) + @Default(null) + @Column + startAt: number + + @AllowNull(true) + @Default(null) + @Column + endAt: number + + @AllowNull(true) + @Default(null) + @Column(DataType.JSONB) + deletedVideo: VideoDetails + + @ForeignKey(() => AbuseModel) + @Column + abuseId: number + + @BelongsTo(() => AbuseModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + Abuse: AbuseModel + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'set null' + }) + Video: VideoModel +} diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts new file mode 100644 index 000000000..b4cc2762e --- /dev/null +++ b/server/models/abuse/video-comment-abuse.ts @@ -0,0 +1,53 @@ +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { VideoComment } from '@shared/models' +import { VideoCommentModel } from '../video/video-comment' +import { AbuseModel } from './abuse' + +@Table({ + tableName: 'commentAbuse', + indexes: [ + { + fields: [ 'abuseId' ] + }, + { + fields: [ 'videoCommentId' ] + } + ] +}) +export class VideoCommentAbuseModel extends Model { + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AllowNull(true) + @Default(null) + @Column(DataType.JSONB) + deletedComment: VideoComment + + @ForeignKey(() => AbuseModel) + @Column + abuseId: number + + @BelongsTo(() => AbuseModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + Abuse: AbuseModel + + @ForeignKey(() => VideoCommentModel) + @Column + videoCommentId: number + + @BelongsTo(() => VideoCommentModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'set null' + }) + VideoComment: VideoCommentModel +} diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index cf8872fd5..577b7dc19 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts @@ -1,12 +1,12 @@ -import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' -import { AccountModel } from './account' -import { getSort, searchAttribute } from '../utils' -import { AccountBlock } from '../../../shared/models/blocklist' -import { Op } from 'sequelize' import * as Bluebird from 'bluebird' +import { Op } from 'sequelize' +import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' +import { AccountBlock } from '../../../shared/models' import { ActorModel } from '../activitypub/actor' import { ServerModel } from '../server/server' +import { getSort, searchAttribute } from '../utils' +import { AccountModel } from './account' enum ScopeNames { WITH_ACCOUNTS = 'WITH_ACCOUNTS' diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 4395d179a..466d6258e 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -388,6 +388,10 @@ export class AccountModel extends Model { .findAll(query) } + getClientUrl () { + return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier() + } + toFormattedJSON (this: MAccountFormattable): Account { const actor = this.Actor.toFormattedJSON() const account = { diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 30985bb0f..07db5a2db 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts @@ -1,22 +1,24 @@ +import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' import { UserNotification, UserNotificationType } from '../../../shared' -import { getSort, throwIfNotValid } from '../utils' import { isBooleanValid } from '../../helpers/custom-validators/misc' import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' -import { UserModel } from './user' -import { VideoModel } from '../video/video' -import { VideoCommentModel } from '../video/video-comment' -import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' -import { VideoChannelModel } from '../video/video-channel' -import { AccountModel } from './account' -import { VideoAbuseModel } from '../video/video-abuse' -import { VideoBlacklistModel } from '../video/video-blacklist' -import { VideoImportModel } from '../video/video-import' +import { AbuseModel } from '../abuse/abuse' +import { VideoAbuseModel } from '../abuse/video-abuse' +import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' import { ActorModel } from '../activitypub/actor' import { ActorFollowModel } from '../activitypub/actor-follow' import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' -import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' +import { getSort, throwIfNotValid } from '../utils' +import { VideoModel } from '../video/video' +import { VideoBlacklistModel } from '../video/video-blacklist' +import { VideoChannelModel } from '../video/video-channel' +import { VideoCommentModel } from '../video/video-comment' +import { VideoImportModel } from '../video/video-import' +import { AccountModel } from './account' +import { UserModel } from './user' enum ScopeNames { WITH_ALL = 'WITH_ALL' @@ -87,9 +89,41 @@ function buildAccountInclude (required: boolean, withActor = false) { { attributes: [ 'id' ], - model: VideoAbuseModel.unscoped(), + model: AbuseModel.unscoped(), required: false, - include: [ buildVideoInclude(true) ] + include: [ + { + attributes: [ 'id' ], + model: VideoAbuseModel.unscoped(), + required: false, + include: [ buildVideoInclude(true) ] + }, + { + attributes: [ 'id' ], + model: VideoCommentAbuseModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id', 'originCommentId' ], + model: VideoCommentModel, + required: true, + include: [ + { + attributes: [ 'uuid' ], + model: VideoModel.unscoped(), + required: true + } + ] + } + ] + }, + { + model: AccountModel, + as: 'FlaggedAccount', + required: true, + include: [ buildActorWithAvatarInclude() ] + } + ] }, { @@ -179,9 +213,9 @@ function buildAccountInclude (required: boolean, withActor = false) { } }, { - fields: [ 'videoAbuseId' ], + fields: [ 'abuseId' ], where: { - videoAbuseId: { + abuseId: { [Op.ne]: null } } @@ -276,17 +310,17 @@ export class UserNotificationModel extends Model { }) Comment: VideoCommentModel - @ForeignKey(() => VideoAbuseModel) + @ForeignKey(() => AbuseModel) @Column - videoAbuseId: number + abuseId: number - @BelongsTo(() => VideoAbuseModel, { + @BelongsTo(() => AbuseModel, { foreignKey: { allowNull: true }, onDelete: 'cascade' }) - VideoAbuse: VideoAbuseModel + Abuse: AbuseModel @ForeignKey(() => VideoBlacklistModel) @Column @@ -397,10 +431,7 @@ export class UserNotificationModel extends Model { video: this.formatVideo(this.Comment.Video) } : undefined - const videoAbuse = this.VideoAbuse ? { - id: this.VideoAbuse.id, - video: this.formatVideo(this.VideoAbuse.Video) - } : undefined + const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined const videoBlacklist = this.VideoBlacklist ? { id: this.VideoBlacklist.id, @@ -439,7 +470,7 @@ export class UserNotificationModel extends Model { video, videoImport, comment, - videoAbuse, + abuse, videoBlacklist, account, actorFollow, @@ -456,6 +487,27 @@ export class UserNotificationModel extends Model { } } + formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { + const commentAbuse = abuse.VideoCommentAbuse?.VideoComment ? { + threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), + + video: { + uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid + } + } : undefined + + const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined + + const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined + + return { + id: abuse.id, + video: videoAbuse, + comment: commentAbuse, + account: accountAbuse + } + } + formatActor ( this: UserNotificationModelForApi, accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor diff --git a/server/models/account/user.ts b/server/models/account/user.ts index de193131a..f21eff04b 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -19,7 +19,7 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' +import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' import { User, UserRole } from '../../../shared/models/users' import { isNoInstanceConfigWarningModal, @@ -169,7 +169,7 @@ enum ScopeNames { `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + 'FROM (' + 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + - `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` + + `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + 'FROM "videoAbuse" ' + 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 30f0525e5..68cd72ee7 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts @@ -1,11 +1,11 @@ +import * as Bluebird from 'bluebird' +import { Op } from 'sequelize' import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' +import { ServerBlock } from '@shared/models' import { AccountModel } from '../account/account' -import { ServerModel } from './server' -import { ServerBlock } from '../../../shared/models/blocklist' import { getSort, searchAttribute } from '../utils' -import * as Bluebird from 'bluebird' -import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' -import { Op } from 'sequelize' +import { ServerModel } from './server' enum ScopeNames { WITH_ACCOUNT = 'WITH_ACCOUNT', diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e2718300e..272bba0e1 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1,4 +1,5 @@ import * as Bluebird from 'bluebird' +import { remove } from 'fs-extra' import { maxBy, minBy, pick } from 'lodash' import { join } from 'path' import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' @@ -23,10 +24,18 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared' +import { buildNSFWFilter } from '@server/helpers/express-utils' +import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video' +import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' +import { getServerActor } from '@server/models/application/application' +import { ModelCache } from '@server/models/model-cache' +import { VideoFile } from '@shared/models/videos/video-file.model' +import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' import { Video, VideoDetails } from '../../../shared/models/videos' +import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' import { VideoFilter } from '../../../shared/models/videos/video-query.type' +import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' import { peertubeTruncate } from '../../helpers/core-utils' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isBooleanValid } from '../../helpers/custom-validators/misc' @@ -43,6 +52,7 @@ import { } from '../../helpers/custom-validators/videos' import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' import { logger } from '../../helpers/logger' +import { CONFIG } from '../../initializers/config' import { ACTIVITY_PUB, API_VERSION, @@ -59,40 +69,6 @@ import { WEBSERVER } from '../../initializers/constants' import { sendDeleteVideo } from '../../lib/activitypub/send' -import { AccountModel } from '../account/account' -import { AccountVideoRateModel } from '../account/account-video-rate' -import { ActorModel } from '../activitypub/actor' -import { AvatarModel } from '../avatar/avatar' -import { ServerModel } from '../server/server' -import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' -import { TagModel } from './tag' -import { VideoAbuseModel } from './video-abuse' -import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' -import { VideoCommentModel } from './video-comment' -import { VideoFileModel } from './video-file' -import { VideoShareModel } from './video-share' -import { VideoTagModel } from './video-tag' -import { ScheduleVideoUpdateModel } from './schedule-video-update' -import { VideoCaptionModel } from './video-caption' -import { VideoBlacklistModel } from './video-blacklist' -import { remove } from 'fs-extra' -import { VideoViewModel } from './video-view' -import { VideoRedundancyModel } from '../redundancy/video-redundancy' -import { - videoFilesModelToFormattedJSON, - VideoFormattingJSONOptions, - videoModelToActivityPubObject, - videoModelToFormattedDetailsJSON, - videoModelToFormattedJSON -} from './video-format-utils' -import { UserVideoHistoryModel } from '../account/user-video-history' -import { VideoImportModel } from './video-import' -import { VideoStreamingPlaylistModel } from './video-streaming-playlist' -import { VideoPlaylistElementModel } from './video-playlist-element' -import { CONFIG } from '../../initializers/config' -import { ThumbnailModel } from './thumbnail' -import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' -import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' import { MChannel, MChannelAccountDefault, @@ -118,15 +94,39 @@ import { MVideoWithFile, MVideoWithRights } from '../../types/models' -import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' import { MThumbnail } from '../../types/models/video/thumbnail' -import { VideoFile } from '@shared/models/videos/video-file.model' -import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' -import { ModelCache } from '@server/models/model-cache' +import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' +import { VideoAbuseModel } from '../abuse/video-abuse' +import { AccountModel } from '../account/account' +import { AccountVideoRateModel } from '../account/account-video-rate' +import { UserVideoHistoryModel } from '../account/user-video-history' +import { ActorModel } from '../activitypub/actor' +import { AvatarModel } from '../avatar/avatar' +import { VideoRedundancyModel } from '../redundancy/video-redundancy' +import { ServerModel } from '../server/server' +import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' +import { ScheduleVideoUpdateModel } from './schedule-video-update' +import { TagModel } from './tag' +import { ThumbnailModel } from './thumbnail' +import { VideoBlacklistModel } from './video-blacklist' +import { VideoCaptionModel } from './video-caption' +import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' +import { VideoCommentModel } from './video-comment' +import { VideoFileModel } from './video-file' +import { + videoFilesModelToFormattedJSON, + VideoFormattingJSONOptions, + videoModelToActivityPubObject, + videoModelToFormattedDetailsJSON, + videoModelToFormattedJSON +} from './video-format-utils' +import { VideoImportModel } from './video-import' +import { VideoPlaylistElementModel } from './video-playlist-element' import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' -import { buildNSFWFilter } from '@server/helpers/express-utils' -import { getServerActor } from '@server/models/application/application' -import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video" +import { VideoShareModel } from './video-share' +import { VideoStreamingPlaylistModel } from './video-streaming-playlist' +import { VideoTagModel } from './video-tag' +import { VideoViewModel } from './video-view' export enum ScopeNames { AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts index 557bf20eb..f122baef4 100644 --- a/server/tests/api/check-params/video-abuses.ts +++ b/server/tests/api/check-params/video-abuses.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ import 'mocha' - +import { AbuseState, VideoAbuseCreate } from '@shared/models' import { cleanupTests, createUser, @@ -20,7 +20,8 @@ import { checkBadSortPagination, checkBadStartPagination } from '../../../../shared/extra-utils/requests/check-api-params' -import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos' + +// FIXME: deprecated in 2.3. Remove this controller describe('Test video abuses API validators', function () { let server: ServerInfo @@ -136,7 +137,7 @@ describe('Test video abuses API validators', function () { const fields = { reason: 'my super reason' } const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) - videoAbuseId = res.body.videoAbuse.id + videoAbuseId = res.body.abuse.id }) it('Should fail with a wrong predefined reason', async function () { @@ -190,7 +191,7 @@ describe('Test video abuses API validators', function () { }) it('Should succeed with the correct params', async function () { - const body = { state: VideoAbuseState.ACCEPTED } + const body = { state: AbuseState.ACCEPTED } await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body) }) }) diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 0a66bd1ce..88b68d977 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -2,7 +2,7 @@ import * as chai from 'chai' import 'mocha' -import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index' +import { MyUser, User, UserRole, Video, AbuseState, AbuseUpdate, VideoPlaylistType } from '@shared/models' import { addVideoCommentThread, blockUser, @@ -937,7 +937,7 @@ describe('Test users', function () { expect(user2.videoAbusesCount).to.equal(1) // number of incriminations expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created - const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED } + const body: AbuseUpdate = { state: AbuseState.ACCEPTED } await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body) const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true) diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts index 7383bd991..20975aa4a 100644 --- a/server/tests/api/videos/video-abuse.ts +++ b/server/tests/api/videos/video-abuse.ts @@ -1,21 +1,21 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ -import * as chai from 'chai' import 'mocha' -import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos' +import * as chai from 'chai' +import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models' import { cleanupTests, + createUser, deleteVideoAbuse, flushAndRunMultipleServers, getVideoAbusesList, getVideosList, + removeVideo, reportVideoAbuse, ServerInfo, setAccessTokensToServers, updateVideoAbuse, uploadVideo, - removeVideo, - createUser, userLogin } from '../../../../shared/extra-utils/index' import { doubleFollow } from '../../../../shared/extra-utils/server/follows' @@ -29,9 +29,11 @@ import { const expect = chai.expect +// FIXME: deprecated in 2.3. Remove this controller + describe('Test video abuses', function () { let servers: ServerInfo[] = [] - let abuseServer2: VideoAbuse + let abuseServer2: Abuse before(async function () { this.timeout(50000) @@ -95,7 +97,7 @@ describe('Test video abuses', function () { expect(res1.body.data).to.be.an('array') expect(res1.body.data.length).to.equal(1) - const abuse: VideoAbuse = res1.body.data[0] + const abuse: Abuse = res1.body.data[0] expect(abuse.reason).to.equal('my super bad reason') expect(abuse.reporterAccount.name).to.equal('root') expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port) @@ -128,23 +130,23 @@ describe('Test video abuses', function () { expect(res1.body.data).to.be.an('array') expect(res1.body.data.length).to.equal(2) - const abuse1: VideoAbuse = res1.body.data[0] + const abuse1: Abuse = res1.body.data[0] expect(abuse1.reason).to.equal('my super bad reason') expect(abuse1.reporterAccount.name).to.equal('root') expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port) expect(abuse1.video.id).to.equal(servers[0].video.id) - expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING) + expect(abuse1.state.id).to.equal(AbuseState.PENDING) expect(abuse1.state.label).to.equal('Pending') expect(abuse1.moderationComment).to.be.null expect(abuse1.count).to.equal(1) expect(abuse1.nth).to.equal(1) - const abuse2: VideoAbuse = res1.body.data[1] + const abuse2: Abuse = res1.body.data[1] expect(abuse2.reason).to.equal('my super bad reason 2') expect(abuse2.reporterAccount.name).to.equal('root') expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port) expect(abuse2.video.id).to.equal(servers[1].video.id) - expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING) + expect(abuse2.state.id).to.equal(AbuseState.PENDING) expect(abuse2.state.label).to.equal('Pending') expect(abuse2.moderationComment).to.be.null @@ -157,25 +159,25 @@ describe('Test video abuses', function () { expect(abuseServer2.reason).to.equal('my super bad reason 2') expect(abuseServer2.reporterAccount.name).to.equal('root') expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port) - expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING) + expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) expect(abuseServer2.state.label).to.equal('Pending') expect(abuseServer2.moderationComment).to.be.null }) it('Should update the state of a video abuse', async function () { - const body = { state: VideoAbuseState.REJECTED } + const body = { state: AbuseState.REJECTED } await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) - expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED) + expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED) }) it('Should add a moderation comment', async function () { - const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' } + const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' } await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) - expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED) + expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED) expect(res.body.data[0].moderationComment).to.equal('It is valid') }) @@ -243,7 +245,7 @@ describe('Test video abuses', function () { expect(res.body.data.length).to.equal(2, "wrong number of videos returned") expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video") - const abuse: VideoAbuse = res.body.data[0] + const abuse: Abuse = res.body.data[0] expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id") expect(abuse.video.channel).to.exist expect(abuse.video.deleted).to.be.true @@ -277,7 +279,7 @@ describe('Test video abuses', function () { const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) { - for (const abuse of res2.body.data as VideoAbuse[]) { + for (const abuse of res2.body.data as Abuse[]) { if (abuse.video.id === video3.id) { expect(abuse.count).to.equal(1, "wrong reports count for video 3") expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3") @@ -295,7 +297,7 @@ describe('Test video abuses', function () { this.timeout(10000) const reason5 = 'my super bad reason 5' - const predefinedReasons5: VideoAbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] + const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] const createdAbuse = (await reportVideoAbuse( servers[0].url, servers[0].accessToken, @@ -304,16 +306,16 @@ describe('Test video abuses', function () { predefinedReasons5, 1, 5 - )).body.videoAbuse as VideoAbuse + )).body.abuse const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) { - const abuse = (res.body.data as VideoAbuse[]).find(a => a.id === createdAbuse.id) + const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id) expect(abuse.reason).to.equals(reason5) expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported") - expect(abuse.startAt).to.equal(1, "starting timestamp doesn't match the one reported") - expect(abuse.endAt).to.equal(5, "ending timestamp doesn't match the one reported") + expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported") + expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported") } }) @@ -348,7 +350,7 @@ describe('Test video abuses', function () { const res = await getVideoAbusesList(options) - return res.body.data as VideoAbuse[] + return res.body.data as Abuse[] } expect(await list({ id: 56 })).to.have.lengthOf(0) @@ -365,14 +367,14 @@ describe('Test video abuses', function () { expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5) - expect(await list({ searchReportee: 'root' })).to.have.lengthOf(4) + expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5) expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) - expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0) - expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(6) + expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0) + expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6) expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0) diff --git a/server/types/models/index.ts b/server/types/models/index.ts index 78b4948ce..affa17425 100644 --- a/server/types/models/index.ts +++ b/server/types/models/index.ts @@ -1,4 +1,5 @@ export * from './account' +export * from './moderation' export * from './oauth' export * from './server' export * from './user' diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts new file mode 100644 index 000000000..abbc93d6f --- /dev/null +++ b/server/types/models/moderation/abuse.ts @@ -0,0 +1,97 @@ +import { VideoAbuseModel } from '@server/models/abuse/video-abuse' +import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' +import { PickWith } from '@shared/core-utils' +import { AbuseModel } from '../../../models/abuse/abuse' +import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account' +import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo } from '../video' +import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' + +type Use = PickWith +type UseVideoAbuse = PickWith +type UseCommentAbuse = PickWith + +// ############################################################################ + +export type MAbuse = Omit + +export type MVideoAbuse = Omit + +export type MCommentAbuse = Omit + +// ############################################################################ + +export type MVideoAbuseVideo = + MVideoAbuse & + UseVideoAbuse<'Video', MVideo> + +export type MVideoAbuseVideoUrl = + MVideoAbuse & + UseVideoAbuse<'Video', MVideoUrl> + +export type MVideoAbuseVideoFull = + MVideoAbuse & + UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles> + +export type MVideoAbuseFormattable = + MVideoAbuse & + UseVideoAbuse<'Video', Pick> + +// ############################################################################ + +export type MCommentAbuseAccount = + MCommentAbuse & + UseCommentAbuse<'VideoComment', MCommentOwner> + +export type MCommentAbuseAccountVideo = + MCommentAbuse & + UseCommentAbuse<'VideoComment', MCommentOwnerVideo> + +export type MCommentAbuseUrl = + MCommentAbuse & + UseCommentAbuse<'VideoComment', MCommentUrl> + +// ############################################################################ + +export type MAbuseId = Pick + +export type MAbuseVideo = + MAbuse & + Pick & + Use<'VideoAbuse', MVideoAbuseVideo> + +export type MAbuseUrl = + MAbuse & + Use<'VideoAbuse', MVideoAbuseVideoUrl> & + Use<'VideoCommentAbuse', MCommentAbuseUrl> + +export type MAbuseAccountVideo = + MAbuse & + Pick & + Use<'VideoAbuse', MVideoAbuseVideoFull> & + Use<'ReporterAccount', MAccountDefault> + +export type MAbuseAP = + MAbuse & + Pick & + Use<'ReporterAccount', MAccountUrl> & + Use<'FlaggedAccount', MAccountUrl> & + Use<'VideoAbuse', MVideoAbuseVideo> & + Use<'VideoCommentAbuse', MCommentAbuseAccount> + +export type MAbuseFull = + MAbuse & + Pick & + Use<'ReporterAccount', MAccountLight> & + Use<'FlaggedAccount', MAccountLight> & + Use<'VideoAbuse', MVideoAbuseVideoFull> & + Use<'VideoCommentAbuse', MCommentAbuseAccountVideo> + +// ############################################################################ + +// Format for API or AP object + +export type MAbuseFormattable = + MAbuse & + Use<'ReporterAccount', MAccountFormattable> & + Use<'VideoAbuse', MVideoAbuseFormattable> diff --git a/server/types/models/moderation/index.ts b/server/types/models/moderation/index.ts new file mode 100644 index 000000000..8bea1708f --- /dev/null +++ b/server/types/models/moderation/index.ts @@ -0,0 +1 @@ +export * from './abuse' diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index dd3de423b..92ea16768 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts @@ -1,16 +1,18 @@ -import { UserNotificationModel } from '../../../models/account/user-notification' +import { VideoAbuseModel } from '@server/models/abuse/video-abuse' +import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' import { PickWith, PickWithOpt } from '@shared/core-utils' -import { VideoModel } from '../../../models/video/video' +import { AbuseModel } from '../../../models/abuse/abuse' +import { AccountModel } from '../../../models/account/account' +import { UserNotificationModel } from '../../../models/account/user-notification' import { ActorModel } from '../../../models/activitypub/actor' -import { ServerModel } from '../../../models/server/server' +import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { AvatarModel } from '../../../models/avatar/avatar' +import { ServerModel } from '../../../models/server/server' +import { VideoModel } from '../../../models/video/video' +import { VideoBlacklistModel } from '../../../models/video/video-blacklist' import { VideoChannelModel } from '../../../models/video/video-channel' -import { AccountModel } from '../../../models/account/account' import { VideoCommentModel } from '../../../models/video/video-comment' -import { VideoAbuseModel } from '../../../models/video/video-abuse' -import { VideoBlacklistModel } from '../../../models/video/video-blacklist' import { VideoImportModel } from '../../../models/video/video-import' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' type Use = PickWith @@ -47,6 +49,18 @@ export module UserNotificationIncludes { Pick & PickWith + export type VideoCommentAbuseInclude = + Pick & + PickWith & + PickWith>> + + export type AbuseInclude = + Pick & + PickWith & + PickWith & + PickWith + export type VideoBlacklistInclude = Pick & PickWith @@ -76,7 +90,7 @@ export module UserNotificationIncludes { // ############################################################################ export type MUserNotification = - Omit // ############################################################################ @@ -85,7 +99,7 @@ export type UserNotificationModelForApi = MUserNotification & Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & Use<'Comment', UserNotificationIncludes.VideoCommentInclude> & - Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> & + Use<'Abuse', UserNotificationIncludes.AbuseInclude> & Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & diff --git a/server/types/models/video/index.ts b/server/types/models/video/index.ts index bd69c8a4b..25db23898 100644 --- a/server/types/models/video/index.ts +++ b/server/types/models/video/index.ts @@ -2,7 +2,6 @@ export * from './schedule-video-update' export * from './tag' export * from './thumbnail' export * from './video' -export * from './video-abuse' export * from './video-blacklist' export * from './video-caption' export * from './video-change-ownership' diff --git a/server/types/models/video/video-abuse.ts b/server/types/models/video/video-abuse.ts deleted file mode 100644 index 279a87cf3..000000000 --- a/server/types/models/video/video-abuse.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { VideoAbuseModel } from '../../../models/video/video-abuse' -import { PickWith } from '@shared/core-utils' -import { MVideoAccountLightBlacklistAllFiles, MVideo } from './video' -import { MAccountDefault, MAccountFormattable } from '../account' - -type Use = PickWith - -// ############################################################################ - -export type MVideoAbuse = Omit - -// ############################################################################ - -export type MVideoAbuseId = Pick - -export type MVideoAbuseVideo = - MVideoAbuse & - Pick & - Use<'Video', MVideo> - -export type MVideoAbuseAccountVideo = - MVideoAbuse & - Pick & - Use<'Video', MVideoAccountLightBlacklistAllFiles> & - Use<'Account', MAccountDefault> - -// ############################################################################ - -// Format for API or AP object - -export type MVideoAbuseFormattable = - MVideoAbuse & - Use<'Account', MAccountFormattable> & - Use<'Video', Pick> diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index cac801e55..7595e6d86 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts @@ -1,5 +1,6 @@ import { RegisterServerAuthExternalOptions } from '@server/types' import { + MAbuse, MAccountBlocklist, MActorUrl, MStreamingPlaylist, @@ -26,7 +27,6 @@ import { MComment, MCommentOwnerVideoReply, MUserDefault, - MVideoAbuse, MVideoBlacklist, MVideoCaptionVideo, MVideoFullLight, @@ -77,7 +77,7 @@ declare module 'express' { videoCaption?: MVideoCaptionVideo - videoAbuse?: MVideoAbuse + abuse?: MAbuse videoStreamingPlaylist?: MStreamingPlaylist diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts index 2ac0c6338..af4d23856 100644 --- a/shared/extra-utils/index.ts +++ b/shared/extra-utils/index.ts @@ -17,6 +17,7 @@ export * from './videos/services' export * from './videos/video-playlists' export * from './users/users' export * from './users/accounts' +export * from './moderation/abuses' export * from './videos/video-abuses' export * from './videos/video-blacklist' export * from './videos/video-captions' diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts new file mode 100644 index 000000000..48a51e2b8 --- /dev/null +++ b/shared/extra-utils/moderation/abuses.ts @@ -0,0 +1,112 @@ +import * as request from 'supertest' +import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models' +import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests' + +function reportAbuse ( + url: string, + token: string, + videoId: number | string, + reason: string, + predefinedReasons?: AbusePredefinedReasonsString[], + startAt?: number, + endAt?: number, + specialStatus = 200 +) { + const path = '/api/v1/videos/' + videoId + '/abuse' + + return request(url) + .post(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + token) + .send({ reason, predefinedReasons, startAt, endAt }) + .expect(specialStatus) +} + +function getAbusesList (options: { + url: string + token: string + id?: number + predefinedReason?: AbusePredefinedReasonsString + search?: string + state?: AbuseState + videoIs?: AbuseVideoIs + searchReporter?: string + searchReportee?: string + searchVideo?: string + searchVideoChannel?: string +}) { + const { + url, + token, + id, + predefinedReason, + search, + state, + videoIs, + searchReporter, + searchReportee, + searchVideo, + searchVideoChannel + } = options + const path = '/api/v1/videos/abuse' + + const query = { + sort: 'createdAt', + id, + predefinedReason, + search, + state, + videoIs, + searchReporter, + searchReportee, + searchVideo, + searchVideoChannel + } + + return makeGetRequest({ + url, + path, + token, + query, + statusCodeExpected: 200 + }) +} + +function updateAbuse ( + url: string, + token: string, + videoId: string | number, + videoAbuseId: number, + body: AbuseUpdate, + statusCodeExpected = 204 +) { + const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId + + return makePutBodyRequest({ + url, + token, + path, + fields: body, + statusCodeExpected + }) +} + +function deleteAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) { + const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId + + return makeDeleteRequest({ + url, + token, + path, + statusCodeExpected + }) +} + +// --------------------------------------------------------------------------- + +export { + reportAbuse, + getAbusesList, + updateAbuse, + deleteAbuse +} diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts index a17a39de9..62f3418c5 100644 --- a/shared/extra-utils/users/user-notifications.ts +++ b/shared/extra-utils/users/user-notifications.ts @@ -443,11 +443,11 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU expect(notification).to.not.be.undefined expect(notification.type).to.equal(notificationType) - expect(notification.videoAbuse.id).to.be.a('number') - checkVideo(notification.videoAbuse.video, videoName, videoUUID) + expect(notification.abuse.id).to.be.a('number') + checkVideo(notification.abuse.video, videoName, videoUUID) } else { expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID + return n === undefined || n.abuse === undefined || n.abuse.video.uuid !== videoUUID }) } } diff --git a/shared/extra-utils/videos/video-abuses.ts b/shared/extra-utils/videos/video-abuses.ts index ff006672a..8827b8196 100644 --- a/shared/extra-utils/videos/video-abuses.ts +++ b/shared/extra-utils/videos/video-abuses.ts @@ -1,15 +1,15 @@ import * as request from 'supertest' -import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model' -import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests' -import { VideoAbuseState, VideoAbusePredefinedReasonsString } from '@shared/models' -import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' +import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models' +import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests' + +// FIXME: deprecated in 2.3. Remove this file function reportVideoAbuse ( url: string, token: string, videoId: number | string, reason: string, - predefinedReasons?: VideoAbusePredefinedReasonsString[], + predefinedReasons?: AbusePredefinedReasonsString[], startAt?: number, endAt?: number, specialStatus = 200 @@ -28,10 +28,10 @@ function getVideoAbusesList (options: { url: string token: string id?: number - predefinedReason?: VideoAbusePredefinedReasonsString + predefinedReason?: AbusePredefinedReasonsString search?: string - state?: VideoAbuseState - videoIs?: VideoAbuseVideoIs + state?: AbuseState + videoIs?: AbuseVideoIs searchReporter?: string searchReportee?: string searchVideo?: string @@ -79,7 +79,7 @@ function updateVideoAbuse ( token: string, videoId: string | number, videoAbuseId: number, - body: VideoAbuseUpdate, + body: AbuseUpdate, statusCodeExpected = 204 ) { const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index 31b9e4673..5b4ce214a 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -1,12 +1,12 @@ import { ActivityPubActor } from './activitypub-actor' import { ActivityPubSignature } from './activitypub-signature' -import { CacheFileObject, VideoTorrentObject, ActivityFlagReasonObject } from './objects' +import { ActivityFlagReasonObject, CacheFileObject, VideoTorrentObject } from './objects' +import { AbuseObject } from './objects/abuse-object' import { DislikeObject } from './objects/dislike-object' -import { VideoAbuseObject } from './objects/video-abuse-object' -import { VideoCommentObject } from './objects/video-comment-object' -import { ViewObject } from './objects/view-object' import { APObject } from './objects/object.model' import { PlaylistObject } from './objects/playlist-object' +import { VideoCommentObject } from './objects/video-comment-object' +import { ViewObject } from './objects/view-object' export type Activity = ActivityCreate | @@ -53,7 +53,7 @@ export interface BaseActivity { export interface ActivityCreate extends BaseActivity { type: 'Create' - object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject + object: VideoTorrentObject | AbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject } export interface ActivityUpdate extends BaseActivity { diff --git a/shared/models/activitypub/objects/video-abuse-object.ts b/shared/models/activitypub/objects/abuse-object.ts similarity index 84% rename from shared/models/activitypub/objects/video-abuse-object.ts rename to shared/models/activitypub/objects/abuse-object.ts index 73add8ef4..ad45cc064 100644 --- a/shared/models/activitypub/objects/video-abuse-object.ts +++ b/shared/models/activitypub/objects/abuse-object.ts @@ -1,10 +1,12 @@ import { ActivityFlagReasonObject } from './common-objects' -export interface VideoAbuseObject { +export interface AbuseObject { type: 'Flag' content: string object: string | string[] + tag?: ActivityFlagReasonObject[] + startAt?: number endAt?: number } diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts index 096d422ea..711ce45f4 100644 --- a/shared/models/activitypub/objects/common-objects.ts +++ b/shared/models/activitypub/objects/common-objects.ts @@ -1,4 +1,4 @@ -import { VideoAbusePredefinedReasonsString } from '@shared/models/videos' +import { AbusePredefinedReasonsString } from '@shared/models' export interface ActivityIdentifierObject { identifier: string @@ -85,7 +85,7 @@ export interface ActivityMentionObject { export interface ActivityFlagReasonObject { type: 'Hashtag' - name: VideoAbusePredefinedReasonsString + name: AbusePredefinedReasonsString } export type ActivityTagObject = diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts index fba61e12f..a6a20e87a 100644 --- a/shared/models/activitypub/objects/index.ts +++ b/shared/models/activitypub/objects/index.ts @@ -1,6 +1,6 @@ +export * from './abuse-object' export * from './cache-file-object' export * from './common-objects' -export * from './video-abuse-object' +export * from './dislike-object' export * from './video-torrent-object' export * from './view-object' -export * from './dislike-object' diff --git a/shared/models/index.ts b/shared/models/index.ts index 3d4bdedde..a68f57148 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts @@ -1,7 +1,7 @@ export * from './activitypub' export * from './actors' export * from './avatars' -export * from './blocklist' +export * from './moderation' export * from './bulk' export * from './redundancy' export * from './users' @@ -14,4 +14,3 @@ export * from './search' export * from './server' export * from './oauth-client-local.model' export * from './result-list.model' -export * from './server/server-config.model' diff --git a/shared/models/moderation/abuse/abuse-create.model.ts b/shared/models/moderation/abuse/abuse-create.model.ts new file mode 100644 index 000000000..c0d04e46d --- /dev/null +++ b/shared/models/moderation/abuse/abuse-create.model.ts @@ -0,0 +1,26 @@ +import { AbusePredefinedReasonsString } from './abuse-reason.model' + +export interface AbuseCreate { + accountId: number + + reason: string + predefinedReasons?: AbusePredefinedReasonsString[] + + video?: { + id: number + startAt?: number + endAt?: number + } + + comment?: { + id: number + } +} + +// FIXME: deprecated in 2.3. Remove it +export interface VideoAbuseCreate { + reason: string + predefinedReasons?: AbusePredefinedReasonsString[] + startAt?: number + endAt?: number +} diff --git a/shared/models/moderation/abuse/abuse-filter.ts b/shared/models/moderation/abuse/abuse-filter.ts new file mode 100644 index 000000000..03303bbab --- /dev/null +++ b/shared/models/moderation/abuse/abuse-filter.ts @@ -0,0 +1 @@ +export type AbuseFilter = 'video' | 'comment' diff --git a/shared/models/moderation/abuse/abuse-reason.model.ts b/shared/models/moderation/abuse/abuse-reason.model.ts new file mode 100644 index 000000000..36875969d --- /dev/null +++ b/shared/models/moderation/abuse/abuse-reason.model.ts @@ -0,0 +1,33 @@ +export enum AbusePredefinedReasons { + VIOLENT_OR_REPULSIVE = 1, + HATEFUL_OR_ABUSIVE, + SPAM_OR_MISLEADING, + PRIVACY, + RIGHTS, + SERVER_RULES, + THUMBNAILS, + CAPTIONS +} + +export type AbusePredefinedReasonsString = + 'violentOrRepulsive' | + 'hatefulOrAbusive' | + 'spamOrMisleading' | + 'privacy' | + 'rights' | + 'serverRules' | + 'thumbnails' | + 'captions' + +export const abusePredefinedReasonsMap: { + [key in AbusePredefinedReasonsString]: AbusePredefinedReasons +} = { + violentOrRepulsive: AbusePredefinedReasons.VIOLENT_OR_REPULSIVE, + hatefulOrAbusive: AbusePredefinedReasons.HATEFUL_OR_ABUSIVE, + spamOrMisleading: AbusePredefinedReasons.SPAM_OR_MISLEADING, + privacy: AbusePredefinedReasons.PRIVACY, + rights: AbusePredefinedReasons.RIGHTS, + serverRules: AbusePredefinedReasons.SERVER_RULES, + thumbnails: AbusePredefinedReasons.THUMBNAILS, + captions: AbusePredefinedReasons.CAPTIONS +} diff --git a/shared/models/videos/abuse/video-abuse-state.model.ts b/shared/models/moderation/abuse/abuse-state.model.ts similarity index 61% rename from shared/models/videos/abuse/video-abuse-state.model.ts rename to shared/models/moderation/abuse/abuse-state.model.ts index 529f034bd..b00cccad8 100644 --- a/shared/models/videos/abuse/video-abuse-state.model.ts +++ b/shared/models/moderation/abuse/abuse-state.model.ts @@ -1,4 +1,4 @@ -export enum VideoAbuseState { +export enum AbuseState { PENDING = 1, REJECTED = 2, ACCEPTED = 3 diff --git a/shared/models/moderation/abuse/abuse-update.model.ts b/shared/models/moderation/abuse/abuse-update.model.ts new file mode 100644 index 000000000..4360fe7ac --- /dev/null +++ b/shared/models/moderation/abuse/abuse-update.model.ts @@ -0,0 +1,7 @@ +import { AbuseState } from './abuse-state.model' + +export interface AbuseUpdate { + moderationComment?: string + + state?: AbuseState +} diff --git a/shared/models/moderation/abuse/abuse-video-is.type.ts b/shared/models/moderation/abuse/abuse-video-is.type.ts new file mode 100644 index 000000000..74937f3b9 --- /dev/null +++ b/shared/models/moderation/abuse/abuse-video-is.type.ts @@ -0,0 +1 @@ +export type AbuseVideoIs = 'deleted' | 'blacklisted' diff --git a/shared/models/moderation/abuse/abuse.model.ts b/shared/models/moderation/abuse/abuse.model.ts new file mode 100644 index 000000000..9ff150c4a --- /dev/null +++ b/shared/models/moderation/abuse/abuse.model.ts @@ -0,0 +1,53 @@ +import { Account } from '../../actors/account.model' +import { AbuseState } from './abuse-state.model' +import { AbusePredefinedReasonsString } from './abuse-reason.model' +import { VideoConstant } from '../../videos/video-constant.model' +import { VideoChannel } from '../../videos/channel/video-channel.model' + +export interface VideoAbuse { + id: number + name: string + uuid: string + nsfw: boolean + deleted: boolean + blacklisted: boolean + + startAt: number | null + endAt: number | null + + thumbnailPath?: string + channel?: VideoChannel +} + +export interface VideoCommentAbuse { + id: number + account?: Account + text: string + deleted: boolean +} + +export interface Abuse { + id: number + reason: string + predefinedReasons?: AbusePredefinedReasonsString[] + reporterAccount: Account + + state: VideoConstant + moderationComment?: string + + video?: VideoAbuse + comment?: VideoCommentAbuse + + createdAt: Date + updatedAt: Date + + // FIXME: deprecated in 2.3, remove this + startAt: null + endAt: null + + count?: number + nth?: number + + countReportsForReporter?: number + countReportsForReportee?: number +} diff --git a/shared/models/moderation/abuse/index.ts b/shared/models/moderation/abuse/index.ts new file mode 100644 index 000000000..32a6b4e6c --- /dev/null +++ b/shared/models/moderation/abuse/index.ts @@ -0,0 +1,6 @@ +export * from './abuse-create.model' +export * from './abuse-reason.model' +export * from './abuse-state.model' +export * from './abuse-update.model' +export * from './abuse-video-is.type' +export * from './abuse.model' diff --git a/shared/models/blocklist/account-block.model.ts b/shared/models/moderation/account-block.model.ts similarity index 100% rename from shared/models/blocklist/account-block.model.ts rename to shared/models/moderation/account-block.model.ts diff --git a/shared/models/blocklist/index.ts b/shared/models/moderation/index.ts similarity index 75% rename from shared/models/blocklist/index.ts rename to shared/models/moderation/index.ts index fc7873270..8b6042e97 100644 --- a/shared/models/blocklist/index.ts +++ b/shared/models/moderation/index.ts @@ -1,2 +1,3 @@ +export * from './abuse' export * from './account-block.model' export * from './server-block.model' diff --git a/shared/models/blocklist/server-block.model.ts b/shared/models/moderation/server-block.model.ts similarity index 100% rename from shared/models/blocklist/server-block.model.ts rename to shared/models/moderation/server-block.model.ts diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts index e9be1ca7f..39090f5a1 100644 --- a/shared/models/users/user-notification.model.ts +++ b/shared/models/users/user-notification.model.ts @@ -64,9 +64,20 @@ export interface UserNotification { video: VideoInfo } - videoAbuse?: { + abuse?: { id: number - video: VideoInfo + + video?: VideoInfo + + comment?: { + threadId: number + + video: { + uuid: string + } + } + + account?: ActorInfo } videoBlacklist?: { diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts index 2f88a65de..4a7ae4373 100644 --- a/shared/models/users/user-right.enum.ts +++ b/shared/models/users/user-right.enum.ts @@ -11,7 +11,7 @@ export enum UserRight { MANAGE_SERVER_REDUNDANCY, - MANAGE_VIDEO_ABUSES, + MANAGE_ABUSES, MANAGE_JOBS, diff --git a/shared/models/users/user-role.ts b/shared/models/users/user-role.ts index 2b08b5850..772988c0c 100644 --- a/shared/models/users/user-role.ts +++ b/shared/models/users/user-role.ts @@ -20,7 +20,7 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = { [UserRole.MODERATOR]: [ UserRight.MANAGE_VIDEO_BLACKLIST, - UserRight.MANAGE_VIDEO_ABUSES, + UserRight.MANAGE_ABUSES, UserRight.REMOVE_ANY_VIDEO, UserRight.REMOVE_ANY_VIDEO_CHANNEL, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, diff --git a/shared/models/videos/abuse/index.ts b/shared/models/videos/abuse/index.ts deleted file mode 100644 index f70bc736f..000000000 --- a/shared/models/videos/abuse/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './video-abuse-create.model' -export * from './video-abuse-reason.model' -export * from './video-abuse-state.model' -export * from './video-abuse-update.model' -export * from './video-abuse-video-is.type' -export * from './video-abuse.model' diff --git a/shared/models/videos/abuse/video-abuse-create.model.ts b/shared/models/videos/abuse/video-abuse-create.model.ts deleted file mode 100644 index c93cb8b2c..000000000 --- a/shared/models/videos/abuse/video-abuse-create.model.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model' - -export interface VideoAbuseCreate { - reason: string - predefinedReasons?: VideoAbusePredefinedReasonsString[] - startAt?: number - endAt?: number -} diff --git a/shared/models/videos/abuse/video-abuse-reason.model.ts b/shared/models/videos/abuse/video-abuse-reason.model.ts deleted file mode 100644 index 9064f0c1a..000000000 --- a/shared/models/videos/abuse/video-abuse-reason.model.ts +++ /dev/null @@ -1,33 +0,0 @@ -export enum VideoAbusePredefinedReasons { - VIOLENT_OR_REPULSIVE = 1, - HATEFUL_OR_ABUSIVE, - SPAM_OR_MISLEADING, - PRIVACY, - RIGHTS, - SERVER_RULES, - THUMBNAILS, - CAPTIONS -} - -export type VideoAbusePredefinedReasonsString = - 'violentOrRepulsive' | - 'hatefulOrAbusive' | - 'spamOrMisleading' | - 'privacy' | - 'rights' | - 'serverRules' | - 'thumbnails' | - 'captions' - -export const videoAbusePredefinedReasonsMap: { - [key in VideoAbusePredefinedReasonsString]: VideoAbusePredefinedReasons -} = { - violentOrRepulsive: VideoAbusePredefinedReasons.VIOLENT_OR_REPULSIVE, - hatefulOrAbusive: VideoAbusePredefinedReasons.HATEFUL_OR_ABUSIVE, - spamOrMisleading: VideoAbusePredefinedReasons.SPAM_OR_MISLEADING, - privacy: VideoAbusePredefinedReasons.PRIVACY, - rights: VideoAbusePredefinedReasons.RIGHTS, - serverRules: VideoAbusePredefinedReasons.SERVER_RULES, - thumbnails: VideoAbusePredefinedReasons.THUMBNAILS, - captions: VideoAbusePredefinedReasons.CAPTIONS -} diff --git a/shared/models/videos/abuse/video-abuse-update.model.ts b/shared/models/videos/abuse/video-abuse-update.model.ts deleted file mode 100644 index 9b32aae48..000000000 --- a/shared/models/videos/abuse/video-abuse-update.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { VideoAbuseState } from './video-abuse-state.model' - -export interface VideoAbuseUpdate { - moderationComment?: string - state?: VideoAbuseState -} diff --git a/shared/models/videos/abuse/video-abuse-video-is.type.ts b/shared/models/videos/abuse/video-abuse-video-is.type.ts deleted file mode 100644 index e86018993..000000000 --- a/shared/models/videos/abuse/video-abuse-video-is.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type VideoAbuseVideoIs = 'deleted' | 'blacklisted' diff --git a/shared/models/videos/abuse/video-abuse.model.ts b/shared/models/videos/abuse/video-abuse.model.ts deleted file mode 100644 index 38605dcac..000000000 --- a/shared/models/videos/abuse/video-abuse.model.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Account } from '../../actors/index' -import { VideoConstant } from '../video-constant.model' -import { VideoAbuseState } from './video-abuse-state.model' -import { VideoChannel } from '../channel/video-channel.model' -import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model' - -export interface VideoAbuse { - id: number - reason: string - predefinedReasons?: VideoAbusePredefinedReasonsString[] - reporterAccount: Account - - state: VideoConstant - moderationComment?: string - - video: { - id: number - name: string - uuid: string - nsfw: boolean - deleted: boolean - blacklisted: boolean - thumbnailPath?: string - channel?: VideoChannel - } - - createdAt: Date - updatedAt: Date - - startAt: number - endAt: number - - count?: number - nth?: number - - countReportsForReporter?: number - countReportsForReportee?: number -} diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index e1d96b40a..20b9638ab 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -1,4 +1,3 @@ -export * from './abuse' export * from './blacklist' export * from './caption' export * from './channel' -- 2.41.0