diff options
162 files changed, 5663 insertions, 2460 deletions
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b12e97361..704b22b8b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md | |||
@@ -4,12 +4,26 @@ Interested in contributing? Awesome! | |||
4 | 4 | ||
5 | **This guide will present you the following contribution topics:** | 5 | **This guide will present you the following contribution topics:** |
6 | 6 | ||
7 | * [Translate](#translate) | 7 | <!-- START doctoc generated TOC please keep comment here to allow auto update --> |
8 | * [Give your feedback](#give-your-feedback) | 8 | <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> |
9 | * [Write documentation](#write-documentation) | 9 | |
10 | * [Improve the website](#improve-the-website) | 10 | |
11 | * [Develop](#develop) | 11 | - [Translate](#translate) |
12 | * [Write a plugin or a theme](#plugins--themes) | 12 | - [Give your feedback](#give-your-feedback) |
13 | - [Write documentation](#write-documentation) | ||
14 | - [Improve the website](#improve-the-website) | ||
15 | - [Develop](#develop) | ||
16 | - [Prerequisites](#prerequisites) | ||
17 | - [Online development](#online-development) | ||
18 | - [Server side](#server-side) | ||
19 | - [Client side](#client-side) | ||
20 | - [Client and server side](#client-and-server-side) | ||
21 | - [Testing the federation of PeerTube servers](#testing-the-federation-of-peertube-servers) | ||
22 | - [Unit tests](#unit-tests) | ||
23 | - [Emails](#emails) | ||
24 | - [Plugins & Themes](#plugins--themes) | ||
25 | |||
26 | <!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
13 | 27 | ||
14 | ## Translate | 28 | ## Translate |
15 | 29 | ||
@@ -30,7 +44,7 @@ You can help to write the documentation of the REST API, code, architecture, | |||
30 | demonstrations. | 44 | demonstrations. |
31 | 45 | ||
32 | For the REST API you can see the documentation in [/support/doc/api](https://github.com/Chocobozzz/PeerTube/tree/develop/support/doc/api) directory. | 46 | For the REST API you can see the documentation in [/support/doc/api](https://github.com/Chocobozzz/PeerTube/tree/develop/support/doc/api) directory. |
33 | Then, you can just open the `openapi.yaml` file in a special editor like [http://editor.swagger.io/](http://editor.swagger.io/) to easily see and edit the documentation. | 47 | Then, you can just open the `openapi.yaml` file in a special editor like [http://editor.swagger.io/](http://editor.swagger.io/) to easily see and edit the documentation. You can also use [redoc-cli](https://github.com/Redocly/redoc/blob/master/cli/README.md) and run `redoc-cli serve --watch support/doc/api/openapi.yaml` to see the final result. |
34 | 48 | ||
35 | Some hints: | 49 | Some hints: |
36 | * Routes are defined in [/server/controllers/](https://github.com/Chocobozzz/PeerTube/tree/develop/server/controllers) directory | 50 | * Routes are defined in [/server/controllers/](https://github.com/Chocobozzz/PeerTube/tree/develop/server/controllers) directory |
@@ -201,6 +215,13 @@ $ npm run mocha -- --exit -r ts-node/register -r tsconfig-paths/register --bail | |||
201 | Instance configurations are in `config/test-{1,2,3,4,5,6}.yaml`. | 215 | Instance configurations are in `config/test-{1,2,3,4,5,6}.yaml`. |
202 | Note that only instance 2 has transcoding enabled. | 216 | Note that only instance 2 has transcoding enabled. |
203 | 217 | ||
218 | ### Emails | ||
219 | |||
220 | To test emails with PeerTube: | ||
221 | |||
222 | * Run [mailslurper](http://mailslurper.com/) | ||
223 | * Run PeerTube using mailslurper SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=test npm start` | ||
224 | |||
204 | ## Plugins & Themes | 225 | ## Plugins & Themes |
205 | 226 | ||
206 | See the dedicated documentation: https://docs.joinpeertube.org/#/contribute-plugins | 227 | See the dedicated documentation: https://docs.joinpeertube.org/#/contribute-plugins |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index af80337ce..31c8e3a8e 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -22,6 +22,7 @@ | |||
22 | <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> | 22 | <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> |
23 | 23 | ||
24 | <my-user-moderation-dropdown | 24 | <my-user-moderation-dropdown |
25 | [prependActions]="prependModerationActions" | ||
25 | buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto" | 26 | buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto" |
26 | (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" | 27 | (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" |
27 | ></my-user-moderation-dropdown> | 28 | ></my-user-moderation-dropdown> |
@@ -50,3 +51,7 @@ | |||
50 | <router-outlet></router-outlet> | 51 | <router-outlet></router-outlet> |
51 | </div> | 52 | </div> |
52 | </div> | 53 | </div> |
54 | |||
55 | <ng-container *ngIf="prependModerationActions"> | ||
56 | <my-account-report #accountReportModal [account]="account"></my-account-report> | ||
57 | </ng-container> | ||
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index 01911cac2..9288fcb42 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { Subscription } from 'rxjs' | 1 | import { Subscription } from 'rxjs' |
2 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' | 2 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute } from '@angular/router' | 4 | import { ActivatedRoute } from '@angular/router' |
5 | import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' | 5 | import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' |
6 | import { Account, AccountService, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 6 | import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
7 | import { AccountReportComponent } from '@app/shared/shared-moderation' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { User, UserRight } from '@shared/models' | 9 | import { User, UserRight } from '@shared/models' |
9 | 10 | ||
@@ -12,6 +13,8 @@ import { User, UserRight } from '@shared/models' | |||
12 | styleUrls: [ './accounts.component.scss' ] | 13 | styleUrls: [ './accounts.component.scss' ] |
13 | }) | 14 | }) |
14 | export class AccountsComponent implements OnInit, OnDestroy { | 15 | export class AccountsComponent implements OnInit, OnDestroy { |
16 | @ViewChild('accountReportModal') accountReportModal: AccountReportComponent | ||
17 | |||
15 | account: Account | 18 | account: Account |
16 | accountUser: User | 19 | accountUser: User |
17 | videoChannels: VideoChannel[] = [] | 20 | videoChannels: VideoChannel[] = [] |
@@ -20,6 +23,8 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
20 | isAccountManageable = false | 23 | isAccountManageable = false |
21 | accountFollowerTitle = '' | 24 | accountFollowerTitle = '' |
22 | 25 | ||
26 | prependModerationActions: DropdownAction<any>[] | ||
27 | |||
23 | private routeSub: Subscription | 28 | private routeSub: Subscription |
24 | 29 | ||
25 | constructor ( | 30 | constructor ( |
@@ -42,24 +47,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
42 | map(params => params[ 'accountId' ]), | 47 | map(params => params[ 'accountId' ]), |
43 | distinctUntilChanged(), | 48 | distinctUntilChanged(), |
44 | switchMap(accountId => this.accountService.getAccount(accountId)), | 49 | switchMap(accountId => this.accountService.getAccount(accountId)), |
45 | tap(account => { | 50 | tap(account => this.onAccount(account)), |
46 | this.account = account | ||
47 | |||
48 | if (this.authService.isLoggedIn()) { | ||
49 | this.authService.userInformationLoaded.subscribe( | ||
50 | () => { | ||
51 | this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id | ||
52 | |||
53 | this.accountFollowerTitle = this.i18n( | ||
54 | '{{followers}} direct account followers', | ||
55 | { followers: this.subscribersDisplayFor(account.followersCount) } | ||
56 | ) | ||
57 | } | ||
58 | ) | ||
59 | } | ||
60 | |||
61 | this.getUserIfNeeded(account) | ||
62 | }), | ||
63 | switchMap(account => this.videoChannelService.listAccountVideoChannels(account)), | 51 | switchMap(account => this.videoChannelService.listAccountVideoChannels(account)), |
64 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) | 52 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) |
65 | ) | 53 | ) |
@@ -107,6 +95,41 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
107 | return this.i18n('{count, plural, =1 {1 subscriber} other {{{count}} subscribers}}', { count }) | 95 | return this.i18n('{count, plural, =1 {1 subscriber} other {{{count}} subscribers}}', { count }) |
108 | } | 96 | } |
109 | 97 | ||
98 | private onAccount (account: Account) { | ||
99 | this.prependModerationActions = undefined | ||
100 | |||
101 | this.account = account | ||
102 | |||
103 | if (this.authService.isLoggedIn()) { | ||
104 | this.authService.userInformationLoaded.subscribe( | ||
105 | () => { | ||
106 | this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id | ||
107 | |||
108 | this.accountFollowerTitle = this.i18n( | ||
109 | '{{followers}} direct account followers', | ||
110 | { followers: this.subscribersDisplayFor(account.followersCount) } | ||
111 | ) | ||
112 | |||
113 | // It's not our account, we can report it | ||
114 | if (!this.isAccountManageable) { | ||
115 | this.prependModerationActions = [ | ||
116 | { | ||
117 | label: this.i18n('Report account'), | ||
118 | handler: () => this.showReportModal() | ||
119 | } | ||
120 | ] | ||
121 | } | ||
122 | } | ||
123 | ) | ||
124 | } | ||
125 | |||
126 | this.getUserIfNeeded(account) | ||
127 | } | ||
128 | |||
129 | private showReportModal () { | ||
130 | this.accountReportModal.show() | ||
131 | } | ||
132 | |||
110 | private getUserIfNeeded (account: Account) { | 133 | private getUserIfNeeded (account: Account) { |
111 | if (!account.userId || !this.authService.isLoggedIn()) return | 134 | if (!account.userId || !this.authService.isLoggedIn()) return |
112 | 135 | ||
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 6f340884f..4345d1945 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts | |||
@@ -45,10 +45,10 @@ export class AdminComponent implements OnInit { | |||
45 | children: [] | 45 | children: [] |
46 | } | 46 | } |
47 | 47 | ||
48 | if (this.hasVideoAbusesRight()) { | 48 | if (this.hasAbusesRight()) { |
49 | moderationItems.children.push({ | 49 | moderationItems.children.push({ |
50 | label: this.i18n('Video reports'), | 50 | label: this.i18n('Reports'), |
51 | routerLink: '/admin/moderation/video-abuses/list', | 51 | routerLink: '/admin/moderation/abuses/list', |
52 | iconName: 'flag' | 52 | iconName: 'flag' |
53 | }) | 53 | }) |
54 | } | 54 | } |
@@ -76,7 +76,7 @@ export class AdminComponent implements OnInit { | |||
76 | 76 | ||
77 | if (this.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' }) | 77 | if (this.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' }) |
78 | if (this.hasServerFollowRight()) this.menuEntries.push(federationItems) | 78 | if (this.hasServerFollowRight()) this.menuEntries.push(federationItems) |
79 | if (this.hasVideoAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems) | 79 | if (this.hasAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems) |
80 | if (this.hasConfigRight()) this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' }) | 80 | if (this.hasConfigRight()) this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' }) |
81 | if (this.hasPluginsRight()) this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' }) | 81 | if (this.hasPluginsRight()) this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' }) |
82 | if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.menuEntries.push({ label: this.i18n('System'), routerLink: '/admin/system' }) | 82 | if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.menuEntries.push({ label: this.i18n('System'), routerLink: '/admin/system' }) |
@@ -90,8 +90,8 @@ export class AdminComponent implements OnInit { | |||
90 | return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW) | 90 | return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW) |
91 | } | 91 | } |
92 | 92 | ||
93 | hasVideoAbusesRight () { | 93 | hasAbusesRight () { |
94 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) | 94 | return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES) |
95 | } | 95 | } |
96 | 96 | ||
97 | hasVideoBlocklistRight () { | 97 | 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 | |||
14 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 14 | import { FollowingListComponent } from './follows/following-list/following-list.component' |
15 | import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' | 15 | import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' |
16 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' | 16 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' |
17 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlockListComponent } from './moderation' | 17 | import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation' |
18 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' | 18 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' |
19 | import { ModerationComponent } from './moderation/moderation.component' | 19 | import { ModerationComponent } from './moderation/moderation.component' |
20 | import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component' | 20 | import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component' |
21 | import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' | 21 | import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' |
22 | import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' | 22 | import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' |
23 | import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' | 23 | import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' |
@@ -60,8 +60,10 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom | |||
60 | 60 | ||
61 | ModerationComponent, | 61 | ModerationComponent, |
62 | VideoBlockListComponent, | 62 | VideoBlockListComponent, |
63 | VideoAbuseListComponent, | 63 | |
64 | VideoAbuseDetailsComponent, | 64 | AbuseListComponent, |
65 | AbuseDetailsComponent, | ||
66 | |||
65 | ModerationCommentModalComponent, | 67 | ModerationCommentModalComponent, |
66 | InstanceServerBlocklistComponent, | 68 | InstanceServerBlocklistComponent, |
67 | InstanceAccountBlocklistComponent, | 69 | 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..cba9cfb73 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html | |||
@@ -0,0 +1,115 @@ | |||
1 | <div class="d-flex moderation-expanded"> | ||
2 | <!-- report left part (report details) --> | ||
3 | <div class="col-8"> | ||
4 | |||
5 | <!-- report metadata --> | ||
6 | <div class="d-flex" *ngIf="abuse.reporterAccount"> | ||
7 | <span class="col-3 moderation-expanded-label" i18n>Reporter</span> | ||
8 | |||
9 | <span class="col-9 moderation-expanded-text"> | ||
10 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" | ||
11 | class="chip" | ||
12 | > | ||
13 | <img | ||
14 | class="avatar" | ||
15 | [src]="abuse.reporterAccount.avatar?.path" | ||
16 | (error)="switchToDefaultAvatar($event)" | ||
17 | alt="Avatar" | ||
18 | > | ||
19 | <div> | ||
20 | <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span> | ||
21 | </div> | ||
22 | </a> | ||
23 | |||
24 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" | ||
25 | class="ml-auto text-muted abuse-details-links" i18n | ||
26 | > | ||
27 | {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span> | ||
28 | </a> | ||
29 | </span> | ||
30 | </div> | ||
31 | |||
32 | <div class="d-flex" *ngIf="abuse.flaggedAccount"> | ||
33 | <span class="col-3 moderation-expanded-label" i18n>Reportee</span> | ||
34 | <span class="col-9 moderation-expanded-text"> | ||
35 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" | ||
36 | class="chip" | ||
37 | > | ||
38 | <img | ||
39 | class="avatar" | ||
40 | [src]="abuse.flaggedAccount?.avatar?.path" | ||
41 | (error)="switchToDefaultAvatar($event)" | ||
42 | alt="Avatar" | ||
43 | > | ||
44 | <div> | ||
45 | <span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span> | ||
46 | </div> | ||
47 | </a> | ||
48 | |||
49 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" | ||
50 | class="ml-auto text-muted abuse-details-links" i18n | ||
51 | > | ||
52 | {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span> | ||
53 | </a> | ||
54 | </span> | ||
55 | </div> | ||
56 | |||
57 | <div class="d-flex" *ngIf="abuse.updatedAt"> | ||
58 | <span class="col-3 moderation-expanded-label" i18n>Updated</span> | ||
59 | <time class="col-9 moderation-expanded-text abuse-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time> | ||
60 | </div> | ||
61 | |||
62 | <!-- report text --> | ||
63 | <div class="mt-3 d-flex"> | ||
64 | <span class="col-3 moderation-expanded-label"> | ||
65 | <ng-container i18n>Report</ng-container> | ||
66 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a> | ||
67 | </span> | ||
68 | <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span> | ||
69 | </div> | ||
70 | |||
71 | <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> | ||
72 | <span class="col-3"></span> | ||
73 | <span class="col-9"> | ||
74 | <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '/admin/moderation/abuses/list' ]" | ||
75 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" | ||
76 | > | ||
77 | <div>{{ reason.label }}</div> | ||
78 | </a> | ||
79 | </span> | ||
80 | </div> | ||
81 | |||
82 | <div *ngIf="abuse.video?.startAt" class="mt-2 d-flex"> | ||
83 | <span class="col-3 moderation-expanded-label" i18n>Reported part</span> | ||
84 | <span class="col-9"> | ||
85 | {{ startAt }}<ng-container *ngIf="abuse.video.endAt"> - {{ endAt }}</ng-container> | ||
86 | </span> | ||
87 | </div> | ||
88 | |||
89 | <div class="mt-3 d-flex" *ngIf="abuse.moderationComment"> | ||
90 | <span class="col-3 moderation-expanded-label" i18n>Note</span> | ||
91 | <span class="col-9 moderation-expanded-text d-block" [innerHTML]="abuse.moderationCommentHtml"></span> | ||
92 | </div> | ||
93 | |||
94 | </div> | ||
95 | |||
96 | <!-- report right part (video/comment details) --> | ||
97 | <div class="col-4"> | ||
98 | <div *ngIf="abuse.video" class="screenratio"> | ||
99 | <div> | ||
100 | <span i18n *ngIf="abuse.video.deleted">The video was deleted</span> | ||
101 | <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span> | ||
102 | </div> | ||
103 | |||
104 | <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div> | ||
105 | </div> | ||
106 | |||
107 | <div *ngIf="abuse.comment" class="comment-html"> | ||
108 | <div> | ||
109 | <strong i18n>Comment:</strong> | ||
110 | </div> | ||
111 | |||
112 | <div [innerHTML]="abuse.commentHtml"></div> | ||
113 | </div> | ||
114 | </div> | ||
115 | </div> | ||
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 index 5db2887fa..fb0f65764 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 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { Actor } from '@app/shared/shared-main' | 2 | import { Actor } from '@app/shared/shared-main' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model' | 4 | import { AbusePredefinedReasonsString } from '@shared/models' |
5 | import { ProcessedVideoAbuse } from './video-abuse-list.component' | 5 | import { ProcessedAbuse } from './abuse-list.component' |
6 | import { durationToString } from '@app/helpers' | 6 | import { durationToString } from '@app/helpers' |
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
9 | selector: 'my-video-abuse-details', | 9 | selector: 'my-abuse-details', |
10 | templateUrl: './video-abuse-details.component.html', | 10 | templateUrl: './abuse-details.component.html', |
11 | styleUrls: [ '../moderation.component.scss' ] | 11 | styleUrls: [ '../moderation.component.scss' ] |
12 | }) | 12 | }) |
13 | export class VideoAbuseDetailsComponent { | 13 | export class AbuseDetailsComponent { |
14 | @Input() videoAbuse: ProcessedVideoAbuse | 14 | @Input() abuse: ProcessedAbuse |
15 | 15 | ||
16 | private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string } | 16 | private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } |
17 | 17 | ||
18 | constructor ( | 18 | constructor ( |
19 | private i18n: I18n | 19 | private i18n: I18n |
@@ -31,16 +31,17 @@ export class VideoAbuseDetailsComponent { | |||
31 | } | 31 | } |
32 | 32 | ||
33 | get startAt () { | 33 | get startAt () { |
34 | return durationToString(this.videoAbuse.startAt) | 34 | return durationToString(this.abuse.video.startAt) |
35 | } | 35 | } |
36 | 36 | ||
37 | get endAt () { | 37 | get endAt () { |
38 | return durationToString(this.videoAbuse.endAt) | 38 | return durationToString(this.abuse.video.endAt) |
39 | } | 39 | } |
40 | 40 | ||
41 | getPredefinedReasons () { | 41 | getPredefinedReasons () { |
42 | if (!this.videoAbuse.predefinedReasons) return [] | 42 | if (!this.abuse.predefinedReasons) return [] |
43 | return this.videoAbuse.predefinedReasons.map(r => ({ | 43 | |
44 | return this.abuse.predefinedReasons.map(r => ({ | ||
44 | id: r, | 45 | id: r, |
45 | label: this.predefinedReasonsTranslations[r] | 46 | label: this.predefinedReasonsTranslations[r] |
46 | })) | 47 | })) |
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html new file mode 100644 index 000000000..99502304d --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html | |||
@@ -0,0 +1,184 @@ | |||
1 | <p-table | ||
2 | [value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" | ||
3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true" [lazyLoadOnInit]="false" | ||
4 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
5 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports" | ||
6 | (onPage)="onPage($event)" [expandedRowKeys]="expandedRows" | ||
7 | > | ||
8 | <ng-template pTemplate="caption"> | ||
9 | <div class="caption"> | ||
10 | <div class="ml-auto"> | ||
11 | <div class="input-group has-feedback has-clear"> | ||
12 | <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> | ||
13 | <div class="input-group-text" ngbDropdownToggle> | ||
14 | <span class="caret" aria-haspopup="menu" role="button"></span> | ||
15 | </div> | ||
16 | |||
17 | <div role="menu" ngbDropdownMenu> | ||
18 | <h6 class="dropdown-header" i18n>Advanced report filters</h6> | ||
19 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a> | ||
20 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a> | ||
21 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a> | ||
22 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a> | ||
23 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a> | ||
24 | </div> | ||
25 | </div> | ||
26 | <input | ||
27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
28 | (keyup)="onAbuseSearch($event)" | ||
29 | > | ||
30 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | ||
31 | <span class="sr-only" i18n>Clear filters</span> | ||
32 | </div> | ||
33 | </div> | ||
34 | </div> | ||
35 | </ng-template> | ||
36 | |||
37 | <ng-template pTemplate="header"> | ||
38 | <tr> <!-- header --> | ||
39 | <th style="width: 40px;"></th> | ||
40 | <th style="width: 20%;" pResizableColumn i18n>Reporter</th> | ||
41 | <th i18n>Video/Comment/Account</th> | ||
42 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
43 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> | ||
44 | <th style="width: 150px;"></th> | ||
45 | </tr> | ||
46 | </ng-template> | ||
47 | |||
48 | <ng-template pTemplate="body" let-expanded="expanded" let-abuse> | ||
49 | <tr> | ||
50 | <td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> | ||
51 | <span class="expander"> | ||
52 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | ||
53 | </span> | ||
54 | </td> | ||
55 | |||
56 | <td> | ||
57 | <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | ||
58 | <div class="chip two-lines"> | ||
59 | <img | ||
60 | class="avatar" | ||
61 | [src]="abuse.reporterAccount.avatar?.path" | ||
62 | (error)="switchToDefaultAvatar($event)" | ||
63 | alt="Avatar" | ||
64 | > | ||
65 | <div> | ||
66 | {{ abuse.reporterAccount.displayName }} | ||
67 | <span>{{ abuse.reporterAccount.nameWithHost }}</span> | ||
68 | </div> | ||
69 | </div> | ||
70 | </a> | ||
71 | |||
72 | <span i18n *ngIf="!abuse.reporterAccount"> | ||
73 | Deleted account | ||
74 | </span> | ||
75 | </td> | ||
76 | |||
77 | <ng-container *ngIf="abuse.video"> | ||
78 | |||
79 | <td *ngIf="!abuse.video.deleted"> | ||
80 | <a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer"> | ||
81 | <div class="table-video"> | ||
82 | <div class="table-video-image"> | ||
83 | <img [src]="abuse.video.thumbnailPath"> | ||
84 | <span | ||
85 | class="table-video-image-label" *ngIf="abuse.count > 1" | ||
86 | i18n-title title="This video has been reported multiple times." | ||
87 | > | ||
88 | {{ abuse.nth }}/{{ abuse.count }} | ||
89 | </span> | ||
90 | </div> | ||
91 | |||
92 | <div class="table-video-text"> | ||
93 | <div> | ||
94 | <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span> | ||
95 | <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span> | ||
96 | {{ abuse.video.name }} | ||
97 | </div> | ||
98 | <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div> | ||
99 | </div> | ||
100 | </div> | ||
101 | </a> | ||
102 | </td> | ||
103 | |||
104 | <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse"> | ||
105 | <div class="table-video" i18n-title title="Video was deleted"> | ||
106 | <div class="table-video-image"> | ||
107 | <span i18n>Deleted</span> | ||
108 | </div> | ||
109 | |||
110 | <div class="table-video-text"> | ||
111 | <div> | ||
112 | {{ abuse.video.name }} | ||
113 | <span class="glyphicon glyphicon-trash"></span> | ||
114 | </div> | ||
115 | <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div> | ||
116 | </div> | ||
117 | </div> | ||
118 | </td> | ||
119 | </ng-container> | ||
120 | |||
121 | <ng-container *ngIf="abuse.comment"> | ||
122 | <td> | ||
123 | <a [href]="getCommentUrl(abuse)" [innerHTML]="abuse.truncatedCommentHtml" class="table-comment-link" | ||
124 | [title]="abuse.comment.video.name" target="_blank" rel="noopener noreferrer" | ||
125 | ></a> | ||
126 | |||
127 | <div class="comment-flagged-account" *ngIf="abuse.flaggedAccount">by {{ abuse.flaggedAccount.displayName }}</div> | ||
128 | </td> | ||
129 | </ng-container> | ||
130 | |||
131 | <ng-container *ngIf="!abuse.comment && !abuse.video"> | ||
132 | <td *ngIf="abuse.flaggedAccount"> | ||
133 | <a [href]="getAccountUrl(abuse)" class="table-account-link" target="_blank" rel="noopener noreferrer"> | ||
134 | <span>{{ abuse.flaggedAccount.displayName }}</span> | ||
135 | |||
136 | <span class="account-flagged-handle">{{ abuse.flaggedAccount.nameWithHostForced }}</span> | ||
137 | </a> | ||
138 | </td> | ||
139 | |||
140 | <td i18n *ngIf="!abuse.flaggedAccount"> | ||
141 | Account deleted | ||
142 | </td> | ||
143 | |||
144 | </ng-container> | ||
145 | |||
146 | |||
147 | <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> | ||
148 | |||
149 | <td class="c-hand abuse-states" [pRowToggler]="abuse"> | ||
150 | <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span> | ||
151 | <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span> | ||
152 | <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span> | ||
153 | </td> | ||
154 | |||
155 | <td class="action-cell"> | ||
156 | <my-action-dropdown | ||
157 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" | ||
158 | i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse" | ||
159 | ></my-action-dropdown> | ||
160 | </td> | ||
161 | </tr> | ||
162 | </ng-template> | ||
163 | |||
164 | <ng-template pTemplate="rowexpansion" let-abuse> | ||
165 | <tr> | ||
166 | <td class="expand-cell" colspan="6"> | ||
167 | <my-abuse-details [abuse]="abuse"></my-abuse-details> | ||
168 | </td> | ||
169 | </tr> | ||
170 | </ng-template> | ||
171 | |||
172 | <ng-template pTemplate="emptymessage"> | ||
173 | <tr> | ||
174 | <td colspan="6"> | ||
175 | <div class="no-results"> | ||
176 | <ng-container *ngIf="search" i18n>No abuses found matching current filters.</ng-container> | ||
177 | <ng-container *ngIf="!search" i18n>No abuses found.</ng-container> | ||
178 | </div> | ||
179 | </td> | ||
180 | </tr> | ||
181 | </ng-template> | ||
182 | </p-table> | ||
183 | |||
184 | <my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal> | ||
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 index 8eee15b64..c22f98c47 100644 --- 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 | |||
@@ -10,7 +10,7 @@ | |||
10 | @include disable-default-a-behaviour; | 10 | @include disable-default-a-behaviour; |
11 | } | 11 | } |
12 | 12 | ||
13 | .video-abuse-states .glyphicon-comment { | 13 | .abuse-states .glyphicon-comment { |
14 | margin-left: 0.5rem; | 14 | margin-left: 0.5rem; |
15 | } | 15 | } |
16 | 16 | ||
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts new file mode 100644 index 000000000..74c5fe2b3 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts | |||
@@ -0,0 +1,454 @@ | |||
1 | import * as debug from 'debug' | ||
2 | import truncate from 'lodash-es/truncate' | ||
3 | import { SortMeta } from 'primeng/api' | ||
4 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' | ||
5 | import { environment } from 'src/environments/environment' | ||
6 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' | ||
7 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | ||
8 | import { ActivatedRoute, Params, Router } from '@angular/router' | ||
9 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' | ||
10 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' | ||
11 | import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' | ||
12 | import { VideoCommentService } from '@app/shared/shared-video-comment' | ||
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
14 | import { Abuse, AbuseState } from '@shared/models' | ||
15 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' | ||
16 | |||
17 | const logger = debug('peertube:moderation:AbuseListComponent') | ||
18 | |||
19 | // Don't use an abuse model because we need external services to compute some properties | ||
20 | // And this model is only used in this component | ||
21 | export type ProcessedAbuse = Abuse & { | ||
22 | moderationCommentHtml?: string, | ||
23 | reasonHtml?: string | ||
24 | embedHtml?: SafeHtml | ||
25 | updatedAt?: Date | ||
26 | |||
27 | // override bare server-side definitions with rich client-side definitions | ||
28 | reporterAccount?: Account | ||
29 | flaggedAccount?: Account | ||
30 | |||
31 | truncatedCommentHtml?: string | ||
32 | commentHtml?: string | ||
33 | |||
34 | video: Abuse['video'] & { | ||
35 | channel: Abuse['video']['channel'] & { | ||
36 | ownerAccount: Account | ||
37 | } | ||
38 | } | ||
39 | } | ||
40 | |||
41 | @Component({ | ||
42 | selector: 'my-abuse-list', | ||
43 | templateUrl: './abuse-list.component.html', | ||
44 | styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ] | ||
45 | }) | ||
46 | export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { | ||
47 | @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent | ||
48 | |||
49 | abuses: ProcessedAbuse[] = [] | ||
50 | totalRecords = 0 | ||
51 | sort: SortMeta = { field: 'createdAt', order: 1 } | ||
52 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
53 | |||
54 | abuseActions: DropdownAction<ProcessedAbuse>[][] = [] | ||
55 | |||
56 | constructor ( | ||
57 | private notifier: Notifier, | ||
58 | private abuseService: AbuseService, | ||
59 | private blocklistService: BlocklistService, | ||
60 | private commentService: VideoCommentService, | ||
61 | private videoService: VideoService, | ||
62 | private videoBlocklistService: VideoBlockService, | ||
63 | private confirmService: ConfirmService, | ||
64 | private i18n: I18n, | ||
65 | private markdownRenderer: MarkdownService, | ||
66 | private sanitizer: DomSanitizer, | ||
67 | private route: ActivatedRoute, | ||
68 | private router: Router | ||
69 | ) { | ||
70 | super() | ||
71 | |||
72 | this.abuseActions = [ | ||
73 | this.buildInternalActions(), | ||
74 | |||
75 | this.buildFlaggedAccountActions(), | ||
76 | |||
77 | this.buildCommentActions(), | ||
78 | |||
79 | this.buildVideoActions(), | ||
80 | |||
81 | this.buildAccountActions() | ||
82 | ] | ||
83 | } | ||
84 | |||
85 | ngOnInit () { | ||
86 | this.initialize() | ||
87 | |||
88 | this.route.queryParams | ||
89 | .subscribe(params => { | ||
90 | this.search = params.search || '' | ||
91 | |||
92 | logger('On URL change (search: %s).', this.search) | ||
93 | |||
94 | this.setTableFilter(this.search) | ||
95 | this.loadData() | ||
96 | }) | ||
97 | } | ||
98 | |||
99 | ngAfterViewInit () { | ||
100 | if (this.search) this.setTableFilter(this.search) | ||
101 | } | ||
102 | |||
103 | getIdentifier () { | ||
104 | return 'AbuseListComponent' | ||
105 | } | ||
106 | |||
107 | openModerationCommentModal (abuse: Abuse) { | ||
108 | this.moderationCommentModal.openModal(abuse) | ||
109 | } | ||
110 | |||
111 | onModerationCommentUpdated () { | ||
112 | this.loadData() | ||
113 | } | ||
114 | |||
115 | /* Table filter functions */ | ||
116 | onAbuseSearch (event: Event) { | ||
117 | this.onSearch(event) | ||
118 | this.setQueryParams((event.target as HTMLInputElement).value) | ||
119 | } | ||
120 | |||
121 | setQueryParams (search: string) { | ||
122 | const queryParams: Params = {} | ||
123 | if (search) Object.assign(queryParams, { search }) | ||
124 | |||
125 | this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams }) | ||
126 | } | ||
127 | |||
128 | resetTableFilter () { | ||
129 | this.setTableFilter('') | ||
130 | this.setQueryParams('') | ||
131 | this.resetSearch() | ||
132 | } | ||
133 | /* END Table filter functions */ | ||
134 | |||
135 | isAbuseAccepted (abuse: Abuse) { | ||
136 | return abuse.state.id === AbuseState.ACCEPTED | ||
137 | } | ||
138 | |||
139 | isAbuseRejected (abuse: Abuse) { | ||
140 | return abuse.state.id === AbuseState.REJECTED | ||
141 | } | ||
142 | |||
143 | getVideoUrl (abuse: Abuse) { | ||
144 | return Video.buildClientUrl(abuse.video.uuid) | ||
145 | } | ||
146 | |||
147 | getCommentUrl (abuse: Abuse) { | ||
148 | return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId | ||
149 | } | ||
150 | |||
151 | getAccountUrl (abuse: ProcessedAbuse) { | ||
152 | return '/accounts/' + abuse.flaggedAccount.nameWithHost | ||
153 | } | ||
154 | |||
155 | getVideoEmbed (abuse: Abuse) { | ||
156 | return buildVideoEmbed( | ||
157 | buildVideoLink({ | ||
158 | baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, | ||
159 | title: false, | ||
160 | warningTitle: false, | ||
161 | startTime: abuse.startAt, | ||
162 | stopTime: abuse.endAt | ||
163 | }) | ||
164 | ) | ||
165 | } | ||
166 | |||
167 | switchToDefaultAvatar ($event: Event) { | ||
168 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | ||
169 | } | ||
170 | |||
171 | async removeAbuse (abuse: Abuse) { | ||
172 | const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) | ||
173 | if (res === false) return | ||
174 | |||
175 | this.abuseService.removeAbuse(abuse).subscribe( | ||
176 | () => { | ||
177 | this.notifier.success(this.i18n('Abuse deleted.')) | ||
178 | this.loadData() | ||
179 | }, | ||
180 | |||
181 | err => this.notifier.error(err.message) | ||
182 | ) | ||
183 | } | ||
184 | |||
185 | updateAbuseState (abuse: Abuse, state: AbuseState) { | ||
186 | this.abuseService.updateAbuse(abuse, { state }) | ||
187 | .subscribe( | ||
188 | () => this.loadData(), | ||
189 | |||
190 | err => this.notifier.error(err.message) | ||
191 | ) | ||
192 | } | ||
193 | |||
194 | protected loadData () { | ||
195 | logger('Load data.') | ||
196 | |||
197 | return this.abuseService.getAbuses({ | ||
198 | pagination: this.pagination, | ||
199 | sort: this.sort, | ||
200 | search: this.search | ||
201 | }).subscribe( | ||
202 | async resultList => { | ||
203 | this.totalRecords = resultList.total | ||
204 | |||
205 | this.abuses = [] | ||
206 | |||
207 | for (const a of resultList.data) { | ||
208 | const abuse = a as ProcessedAbuse | ||
209 | |||
210 | abuse.reasonHtml = await this.toHtml(abuse.reason) | ||
211 | abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment) | ||
212 | |||
213 | if (abuse.video) { | ||
214 | abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)) | ||
215 | |||
216 | if (abuse.video.channel?.ownerAccount) { | ||
217 | abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) | ||
218 | } | ||
219 | } | ||
220 | |||
221 | if (abuse.comment) { | ||
222 | if (abuse.comment.deleted) { | ||
223 | abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment') | ||
224 | } else { | ||
225 | const truncated = truncate(abuse.comment.text, { length: 100 }) | ||
226 | abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true) | ||
227 | abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true) | ||
228 | } | ||
229 | } | ||
230 | |||
231 | if (abuse.reporterAccount) { | ||
232 | abuse.reporterAccount = new Account(abuse.reporterAccount) | ||
233 | } | ||
234 | |||
235 | if (abuse.flaggedAccount) { | ||
236 | abuse.flaggedAccount = new Account(abuse.flaggedAccount) | ||
237 | } | ||
238 | |||
239 | if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt | ||
240 | |||
241 | this.abuses.push(abuse) | ||
242 | } | ||
243 | }, | ||
244 | |||
245 | err => this.notifier.error(err.message) | ||
246 | ) | ||
247 | } | ||
248 | |||
249 | private buildInternalActions (): DropdownAction<ProcessedAbuse>[] { | ||
250 | return [ | ||
251 | { | ||
252 | label: this.i18n('Internal actions'), | ||
253 | isHeader: true | ||
254 | }, | ||
255 | { | ||
256 | label: this.i18n('Delete report'), | ||
257 | handler: abuse => this.removeAbuse(abuse) | ||
258 | }, | ||
259 | { | ||
260 | label: this.i18n('Add note'), | ||
261 | handler: abuse => this.openModerationCommentModal(abuse), | ||
262 | isDisplayed: abuse => !abuse.moderationComment | ||
263 | }, | ||
264 | { | ||
265 | label: this.i18n('Update note'), | ||
266 | handler: abuse => this.openModerationCommentModal(abuse), | ||
267 | isDisplayed: abuse => !!abuse.moderationComment | ||
268 | }, | ||
269 | { | ||
270 | label: this.i18n('Mark as accepted'), | ||
271 | handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED), | ||
272 | isDisplayed: abuse => !this.isAbuseAccepted(abuse) | ||
273 | }, | ||
274 | { | ||
275 | label: this.i18n('Mark as rejected'), | ||
276 | handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED), | ||
277 | isDisplayed: abuse => !this.isAbuseRejected(abuse) | ||
278 | } | ||
279 | ] | ||
280 | } | ||
281 | |||
282 | private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] { | ||
283 | return [ | ||
284 | { | ||
285 | label: this.i18n('Actions for the flagged account'), | ||
286 | isHeader: true, | ||
287 | isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video | ||
288 | }, | ||
289 | |||
290 | { | ||
291 | label: this.i18n('Mute account'), | ||
292 | isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video, | ||
293 | handler: abuse => this.muteAccountHelper(abuse.flaggedAccount) | ||
294 | }, | ||
295 | |||
296 | { | ||
297 | label: this.i18n('Mute server account'), | ||
298 | isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video, | ||
299 | handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host) | ||
300 | } | ||
301 | ] | ||
302 | } | ||
303 | |||
304 | private buildAccountActions (): DropdownAction<ProcessedAbuse>[] { | ||
305 | return [ | ||
306 | { | ||
307 | label: this.i18n('Actions for the reporter'), | ||
308 | isHeader: true, | ||
309 | isDisplayed: abuse => !!abuse.reporterAccount | ||
310 | }, | ||
311 | |||
312 | { | ||
313 | label: this.i18n('Mute reporter'), | ||
314 | isDisplayed: abuse => !!abuse.reporterAccount, | ||
315 | handler: abuse => this.muteAccountHelper(abuse.reporterAccount) | ||
316 | }, | ||
317 | |||
318 | { | ||
319 | label: this.i18n('Mute server'), | ||
320 | isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId, | ||
321 | handler: abuse => this.muteServerHelper(abuse.reporterAccount.host) | ||
322 | } | ||
323 | ] | ||
324 | } | ||
325 | |||
326 | private buildVideoActions (): DropdownAction<ProcessedAbuse>[] { | ||
327 | return [ | ||
328 | { | ||
329 | label: this.i18n('Actions for the video'), | ||
330 | isHeader: true, | ||
331 | isDisplayed: abuse => abuse.video && !abuse.video.deleted | ||
332 | }, | ||
333 | { | ||
334 | label: this.i18n('Block video'), | ||
335 | isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted, | ||
336 | handler: abuse => { | ||
337 | this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true) | ||
338 | .subscribe( | ||
339 | () => { | ||
340 | this.notifier.success(this.i18n('Video blocked.')) | ||
341 | |||
342 | this.updateAbuseState(abuse, AbuseState.ACCEPTED) | ||
343 | }, | ||
344 | |||
345 | err => this.notifier.error(err.message) | ||
346 | ) | ||
347 | } | ||
348 | }, | ||
349 | { | ||
350 | label: this.i18n('Unblock video'), | ||
351 | isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted, | ||
352 | handler: abuse => { | ||
353 | this.videoBlocklistService.unblockVideo(abuse.video.id) | ||
354 | .subscribe( | ||
355 | () => { | ||
356 | this.notifier.success(this.i18n('Video unblocked.')) | ||
357 | |||
358 | this.updateAbuseState(abuse, AbuseState.ACCEPTED) | ||
359 | }, | ||
360 | |||
361 | err => this.notifier.error(err.message) | ||
362 | ) | ||
363 | } | ||
364 | }, | ||
365 | { | ||
366 | label: this.i18n('Delete video'), | ||
367 | isDisplayed: abuse => abuse.video && !abuse.video.deleted, | ||
368 | handler: async abuse => { | ||
369 | const res = await this.confirmService.confirm( | ||
370 | this.i18n('Do you really want to delete this video?'), | ||
371 | this.i18n('Delete') | ||
372 | ) | ||
373 | if (res === false) return | ||
374 | |||
375 | this.videoService.removeVideo(abuse.video.id) | ||
376 | .subscribe( | ||
377 | () => { | ||
378 | this.notifier.success(this.i18n('Video deleted.')) | ||
379 | |||
380 | this.updateAbuseState(abuse, AbuseState.ACCEPTED) | ||
381 | }, | ||
382 | |||
383 | err => this.notifier.error(err.message) | ||
384 | ) | ||
385 | } | ||
386 | } | ||
387 | ] | ||
388 | } | ||
389 | |||
390 | private buildCommentActions (): DropdownAction<ProcessedAbuse>[] { | ||
391 | return [ | ||
392 | { | ||
393 | label: this.i18n('Actions for the comment'), | ||
394 | isHeader: true, | ||
395 | isDisplayed: abuse => abuse.comment && !abuse.comment.deleted | ||
396 | }, | ||
397 | |||
398 | { | ||
399 | label: this.i18n('Delete comment'), | ||
400 | isDisplayed: abuse => abuse.comment && !abuse.comment.deleted, | ||
401 | handler: async abuse => { | ||
402 | const res = await this.confirmService.confirm( | ||
403 | this.i18n('Do you really want to delete this comment?'), | ||
404 | this.i18n('Delete') | ||
405 | ) | ||
406 | if (res === false) return | ||
407 | |||
408 | this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id) | ||
409 | .subscribe( | ||
410 | () => { | ||
411 | this.notifier.success(this.i18n('Comment deleted.')) | ||
412 | |||
413 | this.updateAbuseState(abuse, AbuseState.ACCEPTED) | ||
414 | }, | ||
415 | |||
416 | err => this.notifier.error(err.message) | ||
417 | ) | ||
418 | } | ||
419 | } | ||
420 | ] | ||
421 | } | ||
422 | |||
423 | private muteAccountHelper (account: Account) { | ||
424 | this.blocklistService.blockAccountByInstance(account) | ||
425 | .subscribe( | ||
426 | () => { | ||
427 | this.notifier.success( | ||
428 | this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) | ||
429 | ) | ||
430 | |||
431 | account.mutedByInstance = true | ||
432 | }, | ||
433 | |||
434 | err => this.notifier.error(err.message) | ||
435 | ) | ||
436 | } | ||
437 | |||
438 | private muteServerHelper (host: string) { | ||
439 | this.blocklistService.blockServerByInstance(host) | ||
440 | .subscribe( | ||
441 | () => { | ||
442 | this.notifier.success( | ||
443 | this.i18n('Server {{host}} muted by the instance.', { host: host }) | ||
444 | ) | ||
445 | }, | ||
446 | |||
447 | err => this.notifier.error(err.message) | ||
448 | ) | ||
449 | } | ||
450 | |||
451 | private toHtml (text: string) { | ||
452 | return this.markdownRenderer.textMarkdownToHTML(text) | ||
453 | } | ||
454 | } | ||
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 @@ | |||
1 | export * from './abuse-details.component' | ||
2 | export * from './abuse-list.component' | ||
3 | 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 index 8082e93f4..8082e93f4 100644 --- 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 | |||
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 index afcdb9a16..afcdb9a16 100644 --- 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 | |||
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 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 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms' |
4 | import { VideoAbuseService } from '@app/shared/shared-moderation' | 4 | import { AbuseService } from '@app/shared/shared-moderation' |
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { VideoAbuse } from '@shared/models' | 8 | import { Abuse } from '@shared/models' |
9 | 9 | ||
10 | @Component({ | 10 | @Component({ |
11 | selector: 'my-moderation-comment-modal', | 11 | selector: 'my-moderation-comment-modal', |
@@ -16,15 +16,15 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
16 | @ViewChild('modal', { static: true }) modal: NgbModal | 16 | @ViewChild('modal', { static: true }) modal: NgbModal |
17 | @Output() commentUpdated = new EventEmitter<string>() | 17 | @Output() commentUpdated = new EventEmitter<string>() |
18 | 18 | ||
19 | private abuseToComment: VideoAbuse | 19 | private abuseToComment: Abuse |
20 | private openedModal: NgbModalRef | 20 | private openedModal: NgbModalRef |
21 | 21 | ||
22 | constructor ( | 22 | constructor ( |
23 | protected formValidatorService: FormValidatorService, | 23 | protected formValidatorService: FormValidatorService, |
24 | private modalService: NgbModal, | 24 | private modalService: NgbModal, |
25 | private notifier: Notifier, | 25 | private notifier: Notifier, |
26 | private videoAbuseService: VideoAbuseService, | 26 | private abuseService: AbuseService, |
27 | private videoAbuseValidatorsService: VideoAbuseValidatorsService, | 27 | private abuseValidatorsService: AbuseValidatorsService, |
28 | private i18n: I18n | 28 | private i18n: I18n |
29 | ) { | 29 | ) { |
30 | super() | 30 | super() |
@@ -32,11 +32,11 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
32 | 32 | ||
33 | ngOnInit () { | 33 | ngOnInit () { |
34 | this.buildForm({ | 34 | this.buildForm({ |
35 | moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_MODERATION_COMMENT | 35 | moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT |
36 | }) | 36 | }) |
37 | } | 37 | } |
38 | 38 | ||
39 | openModal (abuseToComment: VideoAbuse) { | 39 | openModal (abuseToComment: Abuse) { |
40 | this.abuseToComment = abuseToComment | 40 | this.abuseToComment = abuseToComment |
41 | this.openedModal = this.modalService.open(this.modal, { centered: true }) | 41 | this.openedModal = this.modalService.open(this.modal, { centered: true }) |
42 | 42 | ||
@@ -54,7 +54,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
54 | async banUser () { | 54 | async banUser () { |
55 | const moderationComment: string = this.form.value[ 'moderationComment' ] | 55 | const moderationComment: string = this.form.value[ 'moderationComment' ] |
56 | 56 | ||
57 | this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) | 57 | this.abuseService.updateAbuse(this.abuseToComment, { moderationComment }) |
58 | .subscribe( | 58 | .subscribe( |
59 | () => { | 59 | () => { |
60 | this.notifier.success(this.i18n('Comment updated.')) | 60 | 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 @@ | |||
1 | export * from './abuse-list' | ||
1 | export * from './instance-blocklist' | 2 | export * from './instance-blocklist' |
2 | export * from './video-abuse-list' | ||
3 | export * from './video-block-list' | 3 | export * from './video-block-list' |
4 | export * from './moderation.component' | 4 | export * from './moderation.component' |
5 | export * from './moderation.routes' | 5 | export * from './moderation.routes' |
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index 0ec420af9..65fe94d39 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss | |||
@@ -25,18 +25,18 @@ | |||
25 | vertical-align: top; | 25 | vertical-align: top; |
26 | text-align: right; | 26 | text-align: right; |
27 | } | 27 | } |
28 | 28 | ||
29 | .moderation-expanded-text { | 29 | .moderation-expanded-text { |
30 | display: inline-flex; | 30 | display: inline-flex; |
31 | word-wrap: break-word; | 31 | word-wrap: break-word; |
32 | 32 | ||
33 | ::ng-deep p:last-child { | 33 | ::ng-deep p:last-child { |
34 | margin-bottom: 0px !important; | 34 | margin-bottom: 0px !important; |
35 | } | 35 | } |
36 | } | 36 | } |
37 | } | 37 | } |
38 | 38 | ||
39 | .video-table-states { | 39 | .table-states { |
40 | & > :not(:first-child) { | 40 | & > :not(:first-child) { |
41 | margin-left: .4rem; | 41 | margin-left: .4rem; |
42 | } | 42 | } |
@@ -59,6 +59,7 @@ p-calendar { | |||
59 | .screenratio { | 59 | .screenratio { |
60 | div { | 60 | div { |
61 | @include miniature-thumbnail; | 61 | @include miniature-thumbnail; |
62 | |||
62 | display: inline-flex; | 63 | display: inline-flex; |
63 | justify-content: center; | 64 | justify-content: center; |
64 | align-items: center; | 65 | align-items: center; |
@@ -72,6 +73,11 @@ p-calendar { | |||
72 | }; | 73 | }; |
73 | } | 74 | } |
74 | 75 | ||
76 | .comment-html { | ||
77 | background-color: #ececec; | ||
78 | padding: 10px; | ||
79 | } | ||
80 | |||
75 | .chip { | 81 | .chip { |
76 | @include chip; | 82 | @include chip; |
77 | } | 83 | } |
@@ -83,16 +89,39 @@ my-action-dropdown.show { | |||
83 | } | 89 | } |
84 | 90 | ||
85 | 91 | ||
86 | .video-table-video-link { | 92 | .table-video-link { |
87 | @include disable-outline; | 93 | @include disable-outline; |
94 | |||
88 | position: relative; | 95 | position: relative; |
89 | top: 3px; | 96 | top: 3px; |
90 | } | 97 | } |
91 | 98 | ||
92 | .video-table-video { | 99 | .table-comment-link, |
100 | .table-account-link { | ||
101 | @include disable-outline; | ||
102 | |||
103 | color: var(--mainForegroundColor); | ||
104 | |||
105 | ::ng-deep p:last-child { | ||
106 | margin: 0; | ||
107 | } | ||
108 | } | ||
109 | |||
110 | .table-account-link { | ||
111 | display: flex; | ||
112 | flex-direction: column; | ||
113 | } | ||
114 | |||
115 | .comment-flagged-account, | ||
116 | .account-flagged-handle { | ||
117 | font-size: 11px; | ||
118 | color: var(--greyForegroundColor); | ||
119 | } | ||
120 | |||
121 | .table-video { | ||
93 | display: inline-flex; | 122 | display: inline-flex; |
94 | 123 | ||
95 | .video-table-video-image { | 124 | .table-video-image { |
96 | @include miniature-thumbnail; | 125 | @include miniature-thumbnail; |
97 | 126 | ||
98 | $image-height: 45px; | 127 | $image-height: 45px; |
@@ -118,7 +147,7 @@ my-action-dropdown.show { | |||
118 | color: pvar(--inputPlaceholderColor); | 147 | color: pvar(--inputPlaceholderColor); |
119 | } | 148 | } |
120 | 149 | ||
121 | .video-table-video-image-label { | 150 | .table-video-image-label { |
122 | @include static-thumbnail-overlay; | 151 | @include static-thumbnail-overlay; |
123 | position: absolute; | 152 | position: absolute; |
124 | border-radius: 3px; | 153 | border-radius: 3px; |
@@ -130,7 +159,7 @@ my-action-dropdown.show { | |||
130 | } | 159 | } |
131 | } | 160 | } |
132 | 161 | ||
133 | .video-table-video-text { | 162 | .table-video-text { |
134 | display: inline-flex; | 163 | display: inline-flex; |
135 | flex-direction: column; | 164 | flex-direction: column; |
136 | justify-content: center; | 165 | justify-content: center; |
@@ -145,7 +174,8 @@ my-action-dropdown.show { | |||
145 | } | 174 | } |
146 | 175 | ||
147 | div + div { | 176 | div + div { |
148 | font-size: 80%; | 177 | color: var(--greyForegroundColor); |
178 | font-size: 11px; | ||
149 | } | 179 | } |
150 | } | 180 | } |
151 | } | 181 | } |
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index cd837bcb9..8a31a54dc 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' | 2 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' |
3 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 3 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
4 | import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' | 4 | import { AbuseListComponent } from '@app/+admin/moderation/abuse-list' |
5 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' | 5 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' |
6 | import { UserRightGuard } from '@app/core' | 6 | import { UserRightGuard } from '@app/core' |
7 | import { UserRight } from '@shared/models' | 7 | import { UserRight } from '@shared/models' |
@@ -13,22 +13,27 @@ export const ModerationRoutes: Routes = [ | |||
13 | children: [ | 13 | children: [ |
14 | { | 14 | { |
15 | path: '', | 15 | path: '', |
16 | redirectTo: 'video-abuses/list', | 16 | redirectTo: 'abuses/list', |
17 | pathMatch: 'full' | 17 | pathMatch: 'full' |
18 | }, | 18 | }, |
19 | { | 19 | { |
20 | path: 'video-abuses', | 20 | path: 'video-abuses', |
21 | redirectTo: 'video-abuses/list', | 21 | redirectTo: 'abuses/list', |
22 | pathMatch: 'full' | 22 | pathMatch: 'full' |
23 | }, | 23 | }, |
24 | { | 24 | { |
25 | path: 'video-abuses/list', | 25 | path: 'video-abuses/list', |
26 | component: VideoAbuseListComponent, | 26 | redirectTo: 'abuses/list', |
27 | pathMatch: 'full' | ||
28 | }, | ||
29 | { | ||
30 | path: 'abuses/list', | ||
31 | component: AbuseListComponent, | ||
27 | canActivate: [ UserRightGuard ], | 32 | canActivate: [ UserRightGuard ], |
28 | data: { | 33 | data: { |
29 | userRight: UserRight.MANAGE_VIDEO_ABUSES, | 34 | userRight: UserRight.MANAGE_ABUSES, |
30 | meta: { | 35 | meta: { |
31 | title: 'Video reports' | 36 | title: 'Reports' |
32 | } | 37 | } |
33 | } | 38 | } |
34 | }, | 39 | }, |
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 @@ | |||
1 | export * from './video-abuse-list.component' | ||
2 | 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 @@ | |||
1 | <div class="d-flex moderation-expanded"> | ||
2 | <!-- report left part (report details) --> | ||
3 | <div class="col-8"> | ||
4 | |||
5 | <!-- report metadata --> | ||
6 | <div class="d-flex"> | ||
7 | <span class="col-3 moderation-expanded-label" i18n>Reporter</span> | ||
8 | <span class="col-9 moderation-expanded-text"> | ||
9 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + videoAbuse.reporterAccount.displayName + '"' }" class="chip"> | ||
10 | <img | ||
11 | class="avatar" | ||
12 | [src]="videoAbuse.reporterAccount.avatar?.path" | ||
13 | (error)="switchToDefaultAvatar($event)" | ||
14 | alt="Avatar" | ||
15 | > | ||
16 | <div> | ||
17 | <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span> | ||
18 | </div> | ||
19 | </a> | ||
20 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + videoAbuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n> | ||
21 | {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span> | ||
22 | </a> | ||
23 | </span> | ||
24 | </div> | ||
25 | |||
26 | <div class="d-flex"> | ||
27 | <span class="col-3 moderation-expanded-label" i18n>Reportee</span> | ||
28 | <span class="col-9 moderation-expanded-text"> | ||
29 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +videoAbuse.video.channel.ownerAccount.displayName + '"' }" class="chip"> | ||
30 | <img | ||
31 | class="avatar" | ||
32 | [src]="videoAbuse.video.channel.ownerAccount?.avatar?.path" | ||
33 | (error)="switchToDefaultAvatar($event)" | ||
34 | alt="Avatar" | ||
35 | > | ||
36 | <div> | ||
37 | <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? videoAbuse.video.channel.ownerAccount.nameWithHost : '' }}</span> | ||
38 | </div> | ||
39 | </a> | ||
40 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +videoAbuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n> | ||
41 | {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span> | ||
42 | </a> | ||
43 | </span> | ||
44 | </div> | ||
45 | |||
46 | <div class="d-flex" *ngIf="videoAbuse.updatedAt"> | ||
47 | <span class="col-3 moderation-expanded-label" i18n>Updated</span> | ||
48 | <time class="col-9 moderation-expanded-text video-details-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time> | ||
49 | </div> | ||
50 | |||
51 | <!-- report text --> | ||
52 | <div class="mt-3 d-flex"> | ||
53 | <span class="col-3 moderation-expanded-label"> | ||
54 | <ng-container i18n>Report</ng-container> | ||
55 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': '#' + videoAbuse.id }" class="ml-1 text-muted">#{{ videoAbuse.id }}</a> | ||
56 | </span> | ||
57 | <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span> | ||
58 | </div> | ||
59 | |||
60 | <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> | ||
61 | <span class="col-3"></span> | ||
62 | <span class="col-9"> | ||
63 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()"> | ||
64 | <div>{{ reason.label }}</div> | ||
65 | </a> | ||
66 | </span> | ||
67 | </div> | ||
68 | |||
69 | <div *ngIf="videoAbuse.startAt" class="mt-2 d-flex"> | ||
70 | <span class="col-3 moderation-expanded-label" i18n>Reported part</span> | ||
71 | <span class="col-9"> | ||
72 | {{ startAt }}<ng-container *ngIf="videoAbuse.endAt"> - {{ endAt }}</ng-container> | ||
73 | </span> | ||
74 | </div> | ||
75 | |||
76 | <div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment"> | ||
77 | <span class="col-3 moderation-expanded-label" i18n>Note</span> | ||
78 | <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span> | ||
79 | </div> | ||
80 | |||
81 | </div> | ||
82 | |||
83 | <!-- report right part (video details) --> | ||
84 | <div class="col-4"> | ||
85 | <div class="screenratio"> | ||
86 | <div *ngIf="videoAbuse.video.deleted || videoAbuse.video.blacklisted"> | ||
87 | <span i18n *ngIf="videoAbuse.video.deleted">The video was deleted</span> | ||
88 | <span i18n *ngIf="!videoAbuse.video.deleted">The video was blocked</span> | ||
89 | </div> | ||
90 | <div *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" [innerHTML]="videoAbuse.embedHtml"></div> | ||
91 | </div> | ||
92 | </div> | ||
93 | </div> | ||
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html deleted file mode 100644 index 64641b28a..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ /dev/null | |||
@@ -1,149 +0,0 @@ | |||
1 | <p-table | ||
2 | [value]="videoAbuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" | ||
3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true" | ||
4 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
5 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports" | ||
6 | (onPage)="onPage($event)" [expandedRowKeys]="expandedRows" | ||
7 | > | ||
8 | <ng-template pTemplate="caption"> | ||
9 | <div class="caption"> | ||
10 | <div class="ml-auto"> | ||
11 | <div class="input-group has-feedback has-clear"> | ||
12 | <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> | ||
13 | <div class="input-group-text" ngbDropdownToggle> | ||
14 | <span class="caret" aria-haspopup="menu" role="button"></span> | ||
15 | </div> | ||
16 | |||
17 | <div role="menu" ngbDropdownMenu> | ||
18 | <h6 class="dropdown-header" i18n>Advanced report filters</h6> | ||
19 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a> | ||
20 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a> | ||
21 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a> | ||
22 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a> | ||
23 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a> | ||
24 | </div> | ||
25 | </div> | ||
26 | <input | ||
27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
28 | (keyup)="onAbuseSearch($event)" | ||
29 | > | ||
30 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | ||
31 | <span class="sr-only" i18n>Clear filters</span> | ||
32 | </div> | ||
33 | </div> | ||
34 | </div> | ||
35 | </ng-template> | ||
36 | |||
37 | <ng-template pTemplate="header"> | ||
38 | <tr> <!-- header --> | ||
39 | <th style="width: 40px;"></th> | ||
40 | <th style="width: 20%;" pResizableColumn i18n>Reporter</th> | ||
41 | <th i18n>Video</th> | ||
42 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
43 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> | ||
44 | <th style="width: 150px;"></th> | ||
45 | </tr> | ||
46 | </ng-template> | ||
47 | |||
48 | <ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse> | ||
49 | <tr> | ||
50 | <td class="c-hand" [pRowToggler]="videoAbuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> | ||
51 | <span class="expander"> | ||
52 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | ||
53 | </span> | ||
54 | </td> | ||
55 | |||
56 | <td> | ||
57 | <a [href]="videoAbuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | ||
58 | <div class="chip two-lines"> | ||
59 | <img | ||
60 | class="avatar" | ||
61 | [src]="videoAbuse.reporterAccount.avatar?.path" | ||
62 | (error)="switchToDefaultAvatar($event)" | ||
63 | alt="Avatar" | ||
64 | > | ||
65 | <div> | ||
66 | {{ videoAbuse.reporterAccount.displayName }} | ||
67 | <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span> | ||
68 | </div> | ||
69 | </div> | ||
70 | </a> | ||
71 | </td> | ||
72 | |||
73 | <td *ngIf="!videoAbuse.video.deleted"> | ||
74 | <a [href]="getVideoUrl(videoAbuse)" class="video-table-video-link" [title]="videoAbuse.video.name" target="_blank" rel="noopener noreferrer"> | ||
75 | <div class="video-table-video"> | ||
76 | <div class="video-table-video-image"> | ||
77 | <img [src]="videoAbuse.video.thumbnailPath"> | ||
78 | <span | ||
79 | class="video-table-video-image-label" *ngIf="videoAbuse.count > 1" | ||
80 | i18n-title title="This video has been reported multiple times." | ||
81 | > | ||
82 | {{ videoAbuse.nth }}/{{ videoAbuse.count }} | ||
83 | </span> | ||
84 | </div> | ||
85 | <div class="video-table-video-text"> | ||
86 | <div> | ||
87 | <span *ngIf="!videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span> | ||
88 | <span *ngIf="videoAbuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span> | ||
89 | {{ videoAbuse.video.name }} | ||
90 | </div> | ||
91 | <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div> | ||
92 | </div> | ||
93 | </div> | ||
94 | </a> | ||
95 | </td> | ||
96 | |||
97 | <td *ngIf="videoAbuse.video.deleted" class="c-hand" [pRowToggler]="videoAbuse"> | ||
98 | <div class="video-table-video" i18n-title title="Video was deleted"> | ||
99 | <div class="video-table-video-image"> | ||
100 | <span i18n>Deleted</span> | ||
101 | </div> | ||
102 | <div class="video-table-video-text"> | ||
103 | <div> | ||
104 | {{ videoAbuse.video.name }} | ||
105 | <span class="glyphicon glyphicon-trash"></span> | ||
106 | </div> | ||
107 | <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div> | ||
108 | </div> | ||
109 | </div> | ||
110 | </td> | ||
111 | |||
112 | <td class="c-hand" [pRowToggler]="videoAbuse">{{ videoAbuse.createdAt | date: 'short' }}</td> | ||
113 | |||
114 | <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse"> | ||
115 | <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> | ||
116 | <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span> | ||
117 | <span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span> | ||
118 | </td> | ||
119 | |||
120 | <td class="action-cell"> | ||
121 | <my-action-dropdown | ||
122 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" | ||
123 | i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse" | ||
124 | ></my-action-dropdown> | ||
125 | </td> | ||
126 | </tr> | ||
127 | </ng-template> | ||
128 | |||
129 | <ng-template pTemplate="rowexpansion" let-videoAbuse> | ||
130 | <tr> | ||
131 | <td class="expand-cell" colspan="6"> | ||
132 | <my-video-abuse-details [videoAbuse]="videoAbuse"></my-video-abuse-details> | ||
133 | </td> | ||
134 | </tr> | ||
135 | </ng-template> | ||
136 | |||
137 | <ng-template pTemplate="emptymessage"> | ||
138 | <tr> | ||
139 | <td colspan="6"> | ||
140 | <div class="no-results"> | ||
141 | <ng-container *ngIf="search" i18n>No video abuses found matching current filters.</ng-container> | ||
142 | <ng-container *ngIf="!search" i18n>No video abuses found.</ng-container> | ||
143 | </div> | ||
144 | </td> | ||
145 | </tr> | ||
146 | </ng-template> | ||
147 | </p-table> | ||
148 | |||
149 | <my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal> | ||
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts deleted file mode 100644 index 409dd42c7..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ /dev/null | |||
@@ -1,328 +0,0 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { filter } from 'rxjs/operators' | ||
3 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' | ||
4 | import { environment } from 'src/environments/environment' | ||
5 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' | ||
6 | import { DomSanitizer } from '@angular/platform-browser' | ||
7 | import { ActivatedRoute, Params, Router } from '@angular/router' | ||
8 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' | ||
9 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' | ||
10 | import { BlocklistService, VideoAbuseService, VideoBlockService } from '@app/shared/shared-moderation' | ||
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
12 | import { VideoAbuse, VideoAbuseState } from '@shared/models' | ||
13 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' | ||
14 | |||
15 | export type ProcessedVideoAbuse = VideoAbuse & { | ||
16 | moderationCommentHtml?: string, | ||
17 | reasonHtml?: string | ||
18 | embedHtml?: string | ||
19 | updatedAt?: Date | ||
20 | // override bare server-side definitions with rich client-side definitions | ||
21 | reporterAccount: Account | ||
22 | video: VideoAbuse['video'] & { | ||
23 | channel: VideoAbuse['video']['channel'] & { | ||
24 | ownerAccount: Account | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | |||
29 | @Component({ | ||
30 | selector: 'my-video-abuse-list', | ||
31 | templateUrl: './video-abuse-list.component.html', | ||
32 | styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ] | ||
33 | }) | ||
34 | export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit { | ||
35 | @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent | ||
36 | |||
37 | videoAbuses: ProcessedVideoAbuse[] = [] | ||
38 | totalRecords = 0 | ||
39 | sort: SortMeta = { field: 'createdAt', order: 1 } | ||
40 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
41 | |||
42 | videoAbuseActions: DropdownAction<VideoAbuse>[][] = [] | ||
43 | |||
44 | constructor ( | ||
45 | private notifier: Notifier, | ||
46 | private videoAbuseService: VideoAbuseService, | ||
47 | private blocklistService: BlocklistService, | ||
48 | private videoService: VideoService, | ||
49 | private videoBlocklistService: VideoBlockService, | ||
50 | private confirmService: ConfirmService, | ||
51 | private i18n: I18n, | ||
52 | private markdownRenderer: MarkdownService, | ||
53 | private sanitizer: DomSanitizer, | ||
54 | private route: ActivatedRoute, | ||
55 | private router: Router | ||
56 | ) { | ||
57 | super() | ||
58 | |||
59 | this.videoAbuseActions = [ | ||
60 | [ | ||
61 | { | ||
62 | label: this.i18n('Internal actions'), | ||
63 | isHeader: true | ||
64 | }, | ||
65 | { | ||
66 | label: this.i18n('Delete report'), | ||
67 | handler: videoAbuse => this.removeVideoAbuse(videoAbuse) | ||
68 | }, | ||
69 | { | ||
70 | label: this.i18n('Add note'), | ||
71 | handler: videoAbuse => this.openModerationCommentModal(videoAbuse), | ||
72 | isDisplayed: videoAbuse => !videoAbuse.moderationComment | ||
73 | }, | ||
74 | { | ||
75 | label: this.i18n('Update note'), | ||
76 | handler: videoAbuse => this.openModerationCommentModal(videoAbuse), | ||
77 | isDisplayed: videoAbuse => !!videoAbuse.moderationComment | ||
78 | }, | ||
79 | { | ||
80 | label: this.i18n('Mark as accepted'), | ||
81 | handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED), | ||
82 | isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse) | ||
83 | }, | ||
84 | { | ||
85 | label: this.i18n('Mark as rejected'), | ||
86 | handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED), | ||
87 | isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse) | ||
88 | } | ||
89 | ], | ||
90 | [ | ||
91 | { | ||
92 | label: this.i18n('Actions for the video'), | ||
93 | isHeader: true, | ||
94 | isDisplayed: videoAbuse => !videoAbuse.video.deleted | ||
95 | }, | ||
96 | { | ||
97 | label: this.i18n('Block video'), | ||
98 | isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted, | ||
99 | handler: videoAbuse => { | ||
100 | this.videoBlocklistService.blockVideo(videoAbuse.video.id, undefined, true) | ||
101 | .subscribe( | ||
102 | () => { | ||
103 | this.notifier.success(this.i18n('Video blocked.')) | ||
104 | |||
105 | this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) | ||
106 | }, | ||
107 | |||
108 | err => this.notifier.error(err.message) | ||
109 | ) | ||
110 | } | ||
111 | }, | ||
112 | { | ||
113 | label: this.i18n('Unblock video'), | ||
114 | isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted, | ||
115 | handler: videoAbuse => { | ||
116 | this.videoBlocklistService.unblockVideo(videoAbuse.video.id) | ||
117 | .subscribe( | ||
118 | () => { | ||
119 | this.notifier.success(this.i18n('Video unblocked.')) | ||
120 | |||
121 | this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) | ||
122 | }, | ||
123 | |||
124 | err => this.notifier.error(err.message) | ||
125 | ) | ||
126 | } | ||
127 | }, | ||
128 | { | ||
129 | label: this.i18n('Delete video'), | ||
130 | isDisplayed: videoAbuse => !videoAbuse.video.deleted, | ||
131 | handler: async videoAbuse => { | ||
132 | const res = await this.confirmService.confirm( | ||
133 | this.i18n('Do you really want to delete this video?'), | ||
134 | this.i18n('Delete') | ||
135 | ) | ||
136 | if (res === false) return | ||
137 | |||
138 | this.videoService.removeVideo(videoAbuse.video.id) | ||
139 | .subscribe( | ||
140 | () => { | ||
141 | this.notifier.success(this.i18n('Video deleted.')) | ||
142 | |||
143 | this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) | ||
144 | }, | ||
145 | |||
146 | err => this.notifier.error(err.message) | ||
147 | ) | ||
148 | } | ||
149 | } | ||
150 | ], | ||
151 | [ | ||
152 | { | ||
153 | label: this.i18n('Actions for the reporter'), | ||
154 | isHeader: true | ||
155 | }, | ||
156 | { | ||
157 | label: this.i18n('Mute reporter'), | ||
158 | handler: async videoAbuse => { | ||
159 | const account = videoAbuse.reporterAccount as Account | ||
160 | |||
161 | this.blocklistService.blockAccountByInstance(account) | ||
162 | .subscribe( | ||
163 | () => { | ||
164 | this.notifier.success( | ||
165 | this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) | ||
166 | ) | ||
167 | |||
168 | account.mutedByInstance = true | ||
169 | }, | ||
170 | |||
171 | err => this.notifier.error(err.message) | ||
172 | ) | ||
173 | } | ||
174 | }, | ||
175 | { | ||
176 | label: this.i18n('Mute server'), | ||
177 | isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId, | ||
178 | handler: async videoAbuse => { | ||
179 | this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host) | ||
180 | .subscribe( | ||
181 | () => { | ||
182 | this.notifier.success( | ||
183 | this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host }) | ||
184 | ) | ||
185 | }, | ||
186 | |||
187 | err => this.notifier.error(err.message) | ||
188 | ) | ||
189 | } | ||
190 | } | ||
191 | ] | ||
192 | ] | ||
193 | } | ||
194 | |||
195 | ngOnInit () { | ||
196 | this.initialize() | ||
197 | |||
198 | this.route.queryParams | ||
199 | .subscribe(params => { | ||
200 | this.search = params.search || '' | ||
201 | |||
202 | this.setTableFilter(this.search) | ||
203 | this.loadData() | ||
204 | }) | ||
205 | } | ||
206 | |||
207 | ngAfterViewInit () { | ||
208 | if (this.search) this.setTableFilter(this.search) | ||
209 | } | ||
210 | |||
211 | getIdentifier () { | ||
212 | return 'VideoAbuseListComponent' | ||
213 | } | ||
214 | |||
215 | openModerationCommentModal (videoAbuse: VideoAbuse) { | ||
216 | this.moderationCommentModal.openModal(videoAbuse) | ||
217 | } | ||
218 | |||
219 | onModerationCommentUpdated () { | ||
220 | this.loadData() | ||
221 | } | ||
222 | |||
223 | /* Table filter functions */ | ||
224 | onAbuseSearch (event: Event) { | ||
225 | this.onSearch(event) | ||
226 | this.setQueryParams((event.target as HTMLInputElement).value) | ||
227 | } | ||
228 | |||
229 | setQueryParams (search: string) { | ||
230 | const queryParams: Params = {} | ||
231 | if (search) Object.assign(queryParams, { search }) | ||
232 | |||
233 | this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams }) | ||
234 | } | ||
235 | |||
236 | resetTableFilter () { | ||
237 | this.setTableFilter('') | ||
238 | this.setQueryParams('') | ||
239 | this.resetSearch() | ||
240 | } | ||
241 | /* END Table filter functions */ | ||
242 | |||
243 | isVideoAbuseAccepted (videoAbuse: VideoAbuse) { | ||
244 | return videoAbuse.state.id === VideoAbuseState.ACCEPTED | ||
245 | } | ||
246 | |||
247 | isVideoAbuseRejected (videoAbuse: VideoAbuse) { | ||
248 | return videoAbuse.state.id === VideoAbuseState.REJECTED | ||
249 | } | ||
250 | |||
251 | getVideoUrl (videoAbuse: VideoAbuse) { | ||
252 | return Video.buildClientUrl(videoAbuse.video.uuid) | ||
253 | } | ||
254 | |||
255 | getVideoEmbed (videoAbuse: VideoAbuse) { | ||
256 | return buildVideoEmbed( | ||
257 | buildVideoLink({ | ||
258 | baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`, | ||
259 | title: false, | ||
260 | warningTitle: false, | ||
261 | startTime: videoAbuse.startAt, | ||
262 | stopTime: videoAbuse.endAt | ||
263 | }) | ||
264 | ) | ||
265 | } | ||
266 | |||
267 | switchToDefaultAvatar ($event: Event) { | ||
268 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | ||
269 | } | ||
270 | |||
271 | async removeVideoAbuse (videoAbuse: VideoAbuse) { | ||
272 | const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) | ||
273 | if (res === false) return | ||
274 | |||
275 | this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( | ||
276 | () => { | ||
277 | this.notifier.success(this.i18n('Abuse deleted.')) | ||
278 | this.loadData() | ||
279 | }, | ||
280 | |||
281 | err => this.notifier.error(err.message) | ||
282 | ) | ||
283 | } | ||
284 | |||
285 | updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) { | ||
286 | this.videoAbuseService.updateVideoAbuse(videoAbuse, { state }) | ||
287 | .subscribe( | ||
288 | () => this.loadData(), | ||
289 | |||
290 | err => this.notifier.error(err.message) | ||
291 | ) | ||
292 | } | ||
293 | |||
294 | protected loadData () { | ||
295 | return this.videoAbuseService.getVideoAbuses({ | ||
296 | pagination: this.pagination, | ||
297 | sort: this.sort, | ||
298 | search: this.search | ||
299 | }).subscribe( | ||
300 | async resultList => { | ||
301 | this.totalRecords = resultList.total | ||
302 | const videoAbuses = [] | ||
303 | |||
304 | for (const abuse of resultList.data) { | ||
305 | Object.assign(abuse, { | ||
306 | reasonHtml: await this.toHtml(abuse.reason), | ||
307 | moderationCommentHtml: await this.toHtml(abuse.moderationComment), | ||
308 | embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)), | ||
309 | reporterAccount: new Account(abuse.reporterAccount) | ||
310 | }) | ||
311 | |||
312 | if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) | ||
313 | if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt | ||
314 | |||
315 | videoAbuses.push(abuse as ProcessedVideoAbuse) | ||
316 | } | ||
317 | |||
318 | this.videoAbuses = videoAbuses | ||
319 | }, | ||
320 | |||
321 | err => this.notifier.error(err.message) | ||
322 | ) | ||
323 | } | ||
324 | |||
325 | private toHtml (text: string) { | ||
326 | return this.markdownRenderer.textMarkdownToHTML(text) | ||
327 | } | ||
328 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 297e6104c..2e7b322ca 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -37,14 +37,14 @@ | |||
37 | </a> | 37 | </a> |
38 | </div> | 38 | </div> |
39 | <div> | 39 | <div> |
40 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' + user?.account.displayName + '"' }"> | 40 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' + user?.account.displayName + '"' }"> |
41 | <div class="dashboard-num">{{ user.videoAbusesCount }}</div> | 41 | <div class="dashboard-num">{{ user.abusesCount }}</div> |
42 | <div class="dashboard-label" i18n>Incriminated in reports</div> | 42 | <div class="dashboard-label" i18n>Incriminated in reports</div> |
43 | </a> | 43 | </a> |
44 | </div> | 44 | </div> |
45 | <div> | 45 | <div> |
46 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + user?.account.displayName + '" state:accepted' }"> | 46 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + user?.account.displayName + '" state:accepted' }"> |
47 | <div class="dashboard-num">{{ user.videoAbusesAcceptedCount }} / {{ user.videoAbusesCreatedCount }}</div> | 47 | <div class="dashboard-num">{{ user.abusesAcceptedCount }} / {{ user.abusesCreatedCount }}</div> |
48 | <div class="dashboard-label" i18n>Authored reports accepted</div> | 48 | <div class="dashboard-label" i18n>Authored reports accepted</div> |
49 | </a> | 49 | </a> |
50 | </div> | 50 | </div> |
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..8562e564b 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 | |||
@@ -33,7 +33,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { | |||
33 | this.labelNotifications = { | 33 | this.labelNotifications = { |
34 | newVideoFromSubscription: this.i18n('New video from your subscriptions'), | 34 | newVideoFromSubscription: this.i18n('New video from your subscriptions'), |
35 | newCommentOnMyVideo: this.i18n('New comment on your video'), | 35 | newCommentOnMyVideo: this.i18n('New comment on your video'), |
36 | videoAbuseAsModerator: this.i18n('New video abuse'), | 36 | abuseAsModerator: this.i18n('New abuse'), |
37 | videoAutoBlacklistAsModerator: this.i18n('Video blocked automatically waiting review'), | 37 | videoAutoBlacklistAsModerator: this.i18n('Video blocked automatically waiting review'), |
38 | blacklistOnMyVideo: this.i18n('One of your video is blocked/unblocked'), | 38 | blacklistOnMyVideo: this.i18n('One of your video is blocked/unblocked'), |
39 | myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'), | 39 | myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'), |
@@ -47,7 +47,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { | |||
47 | this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] | 47 | this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] |
48 | 48 | ||
49 | this.rightNotifications = { | 49 | this.rightNotifications = { |
50 | videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES, | 50 | abuseAsModerator: UserRight.MANAGE_ABUSES, |
51 | videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, | 51 | videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, |
52 | newUserRegistration: UserRight.MANAGE_USERS, | 52 | newUserRegistration: UserRight.MANAGE_USERS, |
53 | newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, | 53 | newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, |
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts index 79505c779..d79efbb49 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts +++ b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts | |||
@@ -4,10 +4,9 @@ import { Router } from '@angular/router' | |||
4 | import { Notifier, User } from '@app/core' | 4 | import { Notifier, User } from '@app/core' |
5 | import { FormReactive, FormValidatorService, VideoCommentValidatorsService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormValidatorService, VideoCommentValidatorsService } from '@app/shared/shared-forms' |
6 | import { Video } from '@app/shared/shared-main' | 6 | import { Video } from '@app/shared/shared-main' |
7 | import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment' | ||
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 8 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { VideoCommentCreate } from '@shared/models' | 9 | import { VideoCommentCreate } from '@shared/models' |
9 | import { VideoComment } from './video-comment.model' | ||
10 | import { VideoCommentService } from './video-comment.service' | ||
11 | 10 | ||
12 | @Component({ | 11 | @Component({ |
13 | selector: 'my-video-comment-add', | 12 | selector: 'my-video-comment-add', |
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/comment/video-comment.component.html index 002de57e4..f02ea549a 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.component.html +++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.html | |||
@@ -45,6 +45,7 @@ | |||
45 | <div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete" i18n>Delete</div> | 45 | <div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete" i18n>Delete</div> |
46 | 46 | ||
47 | <my-user-moderation-dropdown | 47 | <my-user-moderation-dropdown |
48 | [prependActions]="prependModerationActions" | ||
48 | buttonSize="small" [account]="commentAccount" [user]="commentUser" i18n-label label="Options" placement="bottom-left auto" | 49 | buttonSize="small" [account]="commentAccount" [user]="commentUser" i18n-label label="Options" placement="bottom-left auto" |
49 | ></my-user-moderation-dropdown> | 50 | ></my-user-moderation-dropdown> |
50 | </div> | 51 | </div> |
@@ -93,3 +94,7 @@ | |||
93 | </div> | 94 | </div> |
94 | </div> | 95 | </div> |
95 | </div> | 96 | </div> |
97 | |||
98 | <ng-container *ngIf="prependModerationActions"> | ||
99 | <my-comment-report #commentReportModal [comment]="comment"></my-comment-report> | ||
100 | </ng-container> | ||
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts index 27846c1ad..6744a0954 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts +++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts | |||
@@ -1,10 +1,12 @@ | |||
1 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' | 1 | |
2 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { MarkdownService, Notifier, UserService } from '@app/core' | 3 | import { MarkdownService, Notifier, UserService } from '@app/core' |
3 | import { AuthService } from '@app/core/auth' | 4 | import { AuthService } from '@app/core/auth' |
4 | import { Account, Actor, Video } from '@app/shared/shared-main' | 5 | import { Account, Actor, DropdownAction, Video } from '@app/shared/shared-main' |
6 | import { CommentReportComponent } from '@app/shared/shared-moderation/report-modals/comment-report.component' | ||
7 | import { VideoComment, VideoCommentThreadTree } from '@app/shared/shared-video-comment' | ||
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { User, UserRight } from '@shared/models' | 9 | import { User, UserRight } from '@shared/models' |
6 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' | ||
7 | import { VideoComment } from './video-comment.model' | ||
8 | 10 | ||
9 | @Component({ | 11 | @Component({ |
10 | selector: 'my-video-comment', | 12 | selector: 'my-video-comment', |
@@ -12,6 +14,8 @@ import { VideoComment } from './video-comment.model' | |||
12 | styleUrls: ['./video-comment.component.scss'] | 14 | styleUrls: ['./video-comment.component.scss'] |
13 | }) | 15 | }) |
14 | export class VideoCommentComponent implements OnInit, OnChanges { | 16 | export class VideoCommentComponent implements OnInit, OnChanges { |
17 | @ViewChild('commentReportModal') commentReportModal: CommentReportComponent | ||
18 | |||
15 | @Input() video: Video | 19 | @Input() video: Video |
16 | @Input() comment: VideoComment | 20 | @Input() comment: VideoComment |
17 | @Input() parentComments: VideoComment[] = [] | 21 | @Input() parentComments: VideoComment[] = [] |
@@ -26,6 +30,8 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
26 | @Output() resetReply = new EventEmitter() | 30 | @Output() resetReply = new EventEmitter() |
27 | @Output() timestampClicked = new EventEmitter<number>() | 31 | @Output() timestampClicked = new EventEmitter<number>() |
28 | 32 | ||
33 | prependModerationActions: DropdownAction<any>[] | ||
34 | |||
29 | sanitizedCommentHTML = '' | 35 | sanitizedCommentHTML = '' |
30 | newParentComments: VideoComment[] = [] | 36 | newParentComments: VideoComment[] = [] |
31 | 37 | ||
@@ -33,6 +39,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
33 | commentUser: User | 39 | commentUser: User |
34 | 40 | ||
35 | constructor ( | 41 | constructor ( |
42 | private i18n: I18n, | ||
36 | private markdownService: MarkdownService, | 43 | private markdownService: MarkdownService, |
37 | private authService: AuthService, | 44 | private authService: AuthService, |
38 | private userService: UserService, | 45 | private userService: UserService, |
@@ -127,5 +134,20 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
127 | } else { | 134 | } else { |
128 | this.comment.account = null | 135 | this.comment.account = null |
129 | } | 136 | } |
137 | |||
138 | if (this.isUserLoggedIn() && this.authService.getUser().account.id !== this.comment.account.id) { | ||
139 | this.prependModerationActions = [ | ||
140 | { | ||
141 | label: this.i18n('Report comment'), | ||
142 | handler: () => this.showReportModal() | ||
143 | } | ||
144 | ] | ||
145 | } else { | ||
146 | this.prependModerationActions = undefined | ||
147 | } | ||
148 | } | ||
149 | |||
150 | private showReportModal () { | ||
151 | this.commentReportModal.show() | ||
130 | } | 152 | } |
131 | } | 153 | } |
diff --git a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts index df0018ec6..66494a20a 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts | |||
@@ -4,10 +4,8 @@ import { ActivatedRoute } from '@angular/router' | |||
4 | import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, User } from '@app/core' | 4 | import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, User } from '@app/core' |
5 | import { HooksService } from '@app/core/plugins/hooks.service' | 5 | import { HooksService } from '@app/core/plugins/hooks.service' |
6 | import { Syndication, VideoDetails } from '@app/shared/shared-main' | 6 | import { Syndication, VideoDetails } from '@app/shared/shared-main' |
7 | import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' | ||
9 | import { VideoComment } from './video-comment.model' | ||
10 | import { VideoCommentService } from './video-comment.service' | ||
11 | 9 | ||
12 | @Component({ | 10 | @Component({ |
13 | selector: 'my-video-comments', | 11 | selector: 'my-video-comments', |
diff --git a/client/src/app/+videos/+video-watch/video-watch.module.ts b/client/src/app/+videos/+video-watch/video-watch.module.ts index 421170d81..5821dc2b7 100644 --- a/client/src/app/+videos/+video-watch/video-watch.module.ts +++ b/client/src/app/+videos/+video-watch/video-watch.module.ts | |||
@@ -5,16 +5,17 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons' | |||
5 | import { SharedMainModule } from '@app/shared/shared-main' | 5 | import { SharedMainModule } from '@app/shared/shared-main' |
6 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 6 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
7 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 7 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
8 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' | ||
8 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | 9 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' |
9 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' | 10 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' |
10 | import { RecommendationsModule } from './recommendations/recommendations.module' | ||
11 | import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' | 11 | import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' |
12 | import { VideoCommentService } from '../../shared/shared-video-comment/video-comment.service' | ||
12 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' | 13 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' |
13 | import { VideoCommentComponent } from './comment/video-comment.component' | 14 | import { VideoCommentComponent } from './comment/video-comment.component' |
14 | import { VideoCommentService } from './comment/video-comment.service' | ||
15 | import { VideoCommentsComponent } from './comment/video-comments.component' | 15 | import { VideoCommentsComponent } from './comment/video-comments.component' |
16 | import { VideoShareComponent } from './modal/video-share.component' | 16 | import { VideoShareComponent } from './modal/video-share.component' |
17 | import { VideoSupportComponent } from './modal/video-support.component' | 17 | import { VideoSupportComponent } from './modal/video-support.component' |
18 | import { RecommendationsModule } from './recommendations/recommendations.module' | ||
18 | import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' | 19 | import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' |
19 | import { VideoDurationPipe } from './video-duration-formatter.pipe' | 20 | import { VideoDurationPipe } from './video-duration-formatter.pipe' |
20 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' | 21 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' |
@@ -34,7 +35,8 @@ import { VideoWatchComponent } from './video-watch.component' | |||
34 | SharedVideoPlaylistModule, | 35 | SharedVideoPlaylistModule, |
35 | SharedUserSubscriptionModule, | 36 | SharedUserSubscriptionModule, |
36 | SharedModerationModule, | 37 | SharedModerationModule, |
37 | SharedGlobalIconModule | 38 | SharedGlobalIconModule, |
39 | SharedVideoCommentModule | ||
38 | ], | 40 | ], |
39 | 41 | ||
40 | declarations: [ | 42 | declarations: [ |
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts index 1b35ad47d..e6328eddc 100644 --- a/client/src/app/core/rest/rest-table.ts +++ b/client/src/app/core/rest/rest-table.ts | |||
@@ -3,6 +3,9 @@ import { LazyLoadEvent, SortMeta } from 'primeng/api' | |||
3 | import { RestPagination } from './rest-pagination' | 3 | import { RestPagination } from './rest-pagination' |
4 | import { Subject } from 'rxjs' | 4 | import { Subject } from 'rxjs' |
5 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 5 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
6 | import * as debug from 'debug' | ||
7 | |||
8 | const logger = debug('peertube:tables:RestTable') | ||
6 | 9 | ||
7 | export abstract class RestTable { | 10 | export abstract class RestTable { |
8 | 11 | ||
@@ -15,7 +18,7 @@ export abstract class RestTable { | |||
15 | rowsPerPage = this.rowsPerPageOptions[0] | 18 | rowsPerPage = this.rowsPerPageOptions[0] |
16 | expandedRows = {} | 19 | expandedRows = {} |
17 | 20 | ||
18 | private searchStream: Subject<string> | 21 | protected searchStream: Subject<string> |
19 | 22 | ||
20 | abstract getIdentifier (): string | 23 | abstract getIdentifier (): string |
21 | 24 | ||
@@ -37,6 +40,8 @@ export abstract class RestTable { | |||
37 | } | 40 | } |
38 | 41 | ||
39 | loadLazy (event: LazyLoadEvent) { | 42 | loadLazy (event: LazyLoadEvent) { |
43 | logger('Load lazy %o.', event) | ||
44 | |||
40 | this.sort = { | 45 | this.sort = { |
41 | order: event.sortOrder, | 46 | order: event.sortOrder, |
42 | field: event.sortField | 47 | field: event.sortField |
@@ -65,6 +70,9 @@ export abstract class RestTable { | |||
65 | ) | 70 | ) |
66 | .subscribe(search => { | 71 | .subscribe(search => { |
67 | this.search = search | 72 | this.search = search |
73 | |||
74 | logger('On search %s.', this.search) | ||
75 | |||
68 | this.loadData() | 76 | this.loadData() |
69 | }) | 77 | }) |
70 | } | 78 | } |
@@ -75,14 +83,18 @@ export abstract class RestTable { | |||
75 | } | 83 | } |
76 | 84 | ||
77 | onPage (event: { first: number, rows: number }) { | 85 | onPage (event: { first: number, rows: number }) { |
86 | logger('On page %o.', event) | ||
87 | |||
78 | if (this.rowsPerPage !== event.rows) { | 88 | if (this.rowsPerPage !== event.rows) { |
79 | this.rowsPerPage = event.rows | 89 | this.rowsPerPage = event.rows |
80 | this.pagination = { | 90 | this.pagination = { |
81 | start: event.first, | 91 | start: event.first, |
82 | count: this.rowsPerPage | 92 | count: this.rowsPerPage |
83 | } | 93 | } |
94 | |||
84 | this.loadData() | 95 | this.loadData() |
85 | } | 96 | } |
97 | |||
86 | this.expandedRows = {} | 98 | this.expandedRows = {} |
87 | } | 99 | } |
88 | 100 | ||
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index 8ecdf9fcd..31b9c2152 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -51,12 +51,14 @@ export class User implements UserServerModel { | |||
51 | videoQuotaDaily: number | 51 | videoQuotaDaily: number |
52 | videoQuotaUsed?: number | 52 | videoQuotaUsed?: number |
53 | videoQuotaUsedDaily?: number | 53 | videoQuotaUsedDaily?: number |
54 | |||
54 | videosCount?: number | 55 | videosCount?: number |
55 | videoAbusesCount?: number | ||
56 | videoAbusesAcceptedCount?: number | ||
57 | videoAbusesCreatedCount?: number | ||
58 | videoCommentsCount?: number | 56 | videoCommentsCount?: number |
59 | 57 | ||
58 | abusesCount?: number | ||
59 | abusesAcceptedCount?: number | ||
60 | abusesCreatedCount?: number | ||
61 | |||
60 | theme: string | 62 | theme: string |
61 | 63 | ||
62 | account: Account | 64 | account: Account |
@@ -89,9 +91,9 @@ export class User implements UserServerModel { | |||
89 | this.videoQuotaUsed = hash.videoQuotaUsed | 91 | this.videoQuotaUsed = hash.videoQuotaUsed |
90 | this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily | 92 | this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily |
91 | this.videosCount = hash.videosCount | 93 | this.videosCount = hash.videosCount |
92 | this.videoAbusesCount = hash.videoAbusesCount | 94 | this.abusesCount = hash.abusesCount |
93 | this.videoAbusesAcceptedCount = hash.videoAbusesAcceptedCount | 95 | this.abusesAcceptedCount = hash.abusesAcceptedCount |
94 | this.videoAbusesCreatedCount = hash.videoAbusesCreatedCount | 96 | this.abusesCreatedCount = hash.abusesCreatedCount |
95 | this.videoCommentsCount = hash.videoCommentsCount | 97 | this.videoCommentsCount = hash.videoCommentsCount |
96 | 98 | ||
97 | this.nsfwPolicy = hash.nsfwPolicy | 99 | this.nsfwPolicy = hash.nsfwPolicy |
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 { | |||
28 | private routesPerRight: { [ role in UserRight ]?: string } = { | 28 | private routesPerRight: { [ role in UserRight ]?: string } = { |
29 | [UserRight.MANAGE_USERS]: '/admin/users', | 29 | [UserRight.MANAGE_USERS]: '/admin/users', |
30 | [UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends', | 30 | [UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends', |
31 | [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/moderation/video-abuses', | 31 | [UserRight.MANAGE_ABUSES]: '/admin/moderation/abuses', |
32 | [UserRight.MANAGE_VIDEO_BLACKLIST]: '/admin/moderation/video-blocks', | 32 | [UserRight.MANAGE_VIDEO_BLACKLIST]: '/admin/moderation/video-blocks', |
33 | [UserRight.MANAGE_JOBS]: '/admin/jobs', | 33 | [UserRight.MANAGE_JOBS]: '/admin/jobs', |
34 | [UserRight.MANAGE_CONFIGURATION]: '/admin/config' | 34 | [UserRight.MANAGE_CONFIGURATION]: '/admin/config' |
@@ -126,7 +126,7 @@ export class MenuComponent implements OnInit { | |||
126 | const adminRights = [ | 126 | const adminRights = [ |
127 | UserRight.MANAGE_USERS, | 127 | UserRight.MANAGE_USERS, |
128 | UserRight.MANAGE_SERVER_FOLLOW, | 128 | UserRight.MANAGE_SERVER_FOLLOW, |
129 | UserRight.MANAGE_VIDEO_ABUSES, | 129 | UserRight.MANAGE_ABUSES, |
130 | UserRight.MANAGE_VIDEO_BLACKLIST, | 130 | UserRight.MANAGE_VIDEO_BLACKLIST, |
131 | UserRight.MANAGE_JOBS, | 131 | UserRight.MANAGE_JOBS, |
132 | UserRight.MANAGE_CONFIGURATION | 132 | 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 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' | |||
4 | import { BuildFormValidator } from './form-validator.service' | 4 | import { BuildFormValidator } from './form-validator.service' |
5 | 5 | ||
6 | @Injectable() | 6 | @Injectable() |
7 | export class VideoAbuseValidatorsService { | 7 | export class AbuseValidatorsService { |
8 | readonly VIDEO_ABUSE_REASON: BuildFormValidator | 8 | readonly ABUSE_REASON: BuildFormValidator |
9 | readonly VIDEO_ABUSE_MODERATION_COMMENT: BuildFormValidator | 9 | readonly ABUSE_MODERATION_COMMENT: BuildFormValidator |
10 | 10 | ||
11 | constructor (private i18n: I18n) { | 11 | constructor (private i18n: I18n) { |
12 | this.VIDEO_ABUSE_REASON = { | 12 | this.ABUSE_REASON = { |
13 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | 13 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], |
14 | MESSAGES: { | 14 | MESSAGES: { |
15 | 'required': this.i18n('Report reason is required.'), | 15 | 'required': this.i18n('Report reason is required.'), |
@@ -18,7 +18,7 @@ export class VideoAbuseValidatorsService { | |||
18 | } | 18 | } |
19 | } | 19 | } |
20 | 20 | ||
21 | this.VIDEO_ABUSE_MODERATION_COMMENT = { | 21 | this.ABUSE_MODERATION_COMMENT = { |
22 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | 22 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], |
23 | MESSAGES: { | 23 | MESSAGES: { |
24 | 'required': this.i18n('Moderation comment is required.'), | 24 | '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 @@ | |||
1 | export * from './abuse-validators.service' | ||
1 | export * from './batch-domains-validators.service' | 2 | export * from './batch-domains-validators.service' |
2 | export * from './custom-config-validators.service' | 3 | export * from './custom-config-validators.service' |
3 | export * from './form-validator.service' | 4 | export * from './form-validator.service' |
@@ -6,7 +7,6 @@ export * from './instance-validators.service' | |||
6 | export * from './login-validators.service' | 7 | export * from './login-validators.service' |
7 | export * from './reset-password-validators.service' | 8 | export * from './reset-password-validators.service' |
8 | export * from './user-validators.service' | 9 | export * from './user-validators.service' |
9 | export * from './video-abuse-validators.service' | ||
10 | export * from './video-accept-ownership-validators.service' | 10 | export * from './video-accept-ownership-validators.service' |
11 | export * from './video-block-validators.service' | 11 | export * from './video-block-validators.service' |
12 | export * from './video-captions-validators.service' | 12 | 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 { | |||
11 | LoginValidatorsService, | 11 | LoginValidatorsService, |
12 | ResetPasswordValidatorsService, | 12 | ResetPasswordValidatorsService, |
13 | UserValidatorsService, | 13 | UserValidatorsService, |
14 | VideoAbuseValidatorsService, | 14 | AbuseValidatorsService, |
15 | VideoAcceptOwnershipValidatorsService, | 15 | VideoAcceptOwnershipValidatorsService, |
16 | VideoBlockValidatorsService, | 16 | VideoBlockValidatorsService, |
17 | VideoCaptionsValidatorsService, | 17 | VideoCaptionsValidatorsService, |
@@ -69,7 +69,7 @@ import { TimestampInputComponent } from './timestamp-input.component' | |||
69 | LoginValidatorsService, | 69 | LoginValidatorsService, |
70 | ResetPasswordValidatorsService, | 70 | ResetPasswordValidatorsService, |
71 | UserValidatorsService, | 71 | UserValidatorsService, |
72 | VideoAbuseValidatorsService, | 72 | AbuseValidatorsService, |
73 | VideoAcceptOwnershipValidatorsService, | 73 | VideoAcceptOwnershipValidatorsService, |
74 | VideoBlockValidatorsService, | 74 | VideoBlockValidatorsService, |
75 | VideoCaptionsValidatorsService, | 75 | 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..bda88bdee 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,9 @@ export abstract class Actor implements ActorServer { | |||
14 | 14 | ||
15 | avatarUrl: string | 15 | avatarUrl: string |
16 | 16 | ||
17 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: Avatar }) { | 17 | isLocal: boolean |
18 | |||
19 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { | ||
18 | if (actor?.avatar?.url) return actor.avatar.url | 20 | if (actor?.avatar?.url) return actor.avatar.url |
19 | 21 | ||
20 | if (actor && actor.avatar) { | 22 | if (actor && actor.avatar) { |
@@ -46,10 +48,16 @@ export abstract class Actor implements ActorServer { | |||
46 | this.host = hash.host | 48 | this.host = hash.host |
47 | this.followingCount = hash.followingCount | 49 | this.followingCount = hash.followingCount |
48 | this.followersCount = hash.followersCount | 50 | this.followersCount = hash.followersCount |
49 | this.createdAt = new Date(hash.createdAt.toString()) | 51 | |
50 | this.updatedAt = new Date(hash.updatedAt.toString()) | 52 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) |
53 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
54 | |||
51 | this.avatar = hash.avatar | 55 | this.avatar = hash.avatar |
52 | 56 | ||
57 | const absoluteAPIUrl = getAbsoluteAPIUrl() | ||
58 | const thisHost = new URL(absoluteAPIUrl).host | ||
59 | this.isLocal = this.host.trim() === thisHost | ||
60 | |||
53 | this.updateComputedAttributes() | 61 | this.updateComputedAttributes() |
54 | } | 62 | } |
55 | 63 | ||
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..61b48a806 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,22 @@ export class UserNotification implements UserNotificationServer { | |||
25 | video: VideoInfo | 25 | video: VideoInfo |
26 | } | 26 | } |
27 | 27 | ||
28 | videoAbuse?: { | 28 | abuse?: { |
29 | id: number | 29 | id: number |
30 | video: VideoInfo | 30 | |
31 | video?: VideoInfo | ||
32 | |||
33 | comment?: { | ||
34 | threadId: number | ||
35 | |||
36 | video: { | ||
37 | id: number | ||
38 | uuid: string | ||
39 | name: string | ||
40 | } | ||
41 | } | ||
42 | |||
43 | account?: ActorInfo | ||
31 | } | 44 | } |
32 | 45 | ||
33 | videoBlacklist?: { | 46 | videoBlacklist?: { |
@@ -55,7 +68,7 @@ export class UserNotification implements UserNotificationServer { | |||
55 | // Additional fields | 68 | // Additional fields |
56 | videoUrl?: string | 69 | videoUrl?: string |
57 | commentUrl?: any[] | 70 | commentUrl?: any[] |
58 | videoAbuseUrl?: string | 71 | abuseUrl?: string |
59 | videoAutoBlacklistUrl?: string | 72 | videoAutoBlacklistUrl?: string |
60 | accountUrl?: string | 73 | accountUrl?: string |
61 | videoImportIdentifier?: string | 74 | videoImportIdentifier?: string |
@@ -78,7 +91,7 @@ export class UserNotification implements UserNotificationServer { | |||
78 | this.comment = hash.comment | 91 | this.comment = hash.comment |
79 | if (this.comment) this.setAvatarUrl(this.comment.account) | 92 | if (this.comment) this.setAvatarUrl(this.comment.account) |
80 | 93 | ||
81 | this.videoAbuse = hash.videoAbuse | 94 | this.abuse = hash.abuse |
82 | 95 | ||
83 | this.videoBlacklist = hash.videoBlacklist | 96 | this.videoBlacklist = hash.videoBlacklist |
84 | 97 | ||
@@ -104,12 +117,15 @@ export class UserNotification implements UserNotificationServer { | |||
104 | case UserNotificationType.COMMENT_MENTION: | 117 | case UserNotificationType.COMMENT_MENTION: |
105 | if (!this.comment) break | 118 | if (!this.comment) break |
106 | this.accountUrl = this.buildAccountUrl(this.comment.account) | 119 | this.accountUrl = this.buildAccountUrl(this.comment.account) |
107 | this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] | 120 | this.commentUrl = this.buildCommentUrl(this.comment) |
108 | break | 121 | break |
109 | 122 | ||
110 | case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS: | 123 | case UserNotificationType.NEW_ABUSE_FOR_MODERATORS: |
111 | this.videoAbuseUrl = '/admin/moderation/video-abuses/list' | 124 | this.abuseUrl = '/admin/moderation/abuses/list' |
112 | this.videoUrl = this.buildVideoUrl(this.videoAbuse.video) | 125 | |
126 | if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) | ||
127 | else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment) | ||
128 | else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account) | ||
113 | break | 129 | break |
114 | 130 | ||
115 | case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: | 131 | case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: |
@@ -178,7 +194,11 @@ export class UserNotification implements UserNotificationServer { | |||
178 | return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName | 194 | return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName |
179 | } | 195 | } |
180 | 196 | ||
181 | private setAvatarUrl (actor: { avatarUrl?: string, avatar?: Avatar }) { | 197 | private buildCommentUrl (comment: { video: { uuid: string }, threadId: number }) { |
198 | return [ this.buildVideoUrl(comment.video), { threadId: comment.threadId } ] | ||
199 | } | ||
200 | |||
201 | private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) { | ||
182 | actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) | 202 | actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) |
183 | } | 203 | } |
184 | } | 204 | } |
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..8127ae979 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 @@ | |||
19 | 19 | ||
20 | <ng-template #noVideo> | 20 | <ng-template #noVideo> |
21 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> | 21 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> |
22 | 22 | ||
23 | <div class="message" i18n> | 23 | <div class="message" i18n> |
24 | The notification concerns a video now unavailable | 24 | The notification concerns a video now unavailable |
25 | </div> | 25 | </div> |
@@ -42,11 +42,24 @@ | |||
42 | </div> | 42 | </div> |
43 | </ng-container> | 43 | </ng-container> |
44 | 44 | ||
45 | <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS"> | 45 | <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS"> |
46 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> | 46 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> |
47 | 47 | ||
48 | <div class="message" i18n> | 48 | <div class="message" *ngIf="notification.videoUrl" i18n> |
49 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a> | 49 | <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a> |
50 | </div> | ||
51 | |||
52 | <div class="message" *ngIf="notification.commentUrl" i18n> | ||
53 | <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new comment abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.abuse.comment.video.name }}</a> | ||
54 | </div> | ||
55 | |||
56 | <div class="message" *ngIf="notification.accountUrl" i18n> | ||
57 | <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new account abuse</a> has been created on account <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.abuse.account.displayName }}</a> | ||
58 | </div> | ||
59 | |||
60 | <!-- Deleted entity associated to the abuse --> | ||
61 | <div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n> | ||
62 | <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new abuse</a> has been created | ||
50 | </div> | 63 | </div> |
51 | </ng-container> | 64 | </ng-container> |
52 | 65 | ||
@@ -65,7 +78,7 @@ | |||
65 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> | 78 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> |
66 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> | 79 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> |
67 | </a> | 80 | </a> |
68 | 81 | ||
69 | <div class="message" i18n> | 82 | <div class="message" i18n> |
70 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a> | 83 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a> |
71 | </div> | 84 | </div> |
@@ -73,7 +86,7 @@ | |||
73 | 86 | ||
74 | <ng-template #noComment> | 87 | <ng-template #noComment> |
75 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> | 88 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> |
76 | 89 | ||
77 | <div class="message" i18n> | 90 | <div class="message" i18n> |
78 | The notification concerns a comment now unavailable | 91 | The notification concerns a comment now unavailable |
79 | </div> | 92 | </div> |
diff --git a/client/src/app/shared/shared-moderation/abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts new file mode 100644 index 000000000..95ac16955 --- /dev/null +++ b/client/src/app/shared/shared-moderation/abuse.service.ts | |||
@@ -0,0 +1,154 @@ | |||
1 | import { omit } from 'lodash-es' | ||
2 | import { SortMeta } from 'primeng/api' | ||
3 | import { Observable } from 'rxjs' | ||
4 | import { catchError, map } from 'rxjs/operators' | ||
5 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
6 | import { Injectable } from '@angular/core' | ||
7 | import { RestExtractor, RestPagination, RestService } from '@app/core' | ||
8 | import { Abuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList } from '@shared/models' | ||
9 | import { environment } from '../../../environments/environment' | ||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
11 | |||
12 | @Injectable() | ||
13 | export class AbuseService { | ||
14 | private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses' | ||
15 | |||
16 | constructor ( | ||
17 | private i18n: I18n, | ||
18 | private authHttp: HttpClient, | ||
19 | private restService: RestService, | ||
20 | private restExtractor: RestExtractor | ||
21 | ) { } | ||
22 | |||
23 | getAbuses (options: { | ||
24 | pagination: RestPagination, | ||
25 | sort: SortMeta, | ||
26 | search?: string | ||
27 | }): Observable<ResultList<Abuse>> { | ||
28 | const { pagination, sort, search } = options | ||
29 | const url = AbuseService.BASE_ABUSE_URL | ||
30 | |||
31 | let params = new HttpParams() | ||
32 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
33 | |||
34 | if (search) { | ||
35 | const filters = this.restService.parseQueryStringFilter(search, { | ||
36 | id: { prefix: '#' }, | ||
37 | state: { | ||
38 | prefix: 'state:', | ||
39 | handler: v => { | ||
40 | if (v === 'accepted') return AbuseState.ACCEPTED | ||
41 | if (v === 'pending') return AbuseState.PENDING | ||
42 | if (v === 'rejected') return AbuseState.REJECTED | ||
43 | |||
44 | return undefined | ||
45 | } | ||
46 | }, | ||
47 | videoIs: { | ||
48 | prefix: 'videoIs:', | ||
49 | handler: v => { | ||
50 | if (v === 'deleted') return v | ||
51 | if (v === 'blacklisted') return v | ||
52 | |||
53 | return undefined | ||
54 | } | ||
55 | }, | ||
56 | searchReporter: { prefix: 'reporter:' }, | ||
57 | searchReportee: { prefix: 'reportee:' }, | ||
58 | predefinedReason: { prefix: 'tag:' } | ||
59 | }) | ||
60 | |||
61 | params = this.restService.addObjectParams(params, filters) | ||
62 | } | ||
63 | |||
64 | return this.authHttp.get<ResultList<Abuse>>(url, { params }) | ||
65 | .pipe( | ||
66 | catchError(res => this.restExtractor.handleError(res)) | ||
67 | ) | ||
68 | } | ||
69 | |||
70 | reportVideo (parameters: AbuseCreate) { | ||
71 | const url = AbuseService.BASE_ABUSE_URL | ||
72 | |||
73 | const body = omit(parameters, ['id']) | ||
74 | |||
75 | return this.authHttp.post(url, body) | ||
76 | .pipe( | ||
77 | map(this.restExtractor.extractDataBool), | ||
78 | catchError(res => this.restExtractor.handleError(res)) | ||
79 | ) | ||
80 | } | ||
81 | |||
82 | updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) { | ||
83 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id | ||
84 | |||
85 | return this.authHttp.put(url, abuseUpdate) | ||
86 | .pipe( | ||
87 | map(this.restExtractor.extractDataBool), | ||
88 | catchError(res => this.restExtractor.handleError(res)) | ||
89 | ) | ||
90 | } | ||
91 | |||
92 | removeAbuse (abuse: Abuse) { | ||
93 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id | ||
94 | |||
95 | return this.authHttp.delete(url) | ||
96 | .pipe( | ||
97 | map(this.restExtractor.extractDataBool), | ||
98 | catchError(res => this.restExtractor.handleError(res)) | ||
99 | ) | ||
100 | } | ||
101 | |||
102 | getPrefefinedReasons (type: AbuseFilter) { | ||
103 | let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [ | ||
104 | { | ||
105 | id: 'violentOrRepulsive', | ||
106 | label: this.i18n('Violent or repulsive'), | ||
107 | help: this.i18n('Contains offensive, violent, or coarse language or iconography.') | ||
108 | }, | ||
109 | { | ||
110 | id: 'hatefulOrAbusive', | ||
111 | label: this.i18n('Hateful or abusive'), | ||
112 | help: this.i18n('Contains abusive, racist or sexist language or iconography.') | ||
113 | }, | ||
114 | { | ||
115 | id: 'spamOrMisleading', | ||
116 | label: this.i18n('Spam, ad or false news'), | ||
117 | help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') | ||
118 | }, | ||
119 | { | ||
120 | id: 'privacy', | ||
121 | label: this.i18n('Privacy breach or doxxing'), | ||
122 | help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).') | ||
123 | }, | ||
124 | { | ||
125 | id: 'rights', | ||
126 | label: this.i18n('Intellectual property violation'), | ||
127 | help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') | ||
128 | }, | ||
129 | { | ||
130 | id: 'serverRules', | ||
131 | label: this.i18n('Breaks server rules'), | ||
132 | description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.') | ||
133 | } | ||
134 | ] | ||
135 | |||
136 | if (type === 'video') { | ||
137 | reasons = reasons.concat([ | ||
138 | { | ||
139 | id: 'thumbnails', | ||
140 | label: this.i18n('Thumbnails'), | ||
141 | help: this.i18n('The above can only be seen in thumbnails.') | ||
142 | }, | ||
143 | { | ||
144 | id: 'captions', | ||
145 | label: this.i18n('Captions'), | ||
146 | help: this.i18n('The above can only be seen in captions (please describe which).') | ||
147 | } | ||
148 | ]) | ||
149 | } | ||
150 | |||
151 | return reasons | ||
152 | } | ||
153 | |||
154 | } | ||
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts index 8e74254f6..41c910ffe 100644 --- a/client/src/app/shared/shared-moderation/index.ts +++ b/client/src/app/shared/shared-moderation/index.ts | |||
@@ -1,3 +1,6 @@ | |||
1 | export * from './report-modals' | ||
2 | |||
3 | export * from './abuse.service' | ||
1 | export * from './account-block.model' | 4 | export * from './account-block.model' |
2 | export * from './account-blocklist.component' | 5 | export * from './account-blocklist.component' |
3 | export * from './batch-domains-modal.component' | 6 | export * from './batch-domains-modal.component' |
@@ -6,8 +9,6 @@ export * from './bulk.service' | |||
6 | export * from './server-blocklist.component' | 9 | export * from './server-blocklist.component' |
7 | export * from './user-ban-modal.component' | 10 | export * from './user-ban-modal.component' |
8 | export * from './user-moderation-dropdown.component' | 11 | export * from './user-moderation-dropdown.component' |
9 | export * from './video-abuse.service' | ||
10 | export * from './video-block.component' | 12 | export * from './video-block.component' |
11 | export * from './video-block.service' | 13 | export * from './video-block.service' |
12 | export * from './video-report.component' | ||
13 | export * from './shared-moderation.module' | 14 | export * from './shared-moderation.module' |
diff --git a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts new file mode 100644 index 000000000..78ca934c7 --- /dev/null +++ b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts | |||
@@ -0,0 +1,94 @@ | |||
1 | import { mapValues, pickBy } from 'lodash-es' | ||
2 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | ||
3 | import { Notifier } from '@app/core' | ||
4 | import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
5 | import { Account } from '@app/shared/shared-main' | ||
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
9 | import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models' | ||
10 | import { AbuseService } from '../abuse.service' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-account-report', | ||
14 | templateUrl: './report.component.html', | ||
15 | styleUrls: [ './report.component.scss' ] | ||
16 | }) | ||
17 | export class AccountReportComponent extends FormReactive implements OnInit { | ||
18 | @Input() account: Account = null | ||
19 | |||
20 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
21 | |||
22 | error: string = null | ||
23 | predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] | ||
24 | modalTitle: string | ||
25 | |||
26 | private openedModal: NgbModalRef | ||
27 | |||
28 | constructor ( | ||
29 | protected formValidatorService: FormValidatorService, | ||
30 | private modalService: NgbModal, | ||
31 | private abuseValidatorsService: AbuseValidatorsService, | ||
32 | private abuseService: AbuseService, | ||
33 | private notifier: Notifier, | ||
34 | private i18n: I18n | ||
35 | ) { | ||
36 | super() | ||
37 | } | ||
38 | |||
39 | get currentHost () { | ||
40 | return window.location.host | ||
41 | } | ||
42 | |||
43 | get originHost () { | ||
44 | if (this.isRemote()) { | ||
45 | return this.account.host | ||
46 | } | ||
47 | |||
48 | return '' | ||
49 | } | ||
50 | |||
51 | ngOnInit () { | ||
52 | this.modalTitle = this.i18n('Report {{displayName}}', { displayName: this.account.displayName }) | ||
53 | |||
54 | this.buildForm({ | ||
55 | reason: this.abuseValidatorsService.ABUSE_REASON, | ||
56 | predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null) | ||
57 | }) | ||
58 | |||
59 | this.predefinedReasons = this.abuseService.getPrefefinedReasons('account') | ||
60 | } | ||
61 | |||
62 | show () { | ||
63 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' }) | ||
64 | } | ||
65 | |||
66 | hide () { | ||
67 | this.openedModal.close() | ||
68 | this.openedModal = null | ||
69 | } | ||
70 | |||
71 | report () { | ||
72 | const reason = this.form.get('reason').value | ||
73 | const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[] | ||
74 | |||
75 | this.abuseService.reportVideo({ | ||
76 | reason, | ||
77 | predefinedReasons, | ||
78 | account: { | ||
79 | id: this.account.id | ||
80 | } | ||
81 | }).subscribe( | ||
82 | () => { | ||
83 | this.notifier.success(this.i18n('Account reported.')) | ||
84 | this.hide() | ||
85 | }, | ||
86 | |||
87 | err => this.notifier.error(err.message) | ||
88 | ) | ||
89 | } | ||
90 | |||
91 | isRemote () { | ||
92 | return !this.account.isLocal | ||
93 | } | ||
94 | } | ||
diff --git a/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts new file mode 100644 index 000000000..00d7b8d34 --- /dev/null +++ b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts | |||
@@ -0,0 +1,94 @@ | |||
1 | import { mapValues, pickBy } from 'lodash-es' | ||
2 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | ||
3 | import { Notifier } from '@app/core' | ||
4 | import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
5 | import { VideoComment } from '@app/shared/shared-video-comment' | ||
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
9 | import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models' | ||
10 | import { AbuseService } from '../abuse.service' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-comment-report', | ||
14 | templateUrl: './report.component.html', | ||
15 | styleUrls: [ './report.component.scss' ] | ||
16 | }) | ||
17 | export class CommentReportComponent extends FormReactive implements OnInit { | ||
18 | @Input() comment: VideoComment = null | ||
19 | |||
20 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
21 | |||
22 | modalTitle: string | ||
23 | error: string = null | ||
24 | predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] | ||
25 | |||
26 | private openedModal: NgbModalRef | ||
27 | |||
28 | constructor ( | ||
29 | protected formValidatorService: FormValidatorService, | ||
30 | private modalService: NgbModal, | ||
31 | private abuseValidatorsService: AbuseValidatorsService, | ||
32 | private abuseService: AbuseService, | ||
33 | private notifier: Notifier, | ||
34 | private i18n: I18n | ||
35 | ) { | ||
36 | super() | ||
37 | } | ||
38 | |||
39 | get currentHost () { | ||
40 | return window.location.host | ||
41 | } | ||
42 | |||
43 | get originHost () { | ||
44 | if (this.isRemote()) { | ||
45 | return this.comment.account.host | ||
46 | } | ||
47 | |||
48 | return '' | ||
49 | } | ||
50 | |||
51 | ngOnInit () { | ||
52 | this.modalTitle = this.i18n('Report comment') | ||
53 | |||
54 | this.buildForm({ | ||
55 | reason: this.abuseValidatorsService.ABUSE_REASON, | ||
56 | predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null) | ||
57 | }) | ||
58 | |||
59 | this.predefinedReasons = this.abuseService.getPrefefinedReasons('comment') | ||
60 | } | ||
61 | |||
62 | show () { | ||
63 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' }) | ||
64 | } | ||
65 | |||
66 | hide () { | ||
67 | this.openedModal.close() | ||
68 | this.openedModal = null | ||
69 | } | ||
70 | |||
71 | report () { | ||
72 | const reason = this.form.get('reason').value | ||
73 | const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[] | ||
74 | |||
75 | this.abuseService.reportVideo({ | ||
76 | reason, | ||
77 | predefinedReasons, | ||
78 | comment: { | ||
79 | id: this.comment.id | ||
80 | } | ||
81 | }).subscribe( | ||
82 | () => { | ||
83 | this.notifier.success(this.i18n('Comment reported.')) | ||
84 | this.hide() | ||
85 | }, | ||
86 | |||
87 | err => this.notifier.error(err.message) | ||
88 | ) | ||
89 | } | ||
90 | |||
91 | isRemote () { | ||
92 | return !this.comment.isLocal | ||
93 | } | ||
94 | } | ||
diff --git a/client/src/app/shared/shared-moderation/report-modals/index.ts b/client/src/app/shared/shared-moderation/report-modals/index.ts new file mode 100644 index 000000000..f3c4058ae --- /dev/null +++ b/client/src/app/shared/shared-moderation/report-modals/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './account-report.component' | ||
2 | export * from './comment-report.component' | ||
3 | export * from './video-report.component' | ||
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.html b/client/src/app/shared/shared-moderation/report-modals/report.component.html new file mode 100644 index 000000000..bda62312f --- /dev/null +++ b/client/src/app/shared/shared-moderation/report-modals/report.component.html | |||
@@ -0,0 +1,62 @@ | |||
1 | <ng-template #modal> | ||
2 | <div class="modal-header"> | ||
3 | <h4 class="modal-title">{{ modalTitle }}</h4> | ||
4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | ||
6 | |||
7 | <div class="modal-body"> | ||
8 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> | ||
9 | |||
10 | <div class="row"> | ||
11 | <div class="col-5 form-group"> | ||
12 | |||
13 | <label i18n for="reportPredefinedReasons">What is the issue?</label> | ||
14 | |||
15 | <div class="ml-2 mt-2 d-flex flex-column"> | ||
16 | <ng-container formGroupName="predefinedReasons"> | ||
17 | |||
18 | <div class="form-group" *ngFor="let reason of predefinedReasons"> | ||
19 | <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label"> | ||
20 | <ng-template *ngIf="reason.help" ptTemplate="help"> | ||
21 | <div [innerHTML]="reason.help"></div> | ||
22 | </ng-template> | ||
23 | |||
24 | <ng-container *ngIf="reason.description" ngProjectAs="description"> | ||
25 | <div [innerHTML]="reason.description"></div> | ||
26 | </ng-container> | ||
27 | </my-peertube-checkbox> | ||
28 | </div> | ||
29 | |||
30 | </ng-container> | ||
31 | </div> | ||
32 | |||
33 | </div> | ||
34 | |||
35 | <div class="col-7"> | ||
36 | <div i18n class="information"> | ||
37 | Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemote()"> and will be forwarded to the comment origin ({{ originHost }}) too</ng-container>. | ||
38 | </div> | ||
39 | |||
40 | <div class="form-group"> | ||
41 | <textarea | ||
42 | i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus | ||
43 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" | ||
44 | ></textarea> | ||
45 | <div *ngIf="formErrors.reason" class="form-error"> | ||
46 | {{ formErrors.reason }} | ||
47 | </div> | ||
48 | </div> | ||
49 | </div> | ||
50 | </div> | ||
51 | |||
52 | <div class="form-group inputs"> | ||
53 | <input | ||
54 | type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" | ||
55 | (click)="hide()" (key.enter)="hide()" | ||
56 | > | ||
57 | <input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid"> | ||
58 | </div> | ||
59 | |||
60 | </form> | ||
61 | </div> | ||
62 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-moderation/video-report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss index b2606cbd8..b2606cbd8 100644 --- a/client/src/app/shared/shared-moderation/video-report.component.scss +++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss | |||
diff --git a/client/src/app/shared/shared-moderation/video-report.component.html b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html index d6beb6d2a..4947088d1 100644 --- a/client/src/app/shared/shared-moderation/video-report.component.html +++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html | |||
@@ -14,16 +14,19 @@ | |||
14 | 14 | ||
15 | <div class="ml-2 mt-2 d-flex flex-column"> | 15 | <div class="ml-2 mt-2 d-flex flex-column"> |
16 | <ng-container formGroupName="predefinedReasons"> | 16 | <ng-container formGroupName="predefinedReasons"> |
17 | |||
17 | <div class="form-group" *ngFor="let reason of predefinedReasons"> | 18 | <div class="form-group" *ngFor="let reason of predefinedReasons"> |
18 | <my-peertube-checkbox formControlName="{{reason.id}}" labelText="{{reason.label}}"> | 19 | <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label"> |
19 | <ng-template *ngIf="reason.help" ptTemplate="help"> | 20 | <ng-template *ngIf="reason.help" ptTemplate="help"> |
20 | <div [innerHTML]="reason.help"></div> | 21 | <div [innerHTML]="reason.help"></div> |
21 | </ng-template> | 22 | </ng-template> |
23 | |||
22 | <ng-container *ngIf="reason.description" ngProjectAs="description"> | 24 | <ng-container *ngIf="reason.description" ngProjectAs="description"> |
23 | <div [innerHTML]="reason.description"></div> | 25 | <div [innerHTML]="reason.description"></div> |
24 | </ng-container> | 26 | </ng-container> |
25 | </my-peertube-checkbox> | 27 | </my-peertube-checkbox> |
26 | </div> | 28 | </div> |
29 | |||
27 | </ng-container> | 30 | </ng-container> |
28 | </div> | 31 | </div> |
29 | 32 | ||
@@ -69,11 +72,11 @@ | |||
69 | </div> | 72 | </div> |
70 | 73 | ||
71 | <div i18n class="information"> | 74 | <div i18n class="information"> |
72 | Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>. | 75 | Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemote()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>. |
73 | </div> | 76 | </div> |
74 | 77 | ||
75 | <div class="form-group"> | 78 | <div class="form-group"> |
76 | <textarea | 79 | <textarea |
77 | i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus | 80 | i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus |
78 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" | 81 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" |
79 | ></textarea> | 82 | ></textarea> |
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts new file mode 100644 index 000000000..7d53ea3c9 --- /dev/null +++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts | |||
@@ -0,0 +1,122 @@ | |||
1 | import { mapValues, pickBy } from 'lodash-es' | ||
2 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' | ||
3 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | ||
4 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | ||
5 | import { Notifier } from '@app/core' | ||
6 | import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
10 | import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models' | ||
11 | import { Video } from '../../shared-main' | ||
12 | import { AbuseService } from '../abuse.service' | ||
13 | |||
14 | @Component({ | ||
15 | selector: 'my-video-report', | ||
16 | templateUrl: './video-report.component.html', | ||
17 | styleUrls: [ './report.component.scss' ] | ||
18 | }) | ||
19 | export class VideoReportComponent extends FormReactive implements OnInit { | ||
20 | @Input() video: Video = null | ||
21 | |||
22 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
23 | |||
24 | error: string = null | ||
25 | predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] | ||
26 | embedHtml: SafeHtml | ||
27 | |||
28 | private openedModal: NgbModalRef | ||
29 | |||
30 | constructor ( | ||
31 | protected formValidatorService: FormValidatorService, | ||
32 | private modalService: NgbModal, | ||
33 | private abuseValidatorsService: AbuseValidatorsService, | ||
34 | private abuseService: AbuseService, | ||
35 | private notifier: Notifier, | ||
36 | private sanitizer: DomSanitizer, | ||
37 | private i18n: I18n | ||
38 | ) { | ||
39 | super() | ||
40 | } | ||
41 | |||
42 | get currentHost () { | ||
43 | return window.location.host | ||
44 | } | ||
45 | |||
46 | get originHost () { | ||
47 | if (this.isRemote()) { | ||
48 | return this.video.account.host | ||
49 | } | ||
50 | |||
51 | return '' | ||
52 | } | ||
53 | |||
54 | get timestamp () { | ||
55 | return this.form.get('timestamp').value | ||
56 | } | ||
57 | |||
58 | getVideoEmbed () { | ||
59 | return this.sanitizer.bypassSecurityTrustHtml( | ||
60 | buildVideoEmbed( | ||
61 | buildVideoLink({ | ||
62 | baseUrl: this.video.embedUrl, | ||
63 | title: false, | ||
64 | warningTitle: false | ||
65 | }) | ||
66 | ) | ||
67 | ) | ||
68 | } | ||
69 | |||
70 | ngOnInit () { | ||
71 | this.buildForm({ | ||
72 | reason: this.abuseValidatorsService.ABUSE_REASON, | ||
73 | predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null), | ||
74 | timestamp: { | ||
75 | hasStart: null, | ||
76 | startAt: null, | ||
77 | hasEnd: null, | ||
78 | endAt: null | ||
79 | } | ||
80 | }) | ||
81 | |||
82 | this.predefinedReasons = this.abuseService.getPrefefinedReasons('video') | ||
83 | |||
84 | this.embedHtml = this.getVideoEmbed() | ||
85 | } | ||
86 | |||
87 | show () { | ||
88 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' }) | ||
89 | } | ||
90 | |||
91 | hide () { | ||
92 | this.openedModal.close() | ||
93 | this.openedModal = null | ||
94 | } | ||
95 | |||
96 | report () { | ||
97 | const reason = this.form.get('reason').value | ||
98 | const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[] | ||
99 | const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value | ||
100 | |||
101 | this.abuseService.reportVideo({ | ||
102 | reason, | ||
103 | predefinedReasons, | ||
104 | video: { | ||
105 | id: this.video.id, | ||
106 | startAt: hasStart && startAt ? startAt : undefined, | ||
107 | endAt: hasEnd && endAt ? endAt : undefined | ||
108 | } | ||
109 | }).subscribe( | ||
110 | () => { | ||
111 | this.notifier.success(this.i18n('Video reported.')) | ||
112 | this.hide() | ||
113 | }, | ||
114 | |||
115 | err => this.notifier.error(err.message) | ||
116 | ) | ||
117 | } | ||
118 | |||
119 | isRemote () { | ||
120 | return !this.video.isLocal | ||
121 | } | ||
122 | } | ||
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..8fa9ee794 100644 --- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts +++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts | |||
@@ -3,21 +3,23 @@ import { NgModule } from '@angular/core' | |||
3 | import { SharedFormModule } from '../shared-forms/shared-form.module' | 3 | import { SharedFormModule } from '../shared-forms/shared-form.module' |
4 | import { SharedGlobalIconModule } from '../shared-icons' | 4 | import { SharedGlobalIconModule } from '../shared-icons' |
5 | import { SharedMainModule } from '../shared-main/shared-main.module' | 5 | import { SharedMainModule } from '../shared-main/shared-main.module' |
6 | import { SharedVideoCommentModule } from '../shared-video-comment' | ||
7 | import { AbuseService } from './abuse.service' | ||
6 | import { BatchDomainsModalComponent } from './batch-domains-modal.component' | 8 | import { BatchDomainsModalComponent } from './batch-domains-modal.component' |
7 | import { BlocklistService } from './blocklist.service' | 9 | import { BlocklistService } from './blocklist.service' |
8 | import { BulkService } from './bulk.service' | 10 | import { BulkService } from './bulk.service' |
9 | import { UserBanModalComponent } from './user-ban-modal.component' | 11 | import { UserBanModalComponent } from './user-ban-modal.component' |
10 | import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' | 12 | import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' |
11 | import { VideoAbuseService } from './video-abuse.service' | ||
12 | import { VideoBlockComponent } from './video-block.component' | 13 | import { VideoBlockComponent } from './video-block.component' |
13 | import { VideoBlockService } from './video-block.service' | 14 | import { VideoBlockService } from './video-block.service' |
14 | import { VideoReportComponent } from './video-report.component' | 15 | import { VideoReportComponent, AccountReportComponent, CommentReportComponent } from './report-modals' |
15 | 16 | ||
16 | @NgModule({ | 17 | @NgModule({ |
17 | imports: [ | 18 | imports: [ |
18 | SharedMainModule, | 19 | SharedMainModule, |
19 | SharedFormModule, | 20 | SharedFormModule, |
20 | SharedGlobalIconModule | 21 | SharedGlobalIconModule, |
22 | SharedVideoCommentModule | ||
21 | ], | 23 | ], |
22 | 24 | ||
23 | declarations: [ | 25 | declarations: [ |
@@ -25,7 +27,9 @@ import { VideoReportComponent } from './video-report.component' | |||
25 | UserModerationDropdownComponent, | 27 | UserModerationDropdownComponent, |
26 | VideoBlockComponent, | 28 | VideoBlockComponent, |
27 | VideoReportComponent, | 29 | VideoReportComponent, |
28 | BatchDomainsModalComponent | 30 | BatchDomainsModalComponent, |
31 | CommentReportComponent, | ||
32 | AccountReportComponent | ||
29 | ], | 33 | ], |
30 | 34 | ||
31 | exports: [ | 35 | exports: [ |
@@ -33,13 +37,15 @@ import { VideoReportComponent } from './video-report.component' | |||
33 | UserModerationDropdownComponent, | 37 | UserModerationDropdownComponent, |
34 | VideoBlockComponent, | 38 | VideoBlockComponent, |
35 | VideoReportComponent, | 39 | VideoReportComponent, |
36 | BatchDomainsModalComponent | 40 | BatchDomainsModalComponent, |
41 | CommentReportComponent, | ||
42 | AccountReportComponent | ||
37 | ], | 43 | ], |
38 | 44 | ||
39 | providers: [ | 45 | providers: [ |
40 | BlocklistService, | 46 | BlocklistService, |
41 | BulkService, | 47 | BulkService, |
42 | VideoAbuseService, | 48 | AbuseService, |
43 | VideoBlockService | 49 | VideoBlockService |
44 | ] | 50 | ] |
45 | }) | 51 | }) |
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index d3c37f082..78c2658df 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts | |||
@@ -16,6 +16,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
16 | 16 | ||
17 | @Input() user: User | 17 | @Input() user: User |
18 | @Input() account: Account | 18 | @Input() account: Account |
19 | @Input() prependActions: DropdownAction<{ user: User, account: Account }>[] | ||
19 | 20 | ||
20 | @Input() buttonSize: 'normal' | 'small' = 'normal' | 21 | @Input() buttonSize: 'normal' | 'small' = 'normal' |
21 | @Input() placement = 'left-top left-bottom auto' | 22 | @Input() placement = 'left-top left-bottom auto' |
@@ -250,6 +251,12 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
250 | private buildActions () { | 251 | private buildActions () { |
251 | this.userActions = [] | 252 | this.userActions = [] |
252 | 253 | ||
254 | if (this.prependActions) { | ||
255 | this.userActions = [ | ||
256 | this.prependActions | ||
257 | ] | ||
258 | } | ||
259 | |||
253 | if (this.authService.isLoggedIn()) { | 260 | if (this.authService.isLoggedIn()) { |
254 | const authUser = this.authService.getUser() | 261 | const authUser = this.authService.getUser() |
255 | 262 | ||
diff --git a/client/src/app/shared/shared-moderation/video-abuse.service.ts b/client/src/app/shared/shared-moderation/video-abuse.service.ts deleted file mode 100644 index 44dea44a5..000000000 --- a/client/src/app/shared/shared-moderation/video-abuse.service.ts +++ /dev/null | |||
@@ -1,98 +0,0 @@ | |||
1 | import { omit } from 'lodash-es' | ||
2 | import { SortMeta } from 'primeng/api' | ||
3 | import { Observable } from 'rxjs' | ||
4 | import { catchError, map } from 'rxjs/operators' | ||
5 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
6 | import { Injectable } from '@angular/core' | ||
7 | import { RestExtractor, RestPagination, RestService } from '@app/core' | ||
8 | import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '@shared/models' | ||
9 | import { environment } from '../../../environments/environment' | ||
10 | |||
11 | @Injectable() | ||
12 | export class VideoAbuseService { | ||
13 | private static BASE_VIDEO_ABUSE_URL = environment.apiUrl + '/api/v1/videos/' | ||
14 | |||
15 | constructor ( | ||
16 | private authHttp: HttpClient, | ||
17 | private restService: RestService, | ||
18 | private restExtractor: RestExtractor | ||
19 | ) {} | ||
20 | |||
21 | getVideoAbuses (options: { | ||
22 | pagination: RestPagination, | ||
23 | sort: SortMeta, | ||
24 | search?: string | ||
25 | }): Observable<ResultList<VideoAbuse>> { | ||
26 | const { pagination, sort, search } = options | ||
27 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse' | ||
28 | |||
29 | let params = new HttpParams() | ||
30 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
31 | |||
32 | if (search) { | ||
33 | const filters = this.restService.parseQueryStringFilter(search, { | ||
34 | id: { prefix: '#' }, | ||
35 | state: { | ||
36 | prefix: 'state:', | ||
37 | handler: v => { | ||
38 | if (v === 'accepted') return VideoAbuseState.ACCEPTED | ||
39 | if (v === 'pending') return VideoAbuseState.PENDING | ||
40 | if (v === 'rejected') return VideoAbuseState.REJECTED | ||
41 | |||
42 | return undefined | ||
43 | } | ||
44 | }, | ||
45 | videoIs: { | ||
46 | prefix: 'videoIs:', | ||
47 | handler: v => { | ||
48 | if (v === 'deleted') return v | ||
49 | if (v === 'blacklisted') return v | ||
50 | |||
51 | return undefined | ||
52 | } | ||
53 | }, | ||
54 | searchReporter: { prefix: 'reporter:' }, | ||
55 | searchReportee: { prefix: 'reportee:' }, | ||
56 | predefinedReason: { prefix: 'tag:' } | ||
57 | }) | ||
58 | |||
59 | params = this.restService.addObjectParams(params, filters) | ||
60 | } | ||
61 | |||
62 | return this.authHttp.get<ResultList<VideoAbuse>>(url, { params }) | ||
63 | .pipe( | ||
64 | catchError(res => this.restExtractor.handleError(res)) | ||
65 | ) | ||
66 | } | ||
67 | |||
68 | reportVideo (parameters: { id: number } & VideoAbuseCreate) { | ||
69 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse' | ||
70 | |||
71 | const body = omit(parameters, [ 'id' ]) | ||
72 | |||
73 | return this.authHttp.post(url, body) | ||
74 | .pipe( | ||
75 | map(this.restExtractor.extractDataBool), | ||
76 | catchError(res => this.restExtractor.handleError(res)) | ||
77 | ) | ||
78 | } | ||
79 | |||
80 | updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) { | ||
81 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id | ||
82 | |||
83 | return this.authHttp.put(url, abuseUpdate) | ||
84 | .pipe( | ||
85 | map(this.restExtractor.extractDataBool), | ||
86 | catchError(res => this.restExtractor.handleError(res)) | ||
87 | ) | ||
88 | } | ||
89 | |||
90 | removeVideoAbuse (videoAbuse: VideoAbuse) { | ||
91 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id | ||
92 | |||
93 | return this.authHttp.delete(url) | ||
94 | .pipe( | ||
95 | map(this.restExtractor.extractDataBool), | ||
96 | catchError(res => this.restExtractor.handleError(res)) | ||
97 | ) | ||
98 | }} | ||
diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/video-report.component.ts deleted file mode 100644 index 11c805636..000000000 --- a/client/src/app/shared/shared-moderation/video-report.component.ts +++ /dev/null | |||
@@ -1,161 +0,0 @@ | |||
1 | import { mapValues, pickBy } from 'lodash-es' | ||
2 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' | ||
3 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | ||
4 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | ||
5 | import { Notifier } from '@app/core' | ||
6 | import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms' | ||
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
10 | import { videoAbusePredefinedReasonsMap, VideoAbusePredefinedReasonsString } from '@shared/models/videos/abuse/video-abuse-reason.model' | ||
11 | import { Video } from '../shared-main' | ||
12 | import { VideoAbuseService } from './video-abuse.service' | ||
13 | |||
14 | @Component({ | ||
15 | selector: 'my-video-report', | ||
16 | templateUrl: './video-report.component.html', | ||
17 | styleUrls: [ './video-report.component.scss' ] | ||
18 | }) | ||
19 | export class VideoReportComponent extends FormReactive implements OnInit { | ||
20 | @Input() video: Video = null | ||
21 | |||
22 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
23 | |||
24 | error: string = null | ||
25 | predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] | ||
26 | embedHtml: SafeHtml | ||
27 | |||
28 | private openedModal: NgbModalRef | ||
29 | |||
30 | constructor ( | ||
31 | protected formValidatorService: FormValidatorService, | ||
32 | private modalService: NgbModal, | ||
33 | private videoAbuseValidatorsService: VideoAbuseValidatorsService, | ||
34 | private videoAbuseService: VideoAbuseService, | ||
35 | private notifier: Notifier, | ||
36 | private sanitizer: DomSanitizer, | ||
37 | private i18n: I18n | ||
38 | ) { | ||
39 | super() | ||
40 | } | ||
41 | |||
42 | get currentHost () { | ||
43 | return window.location.host | ||
44 | } | ||
45 | |||
46 | get originHost () { | ||
47 | if (this.isRemoteVideo()) { | ||
48 | return this.video.account.host | ||
49 | } | ||
50 | |||
51 | return '' | ||
52 | } | ||
53 | |||
54 | get timestamp () { | ||
55 | return this.form.get('timestamp').value | ||
56 | } | ||
57 | |||
58 | getVideoEmbed () { | ||
59 | return this.sanitizer.bypassSecurityTrustHtml( | ||
60 | buildVideoEmbed( | ||
61 | buildVideoLink({ | ||
62 | baseUrl: this.video.embedUrl, | ||
63 | title: false, | ||
64 | warningTitle: false | ||
65 | }) | ||
66 | ) | ||
67 | ) | ||
68 | } | ||
69 | |||
70 | ngOnInit () { | ||
71 | this.buildForm({ | ||
72 | reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON, | ||
73 | predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null), | ||
74 | timestamp: { | ||
75 | hasStart: null, | ||
76 | startAt: null, | ||
77 | hasEnd: null, | ||
78 | endAt: null | ||
79 | } | ||
80 | }) | ||
81 | |||
82 | this.predefinedReasons = [ | ||
83 | { | ||
84 | id: 'violentOrRepulsive', | ||
85 | label: this.i18n('Violent or repulsive'), | ||
86 | help: this.i18n('Contains offensive, violent, or coarse language or iconography.') | ||
87 | }, | ||
88 | { | ||
89 | id: 'hatefulOrAbusive', | ||
90 | label: this.i18n('Hateful or abusive'), | ||
91 | help: this.i18n('Contains abusive, racist or sexist language or iconography.') | ||
92 | }, | ||
93 | { | ||
94 | id: 'spamOrMisleading', | ||
95 | label: this.i18n('Spam, ad or false news'), | ||
96 | help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') | ||
97 | }, | ||
98 | { | ||
99 | id: 'privacy', | ||
100 | label: this.i18n('Privacy breach or doxxing'), | ||
101 | help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).') | ||
102 | }, | ||
103 | { | ||
104 | id: 'rights', | ||
105 | label: this.i18n('Intellectual property violation'), | ||
106 | help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') | ||
107 | }, | ||
108 | { | ||
109 | id: 'serverRules', | ||
110 | label: this.i18n('Breaks server rules'), | ||
111 | description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.') | ||
112 | }, | ||
113 | { | ||
114 | id: 'thumbnails', | ||
115 | label: this.i18n('Thumbnails'), | ||
116 | help: this.i18n('The above can only be seen in thumbnails.') | ||
117 | }, | ||
118 | { | ||
119 | id: 'captions', | ||
120 | label: this.i18n('Captions'), | ||
121 | help: this.i18n('The above can only be seen in captions (please describe which).') | ||
122 | } | ||
123 | ] | ||
124 | |||
125 | this.embedHtml = this.getVideoEmbed() | ||
126 | } | ||
127 | |||
128 | show () { | ||
129 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' }) | ||
130 | } | ||
131 | |||
132 | hide () { | ||
133 | this.openedModal.close() | ||
134 | this.openedModal = null | ||
135 | } | ||
136 | |||
137 | report () { | ||
138 | const reason = this.form.get('reason').value | ||
139 | const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[] | ||
140 | const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value | ||
141 | |||
142 | this.videoAbuseService.reportVideo({ | ||
143 | id: this.video.id, | ||
144 | reason, | ||
145 | predefinedReasons, | ||
146 | startAt: hasStart && startAt ? startAt : undefined, | ||
147 | endAt: hasEnd && endAt ? endAt : undefined | ||
148 | }).subscribe( | ||
149 | () => { | ||
150 | this.notifier.success(this.i18n('Video reported.')) | ||
151 | this.hide() | ||
152 | }, | ||
153 | |||
154 | err => this.notifier.error(err.message) | ||
155 | ) | ||
156 | } | ||
157 | |||
158 | isRemoteVideo () { | ||
159 | return !this.video.isLocal | ||
160 | } | ||
161 | } | ||
diff --git a/client/src/app/shared/shared-video-comment/index.ts b/client/src/app/shared/shared-video-comment/index.ts new file mode 100644 index 000000000..b1195f232 --- /dev/null +++ b/client/src/app/shared/shared-video-comment/index.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | export * from './video-comment.service' | ||
2 | export * from './video-comment.model' | ||
3 | export * from './video-comment-thread-tree.model' | ||
4 | |||
5 | export * from './shared-video-comment.module' | ||
diff --git a/client/src/app/shared/shared-video-comment/shared-video-comment.module.ts b/client/src/app/shared/shared-video-comment/shared-video-comment.module.ts new file mode 100644 index 000000000..41b329861 --- /dev/null +++ b/client/src/app/shared/shared-video-comment/shared-video-comment.module.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | |||
2 | import { NgModule } from '@angular/core' | ||
3 | import { SharedMainModule } from '../shared-main/shared-main.module' | ||
4 | import { VideoCommentService } from './video-comment.service' | ||
5 | |||
6 | @NgModule({ | ||
7 | imports: [ | ||
8 | SharedMainModule | ||
9 | ], | ||
10 | |||
11 | declarations: [ ], | ||
12 | |||
13 | exports: [ ], | ||
14 | |||
15 | providers: [ | ||
16 | VideoCommentService | ||
17 | ] | ||
18 | }) | ||
19 | export class SharedVideoCommentModule { } | ||
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment-thread-tree.model.ts b/client/src/app/shared/shared-video-comment/video-comment-thread-tree.model.ts index 7c2aaeadd..7c2aaeadd 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment-thread-tree.model.ts +++ b/client/src/app/shared/shared-video-comment/video-comment-thread-tree.model.ts | |||
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.model.ts b/client/src/app/shared/shared-video-comment/video-comment.model.ts index e85443196..e85443196 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.model.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.model.ts | |||
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts index a73fb9ca8..81c65aa38 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.service.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | VideoCommentCreate, | 11 | VideoCommentCreate, |
12 | VideoCommentThreadTree as VideoCommentThreadTreeServerModel | 12 | VideoCommentThreadTree as VideoCommentThreadTreeServerModel |
13 | } from '@shared/models' | 13 | } from '@shared/models' |
14 | import { environment } from '../../../../environments/environment' | 14 | import { environment } from '../../../environments/environment' |
15 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' | 15 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' |
16 | import { VideoComment } from './video-comment.model' | 16 | import { VideoComment } from './video-comment.model' |
17 | 17 | ||
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts new file mode 100644 index 000000000..04a0c06e3 --- /dev/null +++ b/server/controllers/api/abuse.ts | |||
@@ -0,0 +1,168 @@ | |||
1 | import * as express from 'express' | ||
2 | import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation' | ||
3 | import { AbuseModel } from '@server/models/abuse/abuse' | ||
4 | import { getServerActor } from '@server/models/application/application' | ||
5 | import { AbuseCreate, abusePredefinedReasonsMap, AbuseState, UserRight } from '../../../shared' | ||
6 | import { getFormattedObjects } from '../../helpers/utils' | ||
7 | import { sequelizeTypescript } from '../../initializers/database' | ||
8 | import { | ||
9 | abuseGetValidator, | ||
10 | abuseListValidator, | ||
11 | abuseReportValidator, | ||
12 | abusesSortValidator, | ||
13 | abuseUpdateValidator, | ||
14 | asyncMiddleware, | ||
15 | asyncRetryTransactionMiddleware, | ||
16 | authenticate, | ||
17 | ensureUserHasRight, | ||
18 | paginationValidator, | ||
19 | setDefaultPagination, | ||
20 | setDefaultSort | ||
21 | } from '../../middlewares' | ||
22 | import { AccountModel } from '../../models/account/account' | ||
23 | |||
24 | const abuseRouter = express.Router() | ||
25 | |||
26 | abuseRouter.get('/', | ||
27 | authenticate, | ||
28 | ensureUserHasRight(UserRight.MANAGE_ABUSES), | ||
29 | paginationValidator, | ||
30 | abusesSortValidator, | ||
31 | setDefaultSort, | ||
32 | setDefaultPagination, | ||
33 | abuseListValidator, | ||
34 | asyncMiddleware(listAbuses) | ||
35 | ) | ||
36 | abuseRouter.put('/:id', | ||
37 | authenticate, | ||
38 | ensureUserHasRight(UserRight.MANAGE_ABUSES), | ||
39 | asyncMiddleware(abuseUpdateValidator), | ||
40 | asyncRetryTransactionMiddleware(updateAbuse) | ||
41 | ) | ||
42 | abuseRouter.post('/', | ||
43 | authenticate, | ||
44 | asyncMiddleware(abuseReportValidator), | ||
45 | asyncRetryTransactionMiddleware(reportAbuse) | ||
46 | ) | ||
47 | abuseRouter.delete('/:id', | ||
48 | authenticate, | ||
49 | ensureUserHasRight(UserRight.MANAGE_ABUSES), | ||
50 | asyncMiddleware(abuseGetValidator), | ||
51 | asyncRetryTransactionMiddleware(deleteAbuse) | ||
52 | ) | ||
53 | |||
54 | // --------------------------------------------------------------------------- | ||
55 | |||
56 | export { | ||
57 | abuseRouter, | ||
58 | |||
59 | // FIXME: deprecated in 2.3. Remove these exports | ||
60 | listAbuses, | ||
61 | updateAbuse, | ||
62 | deleteAbuse, | ||
63 | reportAbuse | ||
64 | } | ||
65 | |||
66 | // --------------------------------------------------------------------------- | ||
67 | |||
68 | async function listAbuses (req: express.Request, res: express.Response) { | ||
69 | const user = res.locals.oauth.token.user | ||
70 | const serverActor = await getServerActor() | ||
71 | |||
72 | const resultList = await AbuseModel.listForApi({ | ||
73 | start: req.query.start, | ||
74 | count: req.query.count, | ||
75 | sort: req.query.sort, | ||
76 | id: req.query.id, | ||
77 | filter: req.query.filter, | ||
78 | predefinedReason: req.query.predefinedReason, | ||
79 | search: req.query.search, | ||
80 | state: req.query.state, | ||
81 | videoIs: req.query.videoIs, | ||
82 | searchReporter: req.query.searchReporter, | ||
83 | searchReportee: req.query.searchReportee, | ||
84 | searchVideo: req.query.searchVideo, | ||
85 | searchVideoChannel: req.query.searchVideoChannel, | ||
86 | serverAccountId: serverActor.Account.id, | ||
87 | user | ||
88 | }) | ||
89 | |||
90 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
91 | } | ||
92 | |||
93 | async function updateAbuse (req: express.Request, res: express.Response) { | ||
94 | const abuse = res.locals.abuse | ||
95 | |||
96 | if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment | ||
97 | if (req.body.state !== undefined) abuse.state = req.body.state | ||
98 | |||
99 | await sequelizeTypescript.transaction(t => { | ||
100 | return abuse.save({ transaction: t }) | ||
101 | }) | ||
102 | |||
103 | // Do not send the delete to other instances, we updated OUR copy of this abuse | ||
104 | |||
105 | return res.type('json').status(204).end() | ||
106 | } | ||
107 | |||
108 | async function deleteAbuse (req: express.Request, res: express.Response) { | ||
109 | const abuse = res.locals.abuse | ||
110 | |||
111 | await sequelizeTypescript.transaction(t => { | ||
112 | return abuse.destroy({ transaction: t }) | ||
113 | }) | ||
114 | |||
115 | // Do not send the delete to other instances, we delete OUR copy of this abuse | ||
116 | |||
117 | return res.type('json').status(204).end() | ||
118 | } | ||
119 | |||
120 | async function reportAbuse (req: express.Request, res: express.Response) { | ||
121 | const videoInstance = res.locals.videoAll | ||
122 | const commentInstance = res.locals.videoCommentFull | ||
123 | const accountInstance = res.locals.account | ||
124 | |||
125 | const body: AbuseCreate = req.body | ||
126 | |||
127 | const { id } = await sequelizeTypescript.transaction(async t => { | ||
128 | const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) | ||
129 | const predefinedReasons = body.predefinedReasons?.map(r => abusePredefinedReasonsMap[r]) | ||
130 | |||
131 | const baseAbuse = { | ||
132 | reporterAccountId: reporterAccount.id, | ||
133 | reason: body.reason, | ||
134 | state: AbuseState.PENDING, | ||
135 | predefinedReasons | ||
136 | } | ||
137 | |||
138 | if (body.video) { | ||
139 | return createVideoAbuse({ | ||
140 | baseAbuse, | ||
141 | videoInstance, | ||
142 | reporterAccount, | ||
143 | transaction: t, | ||
144 | startAt: body.video.startAt, | ||
145 | endAt: body.video.endAt | ||
146 | }) | ||
147 | } | ||
148 | |||
149 | if (body.comment) { | ||
150 | return createVideoCommentAbuse({ | ||
151 | baseAbuse, | ||
152 | commentInstance, | ||
153 | reporterAccount, | ||
154 | transaction: t | ||
155 | }) | ||
156 | } | ||
157 | |||
158 | // Account report | ||
159 | return createAccountAbuse({ | ||
160 | baseAbuse, | ||
161 | accountInstance, | ||
162 | reporterAccount, | ||
163 | transaction: t | ||
164 | }) | ||
165 | }) | ||
166 | |||
167 | return res.json({ abuse: { id } }) | ||
168 | } | ||
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' | |||
3 | import * as RateLimit from 'express-rate-limit' | 3 | import * as RateLimit from 'express-rate-limit' |
4 | import { badRequest } from '../../helpers/express-utils' | 4 | import { badRequest } from '../../helpers/express-utils' |
5 | import { CONFIG } from '../../initializers/config' | 5 | import { CONFIG } from '../../initializers/config' |
6 | import { abuseRouter } from './abuse' | ||
6 | import { accountsRouter } from './accounts' | 7 | import { accountsRouter } from './accounts' |
7 | import { bulkRouter } from './bulk' | 8 | import { bulkRouter } from './bulk' |
8 | import { configRouter } from './config' | 9 | import { configRouter } from './config' |
@@ -32,6 +33,7 @@ const apiRateLimiter = RateLimit({ | |||
32 | apiRouter.use(apiRateLimiter) | 33 | apiRouter.use(apiRateLimiter) |
33 | 34 | ||
34 | apiRouter.use('/server', serverRouter) | 35 | apiRouter.use('/server', serverRouter) |
36 | apiRouter.use('/abuses', abuseRouter) | ||
35 | apiRouter.use('/bulk', bulkRouter) | 37 | apiRouter.use('/bulk', bulkRouter) |
36 | apiRouter.use('/oauth-clients', oauthClientsRouter) | 38 | apiRouter.use('/oauth-clients', oauthClientsRouter) |
37 | apiRouter.use('/config', configRouter) | 39 | apiRouter.use('/config', configRouter) |
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts index 77a15e5fc..dc915977f 100644 --- a/server/controllers/api/users/my-history.ts +++ b/server/controllers/api/users/my-history.ts | |||
@@ -50,7 +50,5 @@ async function removeUserHistory (req: express.Request, res: express.Response) { | |||
50 | return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t) | 50 | return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t) |
51 | }) | 51 | }) |
52 | 52 | ||
53 | // Do not send the delete to other instances, we delete OUR copy of this video abuse | ||
54 | |||
55 | return res.type('json').status(204).end() | 53 | return res.type('json').status(204).end() |
56 | } | 54 | } |
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index 017f5219e..0be51c128 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -68,7 +68,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re | |||
68 | const values: UserNotificationSetting = { | 68 | const values: UserNotificationSetting = { |
69 | newVideoFromSubscription: body.newVideoFromSubscription, | 69 | newVideoFromSubscription: body.newVideoFromSubscription, |
70 | newCommentOnMyVideo: body.newCommentOnMyVideo, | 70 | newCommentOnMyVideo: body.newCommentOnMyVideo, |
71 | videoAbuseAsModerator: body.videoAbuseAsModerator, | 71 | abuseAsModerator: body.abuseAsModerator, |
72 | videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator, | 72 | videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator, |
73 | blacklistOnMyVideo: body.blacklistOnMyVideo, | 73 | blacklistOnMyVideo: body.blacklistOnMyVideo, |
74 | myVideoPublished: body.myVideoPublished, | 74 | myVideoPublished: body.myVideoPublished, |
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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared' | 2 | import { AbuseModel } from '@server/models/abuse/abuse' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { getServerActor } from '@server/models/application/application' |
4 | import { AbuseCreate, UserRight, VideoAbuseCreate } from '../../../../shared' | ||
4 | import { getFormattedObjects } from '../../../helpers/utils' | 5 | import { getFormattedObjects } from '../../../helpers/utils' |
5 | import { sequelizeTypescript } from '../../../initializers/database' | ||
6 | import { | 6 | import { |
7 | abusesSortValidator, | ||
7 | asyncMiddleware, | 8 | asyncMiddleware, |
8 | asyncRetryTransactionMiddleware, | 9 | asyncRetryTransactionMiddleware, |
9 | authenticate, | 10 | authenticate, |
@@ -12,28 +13,21 @@ import { | |||
12 | setDefaultPagination, | 13 | setDefaultPagination, |
13 | setDefaultSort, | 14 | setDefaultSort, |
14 | videoAbuseGetValidator, | 15 | videoAbuseGetValidator, |
16 | videoAbuseListValidator, | ||
15 | videoAbuseReportValidator, | 17 | videoAbuseReportValidator, |
16 | videoAbusesSortValidator, | 18 | videoAbuseUpdateValidator |
17 | videoAbuseUpdateValidator, | ||
18 | videoAbuseListValidator | ||
19 | } from '../../../middlewares' | 19 | } from '../../../middlewares' |
20 | import { AccountModel } from '../../../models/account/account' | 20 | import { deleteAbuse, reportAbuse, updateAbuse } from '../abuse' |
21 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | 21 | |
22 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' | 22 | // FIXME: deprecated in 2.3. Remove this controller |
23 | import { Notifier } from '../../../lib/notifier' | ||
24 | import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag' | ||
25 | import { MVideoAbuseAccountVideo } from '../../../types/models/video' | ||
26 | import { getServerActor } from '@server/models/application/application' | ||
27 | import { MAccountDefault } from '@server/types/models' | ||
28 | 23 | ||
29 | const auditLogger = auditLoggerFactory('abuse') | ||
30 | const abuseVideoRouter = express.Router() | 24 | const abuseVideoRouter = express.Router() |
31 | 25 | ||
32 | abuseVideoRouter.get('/abuse', | 26 | abuseVideoRouter.get('/abuse', |
33 | authenticate, | 27 | authenticate, |
34 | ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), | 28 | ensureUserHasRight(UserRight.MANAGE_ABUSES), |
35 | paginationValidator, | 29 | paginationValidator, |
36 | videoAbusesSortValidator, | 30 | abusesSortValidator, |
37 | setDefaultSort, | 31 | setDefaultSort, |
38 | setDefaultPagination, | 32 | setDefaultPagination, |
39 | videoAbuseListValidator, | 33 | videoAbuseListValidator, |
@@ -41,7 +35,7 @@ abuseVideoRouter.get('/abuse', | |||
41 | ) | 35 | ) |
42 | abuseVideoRouter.put('/:videoId/abuse/:id', | 36 | abuseVideoRouter.put('/:videoId/abuse/:id', |
43 | authenticate, | 37 | authenticate, |
44 | ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), | 38 | ensureUserHasRight(UserRight.MANAGE_ABUSES), |
45 | asyncMiddleware(videoAbuseUpdateValidator), | 39 | asyncMiddleware(videoAbuseUpdateValidator), |
46 | asyncRetryTransactionMiddleware(updateVideoAbuse) | 40 | asyncRetryTransactionMiddleware(updateVideoAbuse) |
47 | ) | 41 | ) |
@@ -52,7 +46,7 @@ abuseVideoRouter.post('/:videoId/abuse', | |||
52 | ) | 46 | ) |
53 | abuseVideoRouter.delete('/:videoId/abuse/:id', | 47 | abuseVideoRouter.delete('/:videoId/abuse/:id', |
54 | authenticate, | 48 | authenticate, |
55 | ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), | 49 | ensureUserHasRight(UserRight.MANAGE_ABUSES), |
56 | asyncMiddleware(videoAbuseGetValidator), | 50 | asyncMiddleware(videoAbuseGetValidator), |
57 | asyncRetryTransactionMiddleware(deleteVideoAbuse) | 51 | asyncRetryTransactionMiddleware(deleteVideoAbuse) |
58 | ) | 52 | ) |
@@ -69,11 +63,12 @@ async function listVideoAbuses (req: express.Request, res: express.Response) { | |||
69 | const user = res.locals.oauth.token.user | 63 | const user = res.locals.oauth.token.user |
70 | const serverActor = await getServerActor() | 64 | const serverActor = await getServerActor() |
71 | 65 | ||
72 | const resultList = await VideoAbuseModel.listForApi({ | 66 | const resultList = await AbuseModel.listForApi({ |
73 | start: req.query.start, | 67 | start: req.query.start, |
74 | count: req.query.count, | 68 | count: req.query.count, |
75 | sort: req.query.sort, | 69 | sort: req.query.sort, |
76 | id: req.query.id, | 70 | id: req.query.id, |
71 | filter: 'video', | ||
77 | predefinedReason: req.query.predefinedReason, | 72 | predefinedReason: req.query.predefinedReason, |
78 | search: req.query.search, | 73 | search: req.query.search, |
79 | state: req.query.state, | 74 | state: req.query.state, |
@@ -90,74 +85,28 @@ async function listVideoAbuses (req: express.Request, res: express.Response) { | |||
90 | } | 85 | } |
91 | 86 | ||
92 | async function updateVideoAbuse (req: express.Request, res: express.Response) { | 87 | async function updateVideoAbuse (req: express.Request, res: express.Response) { |
93 | const videoAbuse = res.locals.videoAbuse | 88 | return updateAbuse(req, res) |
94 | |||
95 | if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment | ||
96 | if (req.body.state !== undefined) videoAbuse.state = req.body.state | ||
97 | |||
98 | await sequelizeTypescript.transaction(t => { | ||
99 | return videoAbuse.save({ transaction: t }) | ||
100 | }) | ||
101 | |||
102 | // Do not send the delete to other instances, we updated OUR copy of this video abuse | ||
103 | |||
104 | return res.type('json').status(204).end() | ||
105 | } | 89 | } |
106 | 90 | ||
107 | async function deleteVideoAbuse (req: express.Request, res: express.Response) { | 91 | async function deleteVideoAbuse (req: express.Request, res: express.Response) { |
108 | const videoAbuse = res.locals.videoAbuse | 92 | return deleteAbuse(req, res) |
109 | |||
110 | await sequelizeTypescript.transaction(t => { | ||
111 | return videoAbuse.destroy({ transaction: t }) | ||
112 | }) | ||
113 | |||
114 | // Do not send the delete to other instances, we delete OUR copy of this video abuse | ||
115 | |||
116 | return res.type('json').status(204).end() | ||
117 | } | 93 | } |
118 | 94 | ||
119 | async function reportVideoAbuse (req: express.Request, res: express.Response) { | 95 | async function reportVideoAbuse (req: express.Request, res: express.Response) { |
120 | const videoInstance = res.locals.videoAll | 96 | const oldBody = req.body as VideoAbuseCreate |
121 | const body: VideoAbuseCreate = req.body | ||
122 | let reporterAccount: MAccountDefault | ||
123 | let videoAbuseJSON: VideoAbuse | ||
124 | |||
125 | const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { | ||
126 | reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) | ||
127 | const predefinedReasons = body.predefinedReasons?.map(r => videoAbusePredefinedReasonsMap[r]) | ||
128 | |||
129 | const abuseToCreate = { | ||
130 | reporterAccountId: reporterAccount.id, | ||
131 | reason: body.reason, | ||
132 | videoId: videoInstance.id, | ||
133 | state: VideoAbuseState.PENDING, | ||
134 | predefinedReasons, | ||
135 | startAt: body.startAt, | ||
136 | endAt: body.endAt | ||
137 | } | ||
138 | |||
139 | const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) | ||
140 | videoAbuseInstance.Video = videoInstance | ||
141 | videoAbuseInstance.Account = reporterAccount | ||
142 | |||
143 | // We send the video abuse to the origin server | ||
144 | if (videoInstance.isOwned() === false) { | ||
145 | await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t) | ||
146 | } | ||
147 | 97 | ||
148 | videoAbuseJSON = videoAbuseInstance.toFormattedJSON() | 98 | req.body = { |
149 | auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON)) | 99 | accountId: res.locals.videoAll.VideoChannel.accountId, |
150 | 100 | ||
151 | return videoAbuseInstance | 101 | reason: oldBody.reason, |
152 | }) | 102 | predefinedReasons: oldBody.predefinedReasons, |
153 | 103 | ||
154 | Notifier.Instance.notifyOnNewVideoAbuse({ | 104 | video: { |
155 | videoAbuse: videoAbuseJSON, | 105 | id: res.locals.videoAll.id, |
156 | videoAbuseInstance, | 106 | startAt: oldBody.startAt, |
157 | reporter: reporterAccount.Actor.getIdentifier() | 107 | endAt: oldBody.endAt |
158 | }) | 108 | } |
159 | 109 | } as AbuseCreate | |
160 | logger.info('Abuse report for video "%s" created.', videoInstance.name) | ||
161 | 110 | ||
162 | return res.json({ videoAbuse: videoAbuseJSON }).end() | 111 | return reportAbuse(req, res) |
163 | } | 112 | } |
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 @@ | |||
1 | import * as path from 'path' | ||
2 | import * as express from 'express' | ||
3 | import { diff } from 'deep-object-diff' | 1 | import { diff } from 'deep-object-diff' |
4 | import { chain } from 'lodash' | 2 | import * as express from 'express' |
5 | import * as flatten from 'flat' | 3 | import * as flatten from 'flat' |
4 | import { chain } from 'lodash' | ||
5 | import * as path from 'path' | ||
6 | import * as winston from 'winston' | 6 | import * as winston from 'winston' |
7 | import { jsonLoggerFormat, labelFormatter } from './logger' | 7 | import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' |
8 | import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared' | 8 | import { Abuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared' |
9 | import { VideoComment } from '../../shared/models/videos/video-comment.model' | ||
10 | import { CustomConfig } from '../../shared/models/server/custom-config.model' | 9 | import { CustomConfig } from '../../shared/models/server/custom-config.model' |
10 | import { VideoComment } from '../../shared/models/videos/video-comment.model' | ||
11 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
12 | import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' | 12 | import { jsonLoggerFormat, labelFormatter } from './logger' |
13 | 13 | ||
14 | function getAuditIdFromRes (res: express.Response) { | 14 | function getAuditIdFromRes (res: express.Response) { |
15 | return res.locals.oauth.token.User.username | 15 | return res.locals.oauth.token.User.username |
@@ -212,18 +212,15 @@ class VideoChannelAuditView extends EntityAuditView { | |||
212 | } | 212 | } |
213 | } | 213 | } |
214 | 214 | ||
215 | const videoAbuseKeysToKeep = [ | 215 | const abuseKeysToKeep = [ |
216 | 'id', | 216 | 'id', |
217 | 'reason', | 217 | 'reason', |
218 | 'reporterAccount', | 218 | 'reporterAccount', |
219 | 'video-id', | ||
220 | 'video-name', | ||
221 | 'video-uuid', | ||
222 | 'createdAt' | 219 | 'createdAt' |
223 | ] | 220 | ] |
224 | class VideoAbuseAuditView extends EntityAuditView { | 221 | class AbuseAuditView extends EntityAuditView { |
225 | constructor (private readonly videoAbuse: VideoAbuse) { | 222 | constructor (private readonly abuse: Abuse) { |
226 | super(videoAbuseKeysToKeep, 'abuse', videoAbuse) | 223 | super(abuseKeysToKeep, 'abuse', abuse) |
227 | } | 224 | } |
228 | } | 225 | } |
229 | 226 | ||
@@ -274,6 +271,6 @@ export { | |||
274 | CommentAuditView, | 271 | CommentAuditView, |
275 | UserAuditView, | 272 | UserAuditView, |
276 | VideoAuditView, | 273 | VideoAuditView, |
277 | VideoAbuseAuditView, | 274 | AbuseAuditView, |
278 | CustomConfigAuditView | 275 | CustomConfigAuditView |
279 | } | 276 | } |
diff --git a/server/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts new file mode 100644 index 000000000..0ca06a252 --- /dev/null +++ b/server/helpers/custom-validators/abuses.ts | |||
@@ -0,0 +1,61 @@ | |||
1 | import validator from 'validator' | ||
2 | import { AbuseFilter, abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs, AbuseCreate } from '@shared/models' | ||
3 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
4 | import { exists, isArray } from './misc' | ||
5 | |||
6 | const ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES | ||
7 | |||
8 | function isAbuseReasonValid (value: string) { | ||
9 | return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.REASON) | ||
10 | } | ||
11 | |||
12 | function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) { | ||
13 | return exists(value) && value in abusePredefinedReasonsMap | ||
14 | } | ||
15 | |||
16 | function isAbuseFilterValid (value: AbuseFilter) { | ||
17 | return value === 'video' || value === 'comment' || value === 'account' | ||
18 | } | ||
19 | |||
20 | function areAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) { | ||
21 | return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap) | ||
22 | } | ||
23 | |||
24 | function isAbuseTimestampValid (value: number) { | ||
25 | return value === null || (exists(value) && validator.isInt('' + value, { min: 0 })) | ||
26 | } | ||
27 | |||
28 | function isAbuseTimestampCoherent (endAt: number, { req }) { | ||
29 | const startAt = (req.body as AbuseCreate).video.startAt | ||
30 | |||
31 | return exists(startAt) && endAt > startAt | ||
32 | } | ||
33 | |||
34 | function isAbuseModerationCommentValid (value: string) { | ||
35 | return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) | ||
36 | } | ||
37 | |||
38 | function isAbuseStateValid (value: string) { | ||
39 | return exists(value) && ABUSE_STATES[value] !== undefined | ||
40 | } | ||
41 | |||
42 | function isAbuseVideoIsValid (value: AbuseVideoIs) { | ||
43 | return exists(value) && ( | ||
44 | value === 'deleted' || | ||
45 | value === 'blacklisted' | ||
46 | ) | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export { | ||
52 | isAbuseReasonValid, | ||
53 | isAbuseFilterValid, | ||
54 | isAbusePredefinedReasonValid, | ||
55 | areAbusePredefinedReasonsValid as isAbusePredefinedReasonsValid, | ||
56 | isAbuseTimestampValid, | ||
57 | isAbuseTimestampCoherent, | ||
58 | isAbuseModerationCommentValid, | ||
59 | isAbuseStateValid, | ||
60 | isAbuseVideoIsValid | ||
61 | } | ||
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 @@ | |||
1 | import { isActivityPubUrlValid } from './misc' | 1 | import { isActivityPubUrlValid } from './misc' |
2 | import { isVideoAbuseReasonValid } from '../video-abuses' | 2 | import { isAbuseReasonValid } from '../abuses' |
3 | 3 | ||
4 | function isFlagActivityValid (activity: any) { | 4 | function isFlagActivityValid (activity: any) { |
5 | return activity.type === 'Flag' && | 5 | return activity.type === 'Flag' && |
6 | isVideoAbuseReasonValid(activity.content) && | 6 | isAbuseReasonValid(activity.content) && |
7 | isActivityPubUrlValid(activity.object) | 7 | isActivityPubUrlValid(activity.object) |
8 | } | 8 | } |
9 | 9 | ||
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 @@ | |||
1 | import validator from 'validator' | ||
2 | |||
3 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' | ||
4 | import { exists, isArray } from './misc' | ||
5 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' | ||
6 | import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model' | ||
7 | |||
8 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES | ||
9 | |||
10 | function isVideoAbuseReasonValid (value: string) { | ||
11 | return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) | ||
12 | } | ||
13 | |||
14 | function isVideoAbusePredefinedReasonValid (value: VideoAbusePredefinedReasonsString) { | ||
15 | return exists(value) && value in videoAbusePredefinedReasonsMap | ||
16 | } | ||
17 | |||
18 | function isVideoAbusePredefinedReasonsValid (value: VideoAbusePredefinedReasonsString[]) { | ||
19 | return exists(value) && isArray(value) && value.every(v => v in videoAbusePredefinedReasonsMap) | ||
20 | } | ||
21 | |||
22 | function isVideoAbuseTimestampValid (value: number) { | ||
23 | return value === null || (exists(value) && validator.isInt('' + value, { min: 0 })) | ||
24 | } | ||
25 | |||
26 | function isVideoAbuseTimestampCoherent (endAt: number, { req }) { | ||
27 | return exists(req.body.startAt) && endAt > req.body.startAt | ||
28 | } | ||
29 | |||
30 | function isVideoAbuseModerationCommentValid (value: string) { | ||
31 | return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) | ||
32 | } | ||
33 | |||
34 | function isVideoAbuseStateValid (value: string) { | ||
35 | return exists(value) && VIDEO_ABUSE_STATES[value] !== undefined | ||
36 | } | ||
37 | |||
38 | function isAbuseVideoIsValid (value: VideoAbuseVideoIs) { | ||
39 | return exists(value) && ( | ||
40 | value === 'deleted' || | ||
41 | value === 'blacklisted' | ||
42 | ) | ||
43 | } | ||
44 | |||
45 | // --------------------------------------------------------------------------- | ||
46 | |||
47 | export { | ||
48 | isVideoAbuseReasonValid, | ||
49 | isVideoAbusePredefinedReasonValid, | ||
50 | isVideoAbusePredefinedReasonsValid, | ||
51 | isVideoAbuseTimestampValid, | ||
52 | isVideoAbuseTimestampCoherent, | ||
53 | isVideoAbuseModerationCommentValid, | ||
54 | isVideoAbuseStateValid, | ||
55 | isAbuseVideoIsValid | ||
56 | } | ||
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts index 846f28b17..455ff4241 100644 --- a/server/helpers/custom-validators/video-comments.ts +++ b/server/helpers/custom-validators/video-comments.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import 'multer' | 1 | import * as express from 'express' |
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
3 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
5 | import { MVideoId } from '@server/types/models' | ||
4 | 6 | ||
5 | const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS | 7 | const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS |
6 | 8 | ||
@@ -8,8 +10,83 @@ function isValidVideoCommentText (value: string) { | |||
8 | return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) | 10 | return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) |
9 | } | 11 | } |
10 | 12 | ||
13 | async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
14 | const id = parseInt(idArg + '', 10) | ||
15 | const videoComment = await VideoCommentModel.loadById(id) | ||
16 | |||
17 | if (!videoComment) { | ||
18 | res.status(404) | ||
19 | .json({ error: 'Video comment thread not found' }) | ||
20 | .end() | ||
21 | |||
22 | return false | ||
23 | } | ||
24 | |||
25 | if (videoComment.videoId !== video.id) { | ||
26 | res.status(400) | ||
27 | .json({ error: 'Video comment is not associated to this video.' }) | ||
28 | .end() | ||
29 | |||
30 | return false | ||
31 | } | ||
32 | |||
33 | if (videoComment.inReplyToCommentId !== null) { | ||
34 | res.status(400) | ||
35 | .json({ error: 'Video comment is not a thread.' }) | ||
36 | .end() | ||
37 | |||
38 | return false | ||
39 | } | ||
40 | |||
41 | res.locals.videoCommentThread = videoComment | ||
42 | return true | ||
43 | } | ||
44 | |||
45 | async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
46 | const id = parseInt(idArg + '', 10) | ||
47 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
48 | |||
49 | if (!videoComment) { | ||
50 | res.status(404) | ||
51 | .json({ error: 'Video comment thread not found' }) | ||
52 | .end() | ||
53 | |||
54 | return false | ||
55 | } | ||
56 | |||
57 | if (videoComment.videoId !== video.id) { | ||
58 | res.status(400) | ||
59 | .json({ error: 'Video comment is not associated to this video.' }) | ||
60 | .end() | ||
61 | |||
62 | return false | ||
63 | } | ||
64 | |||
65 | res.locals.videoCommentFull = videoComment | ||
66 | return true | ||
67 | } | ||
68 | |||
69 | async function doesCommentIdExist (idArg: number | string, res: express.Response) { | ||
70 | const id = parseInt(idArg + '', 10) | ||
71 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
72 | |||
73 | if (!videoComment) { | ||
74 | res.status(404) | ||
75 | .json({ error: 'Video comment thread not found' }) | ||
76 | |||
77 | return false | ||
78 | } | ||
79 | |||
80 | res.locals.videoCommentFull = videoComment | ||
81 | |||
82 | return true | ||
83 | } | ||
84 | |||
11 | // --------------------------------------------------------------------------- | 85 | // --------------------------------------------------------------------------- |
12 | 86 | ||
13 | export { | 87 | export { |
14 | isValidVideoCommentText | 88 | isValidVideoCommentText, |
89 | doesVideoCommentThreadExist, | ||
90 | doesVideoCommentExist, | ||
91 | doesCommentIdExist | ||
15 | } | 92 | } |
diff --git a/server/helpers/middlewares/abuses.ts b/server/helpers/middlewares/abuses.ts new file mode 100644 index 000000000..be8c8b449 --- /dev/null +++ b/server/helpers/middlewares/abuses.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import { Response } from 'express' | ||
2 | import { AbuseModel } from '../../models/abuse/abuse' | ||
3 | import { fetchVideo } from '../video' | ||
4 | |||
5 | // FIXME: deprecated in 2.3. Remove this function | ||
6 | async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) { | ||
7 | const abuseId = parseInt(abuseIdArg + '', 10) | ||
8 | let abuse = await AbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID) | ||
9 | |||
10 | if (!abuse) { | ||
11 | const userId = res.locals.oauth?.token.User.id | ||
12 | const video = await fetchVideo(videoUUID, 'all', userId) | ||
13 | |||
14 | if (video) abuse = await AbuseModel.loadByIdAndVideoId(abuseId, video.id) | ||
15 | } | ||
16 | |||
17 | if (abuse === null) { | ||
18 | res.status(404) | ||
19 | .json({ error: 'Video abuse not found' }) | ||
20 | |||
21 | return false | ||
22 | } | ||
23 | |||
24 | res.locals.abuse = abuse | ||
25 | return true | ||
26 | } | ||
27 | |||
28 | async function doesAbuseExist (abuseId: number | string, res: Response) { | ||
29 | const abuse = await AbuseModel.loadById(parseInt(abuseId + '', 10)) | ||
30 | |||
31 | if (!abuse) { | ||
32 | res.status(404) | ||
33 | .json({ error: 'Abuse not found' }) | ||
34 | |||
35 | return false | ||
36 | } | ||
37 | |||
38 | res.locals.abuse = abuse | ||
39 | return true | ||
40 | } | ||
41 | |||
42 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | export { | ||
45 | doesAbuseExist, | ||
46 | doesVideoAbuseExist | ||
47 | } | ||
diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts index bddea7eaa..29b4ed1a6 100644 --- a/server/helpers/middlewares/accounts.ts +++ b/server/helpers/middlewares/accounts.ts | |||
@@ -3,8 +3,8 @@ import { AccountModel } from '../../models/account/account' | |||
3 | import * as Bluebird from 'bluebird' | 3 | import * as Bluebird from 'bluebird' |
4 | import { MAccountDefault } from '../../types/models' | 4 | import { MAccountDefault } from '../../types/models' |
5 | 5 | ||
6 | function doesAccountIdExist (id: number, res: Response, sendNotFound = true) { | 6 | function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) { |
7 | const promise = AccountModel.load(id) | 7 | const promise = AccountModel.load(parseInt(id + '', 10)) |
8 | 8 | ||
9 | return doesAccountExist(promise, res, sendNotFound) | 9 | return doesAccountExist(promise, res, sendNotFound) |
10 | } | 10 | } |
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 @@ | |||
1 | export * from './abuses' | ||
1 | export * from './accounts' | 2 | export * from './accounts' |
2 | export * from './video-abuses' | ||
3 | export * from './video-blacklists' | 3 | export * from './video-blacklists' |
4 | export * from './video-captions' | 4 | export * from './video-captions' |
5 | export * from './video-channels' | 5 | export * from './video-channels' |
diff --git a/server/helpers/middlewares/video-abuses.ts b/server/helpers/middlewares/video-abuses.ts deleted file mode 100644 index 97a5724b6..000000000 --- a/server/helpers/middlewares/video-abuses.ts +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | import { Response } from 'express' | ||
2 | import { VideoAbuseModel } from '../../models/video/video-abuse' | ||
3 | import { fetchVideo } from '../video' | ||
4 | |||
5 | async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) { | ||
6 | const abuseId = parseInt(abuseIdArg + '', 10) | ||
7 | let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID) | ||
8 | |||
9 | if (!videoAbuse) { | ||
10 | const userId = res.locals.oauth?.token.User.id | ||
11 | const video = await fetchVideo(videoUUID, 'all', userId) | ||
12 | |||
13 | if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id) | ||
14 | } | ||
15 | |||
16 | if (videoAbuse === null) { | ||
17 | res.status(404) | ||
18 | .json({ error: 'Video abuse not found' }) | ||
19 | .end() | ||
20 | |||
21 | return false | ||
22 | } | ||
23 | |||
24 | res.locals.videoAbuse = videoAbuse | ||
25 | return true | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | export { | ||
31 | doesVideoAbuseExist | ||
32 | } | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index e730e3c84..2e9d3956e 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -1,9 +1,17 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { randomBytes } from 'crypto' | 2 | import { randomBytes } from 'crypto' |
3 | import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models' | ||
4 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 3 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
5 | import { FollowState } from '../../shared/models/actors' | 4 | import { FollowState } from '../../shared/models/actors' |
6 | import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' | 5 | import { |
6 | AbuseState, | ||
7 | VideoImportState, | ||
8 | VideoPrivacy, | ||
9 | VideoTranscodingFPS, | ||
10 | JobType, | ||
11 | VideoRateType, | ||
12 | VideoResolution, | ||
13 | VideoState | ||
14 | } from '../../shared/models' | ||
7 | // Do not use barrels, remain constants as independent as possible | 15 | // Do not use barrels, remain constants as independent as possible |
8 | import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils' | 16 | import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils' |
9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 17 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
@@ -15,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
15 | 23 | ||
16 | // --------------------------------------------------------------------------- | 24 | // --------------------------------------------------------------------------- |
17 | 25 | ||
18 | const LAST_MIGRATION_VERSION = 515 | 26 | const LAST_MIGRATION_VERSION = 520 |
19 | 27 | ||
20 | // --------------------------------------------------------------------------- | 28 | // --------------------------------------------------------------------------- |
21 | 29 | ||
@@ -51,7 +59,6 @@ const SORTABLE_COLUMNS = { | |||
51 | USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], | 59 | USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], |
52 | ACCOUNTS: [ 'createdAt' ], | 60 | ACCOUNTS: [ 'createdAt' ], |
53 | JOBS: [ 'createdAt' ], | 61 | JOBS: [ 'createdAt' ], |
54 | VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ], | ||
55 | VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], | 62 | VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], |
56 | VIDEO_IMPORTS: [ 'createdAt' ], | 63 | VIDEO_IMPORTS: [ 'createdAt' ], |
57 | VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], | 64 | VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], |
@@ -66,6 +73,8 @@ const SORTABLE_COLUMNS = { | |||
66 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], | 73 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], |
67 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], | 74 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], |
68 | 75 | ||
76 | ABUSES: [ 'id', 'createdAt', 'state' ], | ||
77 | |||
69 | ACCOUNTS_BLOCKLIST: [ 'createdAt' ], | 78 | ACCOUNTS_BLOCKLIST: [ 'createdAt' ], |
70 | SERVERS_BLOCKLIST: [ 'createdAt' ], | 79 | SERVERS_BLOCKLIST: [ 'createdAt' ], |
71 | 80 | ||
@@ -193,7 +202,7 @@ const CONSTRAINTS_FIELDS = { | |||
193 | VIDEO_LANGUAGES: { max: 500 }, // Array length | 202 | VIDEO_LANGUAGES: { max: 500 }, // Array length |
194 | BLOCKED_REASON: { min: 3, max: 250 } // Length | 203 | BLOCKED_REASON: { min: 3, max: 250 } // Length |
195 | }, | 204 | }, |
196 | VIDEO_ABUSES: { | 205 | ABUSES: { |
197 | REASON: { min: 2, max: 3000 }, // Length | 206 | REASON: { min: 2, max: 3000 }, // Length |
198 | MODERATION_COMMENT: { min: 2, max: 3000 } // Length | 207 | MODERATION_COMMENT: { min: 2, max: 3000 } // Length |
199 | }, | 208 | }, |
@@ -378,10 +387,10 @@ const VIDEO_IMPORT_STATES = { | |||
378 | [VideoImportState.REJECTED]: 'Rejected' | 387 | [VideoImportState.REJECTED]: 'Rejected' |
379 | } | 388 | } |
380 | 389 | ||
381 | const VIDEO_ABUSE_STATES = { | 390 | const ABUSE_STATES = { |
382 | [VideoAbuseState.PENDING]: 'Pending', | 391 | [AbuseState.PENDING]: 'Pending', |
383 | [VideoAbuseState.REJECTED]: 'Rejected', | 392 | [AbuseState.REJECTED]: 'Rejected', |
384 | [VideoAbuseState.ACCEPTED]: 'Accepted' | 393 | [AbuseState.ACCEPTED]: 'Accepted' |
385 | } | 394 | } |
386 | 395 | ||
387 | const VIDEO_PLAYLIST_PRIVACIES = { | 396 | const VIDEO_PLAYLIST_PRIVACIES = { |
@@ -778,7 +787,7 @@ export { | |||
778 | VIDEO_RATE_TYPES, | 787 | VIDEO_RATE_TYPES, |
779 | VIDEO_TRANSCODING_FPS, | 788 | VIDEO_TRANSCODING_FPS, |
780 | FFMPEG_NICE, | 789 | FFMPEG_NICE, |
781 | VIDEO_ABUSE_STATES, | 790 | ABUSE_STATES, |
782 | VIDEO_CHANNELS, | 791 | VIDEO_CHANNELS, |
783 | LRU_CACHE, | 792 | LRU_CACHE, |
784 | JOB_REQUEST_TIMEOUT, | 793 | 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 @@ | |||
1 | import { QueryTypes, Transaction } from 'sequelize' | ||
1 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' | 2 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' |
3 | import { AbuseModel } from '@server/models/abuse/abuse' | ||
4 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' | ||
5 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' | ||
2 | import { isTestInstance } from '../helpers/core-utils' | 6 | import { isTestInstance } from '../helpers/core-utils' |
3 | import { logger } from '../helpers/logger' | 7 | import { logger } from '../helpers/logger' |
4 | |||
5 | import { AccountModel } from '../models/account/account' | 8 | import { AccountModel } from '../models/account/account' |
9 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | ||
6 | import { AccountVideoRateModel } from '../models/account/account-video-rate' | 10 | import { AccountVideoRateModel } from '../models/account/account-video-rate' |
7 | import { UserModel } from '../models/account/user' | 11 | import { UserModel } from '../models/account/user' |
12 | import { UserNotificationModel } from '../models/account/user-notification' | ||
13 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | ||
14 | import { UserVideoHistoryModel } from '../models/account/user-video-history' | ||
8 | import { ActorModel } from '../models/activitypub/actor' | 15 | import { ActorModel } from '../models/activitypub/actor' |
9 | import { ActorFollowModel } from '../models/activitypub/actor-follow' | 16 | import { ActorFollowModel } from '../models/activitypub/actor-follow' |
10 | import { ApplicationModel } from '../models/application/application' | 17 | import { ApplicationModel } from '../models/application/application' |
11 | import { AvatarModel } from '../models/avatar/avatar' | 18 | import { AvatarModel } from '../models/avatar/avatar' |
12 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 19 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
13 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 20 | import { OAuthTokenModel } from '../models/oauth/oauth-token' |
21 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | ||
22 | import { PluginModel } from '../models/server/plugin' | ||
14 | import { ServerModel } from '../models/server/server' | 23 | import { ServerModel } from '../models/server/server' |
24 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | ||
25 | import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' | ||
15 | import { TagModel } from '../models/video/tag' | 26 | import { TagModel } from '../models/video/tag' |
27 | import { ThumbnailModel } from '../models/video/thumbnail' | ||
16 | import { VideoModel } from '../models/video/video' | 28 | import { VideoModel } from '../models/video/video' |
17 | import { VideoAbuseModel } from '../models/video/video-abuse' | ||
18 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | 29 | import { VideoBlacklistModel } from '../models/video/video-blacklist' |
30 | import { VideoCaptionModel } from '../models/video/video-caption' | ||
31 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' | ||
19 | import { VideoChannelModel } from '../models/video/video-channel' | 32 | import { VideoChannelModel } from '../models/video/video-channel' |
20 | import { VideoCommentModel } from '../models/video/video-comment' | 33 | import { VideoCommentModel } from '../models/video/video-comment' |
21 | import { VideoFileModel } from '../models/video/video-file' | 34 | import { VideoFileModel } from '../models/video/video-file' |
22 | import { VideoShareModel } from '../models/video/video-share' | ||
23 | import { VideoTagModel } from '../models/video/video-tag' | ||
24 | import { CONFIG } from './config' | ||
25 | import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' | ||
26 | import { VideoCaptionModel } from '../models/video/video-caption' | ||
27 | import { VideoImportModel } from '../models/video/video-import' | 35 | import { VideoImportModel } from '../models/video/video-import' |
28 | import { VideoViewModel } from '../models/video/video-view' | ||
29 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' | ||
30 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | ||
31 | import { UserVideoHistoryModel } from '../models/account/user-video-history' | ||
32 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | ||
33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | ||
34 | import { UserNotificationModel } from '../models/account/user-notification' | ||
35 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | ||
36 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
37 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 36 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
38 | import { VideoPlaylistElementModel } from '../models/video/video-playlist-element' | 37 | import { VideoPlaylistElementModel } from '../models/video/video-playlist-element' |
39 | import { ThumbnailModel } from '../models/video/thumbnail' | 38 | import { VideoShareModel } from '../models/video/video-share' |
40 | import { PluginModel } from '../models/server/plugin' | 39 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
41 | import { QueryTypes, Transaction } from 'sequelize' | 40 | import { VideoTagModel } from '../models/video/video-tag' |
41 | import { VideoViewModel } from '../models/video/video-view' | ||
42 | import { CONFIG } from './config' | ||
42 | 43 | ||
43 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 44 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
44 | 45 | ||
@@ -86,6 +87,8 @@ async function initDatabaseModels (silent: boolean) { | |||
86 | TagModel, | 87 | TagModel, |
87 | AccountVideoRateModel, | 88 | AccountVideoRateModel, |
88 | UserModel, | 89 | UserModel, |
90 | AbuseModel, | ||
91 | VideoCommentAbuseModel, | ||
89 | VideoAbuseModel, | 92 | VideoAbuseModel, |
90 | VideoModel, | 93 | VideoModel, |
91 | VideoChangeOwnershipModel, | 94 | 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { VideoAbuseState } from '../../../shared/models/videos' | 2 | import { AbuseState } from '../../../shared/models' |
3 | 3 | ||
4 | async function up (utils: { | 4 | async function up (utils: { |
5 | transaction: Sequelize.Transaction | 5 | transaction: Sequelize.Transaction |
@@ -16,7 +16,7 @@ async function up (utils: { | |||
16 | } | 16 | } |
17 | 17 | ||
18 | { | 18 | { |
19 | const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING | 19 | const query = 'UPDATE "videoAbuse" SET "state" = ' + AbuseState.PENDING |
20 | await utils.sequelize.query(query) | 20 | await utils.sequelize.query(query) |
21 | } | 21 | } |
22 | 22 | ||
diff --git a/server/initializers/migrations/0470-cleaup-indexes.ts b/server/initializers/migrations/0470-cleanup-indexes.ts index 7365c30f8..7365c30f8 100644 --- a/server/initializers/migrations/0470-cleaup-indexes.ts +++ b/server/initializers/migrations/0470-cleanup-indexes.ts | |||
diff --git a/server/initializers/migrations/0520-abuses-split.ts b/server/initializers/migrations/0520-abuses-split.ts new file mode 100644 index 000000000..b02a21989 --- /dev/null +++ b/server/initializers/migrations/0520-abuses-split.ts | |||
@@ -0,0 +1,90 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | await utils.queryInterface.renameTable('videoAbuse', 'abuse') | ||
9 | |||
10 | await utils.sequelize.query(` | ||
11 | ALTER TABLE "abuse" | ||
12 | ADD COLUMN "flaggedAccountId" INTEGER REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE | ||
13 | `) | ||
14 | |||
15 | await utils.sequelize.query(` | ||
16 | UPDATE "abuse" SET "videoId" = NULL | ||
17 | WHERE "videoId" NOT IN (SELECT "id" FROM "video") | ||
18 | `) | ||
19 | |||
20 | await utils.sequelize.query(` | ||
21 | UPDATE "abuse" SET "flaggedAccountId" = "videoChannel"."accountId" | ||
22 | FROM "video" INNER JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id" | ||
23 | WHERE "abuse"."videoId" = "video"."id" | ||
24 | `) | ||
25 | |||
26 | await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_video_id;') | ||
27 | await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_reporter_account_id;') | ||
28 | |||
29 | await utils.sequelize.query(` | ||
30 | CREATE TABLE IF NOT EXISTS "videoAbuse" ( | ||
31 | "id" serial, | ||
32 | "startAt" integer DEFAULT NULL, | ||
33 | "endAt" integer DEFAULT NULL, | ||
34 | "deletedVideo" jsonb DEFAULT NULL, | ||
35 | "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
36 | "videoId" integer REFERENCES "video" ("id") ON DELETE SET NULL ON UPDATE CASCADE, | ||
37 | "createdAt" TIMESTAMP WITH time zone NOT NULL, | ||
38 | "updatedAt" timestamp WITH time zone NOT NULL, | ||
39 | PRIMARY KEY ("id") | ||
40 | ); | ||
41 | `) | ||
42 | |||
43 | await utils.sequelize.query(` | ||
44 | CREATE TABLE IF NOT EXISTS "commentAbuse" ( | ||
45 | "id" serial, | ||
46 | "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
47 | "videoCommentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE, | ||
48 | "createdAt" timestamp WITH time zone NOT NULL, | ||
49 | "updatedAt" timestamp WITH time zone NOT NULL, | ||
50 | PRIMARY KEY ("id") | ||
51 | ); | ||
52 | `) | ||
53 | |||
54 | await utils.sequelize.query(` | ||
55 | INSERT INTO "videoAbuse" ("startAt", "endAt", "deletedVideo", "abuseId", "videoId", "createdAt", "updatedAt") | ||
56 | SELECT "abuse"."startAt", "abuse"."endAt", "abuse"."deletedVideo", "abuse"."id", "abuse"."videoId", | ||
57 | "abuse"."createdAt", "abuse"."updatedAt" | ||
58 | FROM "abuse" | ||
59 | `) | ||
60 | |||
61 | await utils.queryInterface.removeColumn('abuse', 'startAt') | ||
62 | await utils.queryInterface.removeColumn('abuse', 'endAt') | ||
63 | await utils.queryInterface.removeColumn('abuse', 'deletedVideo') | ||
64 | await utils.queryInterface.removeColumn('abuse', 'videoId') | ||
65 | |||
66 | await utils.sequelize.query('DROP INDEX IF EXISTS user_notification_video_abuse_id') | ||
67 | await utils.queryInterface.renameColumn('userNotification', 'videoAbuseId', 'abuseId') | ||
68 | await utils.sequelize.query( | ||
69 | 'ALTER TABLE "userNotification" RENAME CONSTRAINT "userNotification_videoAbuseId_fkey" TO "userNotification_abuseId_fkey"' | ||
70 | ) | ||
71 | |||
72 | await utils.sequelize.query( | ||
73 | 'ALTER TABLE "abuse" RENAME CONSTRAINT "videoAbuse_reporterAccountId_fkey" TO "abuse_reporterAccountId_fkey"' | ||
74 | ) | ||
75 | |||
76 | await utils.sequelize.query( | ||
77 | 'ALTER INDEX IF EXISTS "videoAbuse_pkey" RENAME TO "abuse_pkey"' | ||
78 | ) | ||
79 | |||
80 | await utils.queryInterface.renameColumn('userNotificationSetting', 'videoAbuseAsModerator', 'abuseAsModerator') | ||
81 | } | ||
82 | |||
83 | function down (options) { | ||
84 | throw new Error('Not implemented.') | ||
85 | } | ||
86 | |||
87 | export { | ||
88 | up, | ||
89 | down | ||
90 | } | ||
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 @@ | |||
1 | import { | 1 | import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation' |
2 | ActivityCreate, | 2 | import { AccountModel } from '@server/models/account/account' |
3 | ActivityFlag, | 3 | import { VideoModel } from '@server/models/video/video' |
4 | VideoAbuseState, | 4 | import { VideoCommentModel } from '@server/models/video/video-comment' |
5 | videoAbusePredefinedReasonsMap | 5 | import { AbuseObject, abusePredefinedReasonsMap, AbuseState, ActivityCreate, ActivityFlag } from '../../../../shared' |
6 | } from '../../../../shared' | 6 | import { getAPId } from '../../../helpers/activitypub' |
7 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' | ||
8 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
9 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
10 | import { sequelizeTypescript } from '../../../initializers/database' | 9 | import { sequelizeTypescript } from '../../../initializers/database' |
11 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
12 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
13 | import { Notifier } from '../../notifier' | ||
14 | import { getAPId } from '../../../helpers/activitypub' | ||
15 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 10 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
16 | import { MActorSignature, MVideoAbuseAccountVideo } from '../../../types/models' | 11 | import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' |
17 | import { AccountModel } from '@server/models/account/account' | ||
18 | 12 | ||
19 | async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { | 13 | async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { |
20 | const { activity, byActor } = options | 14 | const { activity, byActor } = options |
21 | return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor) | 15 | |
16 | return retryTransactionWrapper(processCreateAbuse, activity, byActor) | ||
22 | } | 17 | } |
23 | 18 | ||
24 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
@@ -29,55 +24,79 @@ export { | |||
29 | 24 | ||
30 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
31 | 26 | ||
32 | async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { | 27 | async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { |
33 | const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject) | 28 | const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject) |
34 | 29 | ||
35 | const account = byActor.Account | 30 | const account = byActor.Account |
36 | if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url) | 31 | if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) |
32 | |||
33 | const reporterAccount = await AccountModel.load(account.id) | ||
37 | 34 | ||
38 | const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ] | 35 | const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ] |
39 | 36 | ||
37 | const tags = Array.isArray(flag.tag) ? flag.tag : [] | ||
38 | const predefinedReasons = tags.map(tag => abusePredefinedReasonsMap[tag.name]) | ||
39 | .filter(v => !isNaN(v)) | ||
40 | |||
41 | const startAt = flag.startAt | ||
42 | const endAt = flag.endAt | ||
43 | |||
40 | for (const object of objects) { | 44 | for (const object of objects) { |
41 | try { | 45 | try { |
42 | logger.debug('Reporting remote abuse for video %s.', getAPId(object)) | 46 | const uri = getAPId(object) |
43 | |||
44 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) | ||
45 | const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t)) | ||
46 | const tags = Array.isArray(flag.tag) ? flag.tag : [] | ||
47 | const predefinedReasons = tags.map(tag => videoAbusePredefinedReasonsMap[tag.name]) | ||
48 | .filter(v => !isNaN(v)) | ||
49 | const startAt = flag.startAt | ||
50 | const endAt = flag.endAt | ||
51 | |||
52 | const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { | ||
53 | const videoAbuseData = { | ||
54 | reporterAccountId: account.id, | ||
55 | reason: flag.content, | ||
56 | videoId: video.id, | ||
57 | state: VideoAbuseState.PENDING, | ||
58 | predefinedReasons, | ||
59 | startAt, | ||
60 | endAt | ||
61 | } | ||
62 | 47 | ||
63 | const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) | 48 | logger.debug('Reporting remote abuse for object %s.', uri) |
64 | videoAbuseInstance.Video = video | ||
65 | videoAbuseInstance.Account = reporterAccount | ||
66 | 49 | ||
67 | logger.info('Remote abuse for video uuid %s created', flag.object) | 50 | await sequelizeTypescript.transaction(async t => { |
68 | 51 | ||
69 | return videoAbuseInstance | 52 | const video = await VideoModel.loadByUrlAndPopulateAccount(uri) |
70 | }) | 53 | let videoComment: MCommentOwnerVideo |
54 | let flaggedAccount: MAccountDefault | ||
55 | |||
56 | if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri) | ||
57 | if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri) | ||
58 | |||
59 | if (!video && !videoComment && !flaggedAccount) { | ||
60 | logger.warn('Cannot flag unknown entity %s.', object) | ||
61 | return | ||
62 | } | ||
63 | |||
64 | const baseAbuse = { | ||
65 | reporterAccountId: reporterAccount.id, | ||
66 | reason: flag.content, | ||
67 | state: AbuseState.PENDING, | ||
68 | predefinedReasons | ||
69 | } | ||
71 | 70 | ||
72 | const videoAbuseJSON = videoAbuseInstance.toFormattedJSON() | 71 | if (video) { |
72 | return createVideoAbuse({ | ||
73 | baseAbuse, | ||
74 | startAt, | ||
75 | endAt, | ||
76 | reporterAccount, | ||
77 | transaction: t, | ||
78 | videoInstance: video | ||
79 | }) | ||
80 | } | ||
81 | |||
82 | if (videoComment) { | ||
83 | return createVideoCommentAbuse({ | ||
84 | baseAbuse, | ||
85 | reporterAccount, | ||
86 | transaction: t, | ||
87 | commentInstance: videoComment | ||
88 | }) | ||
89 | } | ||
73 | 90 | ||
74 | Notifier.Instance.notifyOnNewVideoAbuse({ | 91 | return await createAccountAbuse({ |
75 | videoAbuse: videoAbuseJSON, | 92 | baseAbuse, |
76 | videoAbuseInstance, | 93 | reporterAccount, |
77 | reporter: reporterAccount.Actor.getIdentifier() | 94 | transaction: t, |
95 | accountInstance: flaggedAccount | ||
96 | }) | ||
78 | }) | 97 | }) |
79 | } catch (err) { | 98 | } catch (err) { |
80 | logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) | 99 | logger.debug('Cannot process report of %s', getAPId(object), { err }) |
81 | } | 100 | } |
82 | } | 101 | } |
83 | } | 102 | } |
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 @@ | |||
1 | import { getVideoAbuseActivityPubUrl } from '../url' | 1 | import { Transaction } from 'sequelize' |
2 | import { unicastTo } from './utils' | ||
3 | import { logger } from '../../../helpers/logger' | ||
4 | import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' | 2 | import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' |
3 | import { logger } from '../../../helpers/logger' | ||
4 | import { MAbuseAP, MAccountLight, MActor } from '../../../types/models' | ||
5 | import { audiencify, getAudience } from '../audience' | 5 | import { audiencify, getAudience } from '../audience' |
6 | import { Transaction } from 'sequelize' | 6 | import { getAbuseActivityPubUrl } from '../url' |
7 | import { MActor, MVideoFullLight } from '../../../types/models' | 7 | import { unicastTo } from './utils' |
8 | import { MVideoAbuseVideo } from '../../../types/models/video' | ||
9 | 8 | ||
10 | function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { | 9 | function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) { |
11 | if (!video.VideoChannel.Account.Actor.serverId) return // Local user | 10 | if (!flaggedAccount.Actor.serverId) return // Local user |
12 | 11 | ||
13 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | 12 | const url = getAbuseActivityPubUrl(abuse) |
14 | 13 | ||
15 | logger.info('Creating job to send video abuse %s.', url) | 14 | logger.info('Creating job to send abuse %s.', url) |
16 | 15 | ||
17 | // Custom audience, we only send the abuse to the origin instance | 16 | // Custom audience, we only send the abuse to the origin instance |
18 | const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } | 17 | const audience = { to: [ flaggedAccount.Actor.url ], cc: [] } |
19 | const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience) | 18 | const flagActivity = buildFlagActivity(url, byActor, abuse, audience) |
20 | 19 | ||
21 | t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.getSharedInbox())) | 20 | t.afterCommit(() => unicastTo(flagActivity, byActor, flaggedAccount.Actor.getSharedInbox())) |
22 | } | 21 | } |
23 | 22 | ||
24 | function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag { | 23 | function buildFlagActivity (url: string, byActor: MActor, abuse: MAbuseAP, audience: ActivityAudience): ActivityFlag { |
25 | if (!audience) audience = getAudience(byActor) | 24 | if (!audience) audience = getAudience(byActor) |
26 | 25 | ||
27 | const activity = Object.assign( | 26 | const activity = Object.assign( |
28 | { id: url, actor: byActor.url }, | 27 | { id: url, actor: byActor.url }, |
29 | videoAbuse.toActivityPubObject() | 28 | abuse.toActivityPubObject() |
30 | ) | 29 | ) |
31 | 30 | ||
32 | return audiencify(activity, audience) | 31 | return audiencify(activity, audience) |
@@ -35,5 +34,5 @@ function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbus | |||
35 | // --------------------------------------------------------------------------- | 34 | // --------------------------------------------------------------------------- |
36 | 35 | ||
37 | export { | 36 | export { |
38 | sendVideoAbuse | 37 | sendAbuse |
39 | } | 38 | } |
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 { | |||
5 | MActorId, | 5 | MActorId, |
6 | MActorUrl, | 6 | MActorUrl, |
7 | MCommentId, | 7 | MCommentId, |
8 | MVideoAbuseId, | ||
9 | MVideoId, | 8 | MVideoId, |
10 | MVideoUrl, | 9 | MVideoUrl, |
11 | MVideoUUID | 10 | MVideoUUID, |
11 | MAbuseId | ||
12 | } from '../../types/models' | 12 | } from '../../types/models' |
13 | import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist' | 13 | import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist' |
14 | import { MVideoFileVideoUUID } from '../../types/models/video/video-file' | 14 | import { MVideoFileVideoUUID } from '../../types/models/video/video-file' |
@@ -48,8 +48,8 @@ function getAccountActivityPubUrl (accountName: string) { | |||
48 | return WEBSERVER.URL + '/accounts/' + accountName | 48 | return WEBSERVER.URL + '/accounts/' + accountName |
49 | } | 49 | } |
50 | 50 | ||
51 | function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) { | 51 | function getAbuseActivityPubUrl (abuse: MAbuseId) { |
52 | return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id | 52 | return WEBSERVER.URL + '/admin/abuses/' + abuse.id |
53 | } | 53 | } |
54 | 54 | ||
55 | function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) { | 55 | function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) { |
@@ -118,7 +118,7 @@ export { | |||
118 | getVideoCacheStreamingPlaylistActivityPubUrl, | 118 | getVideoCacheStreamingPlaylistActivityPubUrl, |
119 | getVideoChannelActivityPubUrl, | 119 | getVideoChannelActivityPubUrl, |
120 | getAccountActivityPubUrl, | 120 | getAccountActivityPubUrl, |
121 | getVideoAbuseActivityPubUrl, | 121 | getAbuseActivityPubUrl, |
122 | getActorFollowActivityPubUrl, | 122 | getActorFollowActivityPubUrl, |
123 | getActorFollowAcceptActivityPubUrl, | 123 | getActorFollowAcceptActivityPubUrl, |
124 | getVideoAnnounceActivityPubUrl, | 124 | getVideoAnnounceActivityPubUrl, |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index c08732b48..d54eab966 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -1,26 +1,20 @@ | |||
1 | import { readFileSync } from 'fs-extra' | ||
2 | import { merge } from 'lodash' | ||
1 | import { createTransport, Transporter } from 'nodemailer' | 3 | import { createTransport, Transporter } from 'nodemailer' |
4 | import { join } from 'path' | ||
5 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
6 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | ||
7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' | ||
8 | import { Abuse, EmailPayload } from '@shared/models' | ||
9 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' | ||
2 | import { isTestInstance, root } from '../helpers/core-utils' | 10 | import { isTestInstance, root } from '../helpers/core-utils' |
3 | import { bunyanLogger, logger } from '../helpers/logger' | 11 | import { bunyanLogger, logger } from '../helpers/logger' |
4 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 12 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
5 | import { JobQueue } from './job-queue' | ||
6 | import { readFileSync } from 'fs-extra' | ||
7 | import { WEBSERVER } from '../initializers/constants' | 13 | import { WEBSERVER } from '../initializers/constants' |
8 | import { | 14 | import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' |
9 | MCommentOwnerVideo, | 15 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' |
10 | MVideo, | 16 | import { JobQueue } from './job-queue' |
11 | MVideoAbuseVideo, | 17 | |
12 | MVideoAccountLight, | ||
13 | MVideoBlacklistLightVideo, | ||
14 | MVideoBlacklistVideo | ||
15 | } from '../types/models/video' | ||
16 | import { MActorFollowActors, MActorFollowFull, MUser } from '../types/models' | ||
17 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' | ||
18 | import { EmailPayload } from '@shared/models' | ||
19 | import { join } from 'path' | ||
20 | import { VideoAbuse } from '../../shared/models/videos' | ||
21 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' | ||
22 | import { merge } from 'lodash' | ||
23 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
24 | const Email = require('email-templates') | 18 | const Email = require('email-templates') |
25 | 19 | ||
26 | class Emailer { | 20 | class Emailer { |
@@ -288,28 +282,74 @@ class Emailer { | |||
288 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 282 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
289 | } | 283 | } |
290 | 284 | ||
291 | addVideoAbuseModeratorsNotification (to: string[], parameters: { | 285 | addAbuseModeratorsNotification (to: string[], parameters: { |
292 | videoAbuse: VideoAbuse | 286 | abuse: Abuse |
293 | videoAbuseInstance: MVideoAbuseVideo | 287 | abuseInstance: MAbuseFull |
294 | reporter: string | 288 | reporter: string |
295 | }) { | 289 | }) { |
296 | const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id | 290 | const { abuse, abuseInstance, reporter } = parameters |
297 | const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath() | ||
298 | 291 | ||
299 | const emailPayload: EmailPayload = { | 292 | const action = { |
300 | template: 'video-abuse-new', | 293 | text: 'View report #' + abuse.id, |
301 | to, | 294 | url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id |
302 | subject: `New video abuse report from ${parameters.reporter}`, | 295 | } |
303 | locals: { | 296 | |
304 | videoUrl, | 297 | let emailPayload: EmailPayload |
305 | videoAbuseUrl, | 298 | |
306 | videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(), | 299 | if (abuseInstance.VideoAbuse) { |
307 | videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(), | 300 | const video = abuseInstance.VideoAbuse.Video |
308 | videoAbuse: parameters.videoAbuse, | 301 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() |
309 | reporter: parameters.reporter, | 302 | |
310 | action: { | 303 | emailPayload = { |
311 | text: 'View report #' + parameters.videoAbuse.id, | 304 | template: 'video-abuse-new', |
312 | url: videoAbuseUrl | 305 | to, |
306 | subject: `New video abuse report from ${reporter}`, | ||
307 | locals: { | ||
308 | videoUrl, | ||
309 | isLocal: video.remote === false, | ||
310 | videoCreatedAt: new Date(video.createdAt).toLocaleString(), | ||
311 | videoPublishedAt: new Date(video.publishedAt).toLocaleString(), | ||
312 | videoName: video.name, | ||
313 | reason: abuse.reason, | ||
314 | videoChannel: abuse.video.channel, | ||
315 | reporter, | ||
316 | action | ||
317 | } | ||
318 | } | ||
319 | } else if (abuseInstance.VideoCommentAbuse) { | ||
320 | const comment = abuseInstance.VideoCommentAbuse.VideoComment | ||
321 | const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId() | ||
322 | |||
323 | emailPayload = { | ||
324 | template: 'video-comment-abuse-new', | ||
325 | to, | ||
326 | subject: `New comment abuse report from ${reporter}`, | ||
327 | locals: { | ||
328 | commentUrl, | ||
329 | videoName: comment.Video.name, | ||
330 | isLocal: comment.isOwned(), | ||
331 | commentCreatedAt: new Date(comment.createdAt).toLocaleString(), | ||
332 | reason: abuse.reason, | ||
333 | flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(), | ||
334 | reporter, | ||
335 | action | ||
336 | } | ||
337 | } | ||
338 | } else { | ||
339 | const account = abuseInstance.FlaggedAccount | ||
340 | const accountUrl = account.getClientUrl() | ||
341 | |||
342 | emailPayload = { | ||
343 | template: 'account-abuse-new', | ||
344 | to, | ||
345 | subject: `New account abuse report from ${reporter}`, | ||
346 | locals: { | ||
347 | accountUrl, | ||
348 | accountDisplayName: account.getDisplayName(), | ||
349 | isLocal: account.isOwned(), | ||
350 | reason: abuse.reason, | ||
351 | reporter, | ||
352 | action | ||
313 | } | 353 | } |
314 | } | 354 | } |
315 | } | 355 | } |
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..f1aa2886e --- /dev/null +++ b/server/lib/emails/account-abuse-new/html.pug | |||
@@ -0,0 +1,14 @@ | |||
1 | extends ../common/greetings | ||
2 | include ../common/mixins.pug | ||
3 | |||
4 | block title | ||
5 | | An account is pending moderation | ||
6 | |||
7 | block content | ||
8 | p | ||
9 | | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}account | ||
10 | a(href=accountUrl) #{accountDisplayName} | ||
11 | |||
12 | p The reporter, #{reporter}, cited the following reason(s): | ||
13 | blockquote #{reason} | ||
14 | 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 @@ | |||
1 | mixin channel(channel) | 1 | mixin channel(channel) |
2 | - var handle = `${channel.name}@${channel.host}` | 2 | - var handle = `${channel.name}@${channel.host}` |
3 | | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] \ No newline at end of file | 3 | | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] |
4 | |||
5 | mixin account(account) | ||
6 | - var handle = `${account.name}@${account.host}` | ||
7 | | #[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 | |||
6 | 6 | ||
7 | block content | 7 | block content |
8 | p | 8 | p |
9 | | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video " | 9 | | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}video " |
10 | a(href=videoUrl) #{videoAbuse.video.name} | 10 | a(href=videoUrl) #{videoName} |
11 | | " by #[+channel(videoAbuse.video.channel)] | 11 | | " by #[+channel(videoChannel)] |
12 | if videoPublishedAt | 12 | if videoPublishedAt |
13 | | , published the #{videoPublishedAt}. | 13 | | , published the #{videoPublishedAt}. |
14 | else | 14 | else |
15 | | , uploaded the #{videoCreatedAt} but not yet published. | 15 | | , uploaded the #{videoCreatedAt} but not yet published. |
16 | p The reporter, #{reporter}, cited the following reason(s): | 16 | p The reporter, #{reporter}, cited the following reason(s): |
17 | blockquote #{videoAbuse.reason} | 17 | blockquote #{reason} |
18 | br(style="display: none;") | 18 | 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..e92d986b5 --- /dev/null +++ b/server/lib/emails/video-comment-abuse-new/html.pug | |||
@@ -0,0 +1,16 @@ | |||
1 | extends ../common/greetings | ||
2 | include ../common/mixins.pug | ||
3 | |||
4 | block title | ||
5 | | A comment is pending moderation | ||
6 | |||
7 | block content | ||
8 | p | ||
9 | | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '} | ||
10 | a(href=commentUrl) comment on video "#{videoName}" | ||
11 | | of #{flaggedAccount} | ||
12 | | created on #{commentCreatedAt} | ||
13 | |||
14 | p The reporter, #{reporter}, cited the following reason(s): | ||
15 | blockquote #{reason} | ||
16 | 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 @@ | |||
1 | import { VideoModel } from '../models/video/video' | 1 | import { PathLike } from 'fs-extra' |
2 | import { VideoCommentModel } from '../models/video/video-comment' | 2 | import { Transaction } from 'sequelize/types' |
3 | import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' | 3 | import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger' |
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { AbuseModel } from '@server/models/abuse/abuse' | ||
6 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' | ||
7 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' | ||
8 | import { VideoFileModel } from '@server/models/video/video-file' | ||
9 | import { FilteredModelAttributes } from '@server/types' | ||
10 | import { | ||
11 | MAbuseFull, | ||
12 | MAccountDefault, | ||
13 | MAccountLight, | ||
14 | MCommentAbuseAccountVideo, | ||
15 | MCommentOwnerVideo, | ||
16 | MUser, | ||
17 | MVideoAbuseVideoFull, | ||
18 | MVideoAccountLightBlacklistAllFiles | ||
19 | } from '@server/types/models' | ||
20 | import { ActivityCreate } from '../../shared/models/activitypub' | ||
21 | import { VideoTorrentObject } from '../../shared/models/activitypub/objects' | ||
22 | import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' | ||
4 | import { VideoCreate, VideoImportCreate } from '../../shared/models/videos' | 23 | import { VideoCreate, VideoImportCreate } from '../../shared/models/videos' |
24 | import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' | ||
5 | import { UserModel } from '../models/account/user' | 25 | import { UserModel } from '../models/account/user' |
6 | import { VideoTorrentObject } from '../../shared/models/activitypub/objects' | ||
7 | import { ActivityCreate } from '../../shared/models/activitypub' | ||
8 | import { ActorModel } from '../models/activitypub/actor' | 26 | import { ActorModel } from '../models/activitypub/actor' |
9 | import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' | 27 | import { VideoModel } from '../models/video/video' |
10 | import { VideoFileModel } from '@server/models/video/video-file' | 28 | import { VideoCommentModel } from '../models/video/video-comment' |
11 | import { PathLike } from 'fs-extra' | 29 | import { sendAbuse } from './activitypub/send/send-flag' |
12 | import { MUser } from '@server/types/models' | 30 | import { Notifier } from './notifier' |
13 | 31 | ||
14 | export type AcceptResult = { | 32 | export type AcceptResult = { |
15 | accepted: boolean | 33 | accepted: boolean |
@@ -73,6 +91,89 @@ function isPostImportVideoAccepted (object: { | |||
73 | return { accepted: true } | 91 | return { accepted: true } |
74 | } | 92 | } |
75 | 93 | ||
94 | async function createVideoAbuse (options: { | ||
95 | baseAbuse: FilteredModelAttributes<AbuseModel> | ||
96 | videoInstance: MVideoAccountLightBlacklistAllFiles | ||
97 | startAt: number | ||
98 | endAt: number | ||
99 | transaction: Transaction | ||
100 | reporterAccount: MAccountDefault | ||
101 | }) { | ||
102 | const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount } = options | ||
103 | |||
104 | const associateFun = async (abuseInstance: MAbuseFull) => { | ||
105 | const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({ | ||
106 | abuseId: abuseInstance.id, | ||
107 | videoId: videoInstance.id, | ||
108 | startAt: startAt, | ||
109 | endAt: endAt | ||
110 | }, { transaction }) | ||
111 | |||
112 | videoAbuseInstance.Video = videoInstance | ||
113 | abuseInstance.VideoAbuse = videoAbuseInstance | ||
114 | |||
115 | return { isOwned: videoInstance.isOwned() } | ||
116 | } | ||
117 | |||
118 | return createAbuse({ | ||
119 | base: baseAbuse, | ||
120 | reporterAccount, | ||
121 | flaggedAccount: videoInstance.VideoChannel.Account, | ||
122 | transaction, | ||
123 | associateFun | ||
124 | }) | ||
125 | } | ||
126 | |||
127 | function createVideoCommentAbuse (options: { | ||
128 | baseAbuse: FilteredModelAttributes<AbuseModel> | ||
129 | commentInstance: MCommentOwnerVideo | ||
130 | transaction: Transaction | ||
131 | reporterAccount: MAccountDefault | ||
132 | }) { | ||
133 | const { baseAbuse, commentInstance, transaction, reporterAccount } = options | ||
134 | |||
135 | const associateFun = async (abuseInstance: MAbuseFull) => { | ||
136 | const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({ | ||
137 | abuseId: abuseInstance.id, | ||
138 | videoCommentId: commentInstance.id | ||
139 | }, { transaction }) | ||
140 | |||
141 | commentAbuseInstance.VideoComment = commentInstance | ||
142 | abuseInstance.VideoCommentAbuse = commentAbuseInstance | ||
143 | |||
144 | return { isOwned: commentInstance.isOwned() } | ||
145 | } | ||
146 | |||
147 | return createAbuse({ | ||
148 | base: baseAbuse, | ||
149 | reporterAccount, | ||
150 | flaggedAccount: commentInstance.Account, | ||
151 | transaction, | ||
152 | associateFun | ||
153 | }) | ||
154 | } | ||
155 | |||
156 | function createAccountAbuse (options: { | ||
157 | baseAbuse: FilteredModelAttributes<AbuseModel> | ||
158 | accountInstance: MAccountDefault | ||
159 | transaction: Transaction | ||
160 | reporterAccount: MAccountDefault | ||
161 | }) { | ||
162 | const { baseAbuse, accountInstance, transaction, reporterAccount } = options | ||
163 | |||
164 | const associateFun = async () => { | ||
165 | return { isOwned: accountInstance.isOwned() } | ||
166 | } | ||
167 | |||
168 | return createAbuse({ | ||
169 | base: baseAbuse, | ||
170 | reporterAccount, | ||
171 | flaggedAccount: accountInstance, | ||
172 | transaction, | ||
173 | associateFun | ||
174 | }) | ||
175 | } | ||
176 | |||
76 | export { | 177 | export { |
77 | isLocalVideoAccepted, | 178 | isLocalVideoAccepted, |
78 | isLocalVideoThreadAccepted, | 179 | isLocalVideoThreadAccepted, |
@@ -80,5 +181,48 @@ export { | |||
80 | isRemoteVideoCommentAccepted, | 181 | isRemoteVideoCommentAccepted, |
81 | isLocalVideoCommentReplyAccepted, | 182 | isLocalVideoCommentReplyAccepted, |
82 | isPreImportVideoAccepted, | 183 | isPreImportVideoAccepted, |
83 | isPostImportVideoAccepted | 184 | isPostImportVideoAccepted, |
185 | |||
186 | createAbuse, | ||
187 | createVideoAbuse, | ||
188 | createVideoCommentAbuse, | ||
189 | createAccountAbuse | ||
190 | } | ||
191 | |||
192 | // --------------------------------------------------------------------------- | ||
193 | |||
194 | async function createAbuse (options: { | ||
195 | base: FilteredModelAttributes<AbuseModel> | ||
196 | reporterAccount: MAccountDefault | ||
197 | flaggedAccount: MAccountLight | ||
198 | associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean} > | ||
199 | transaction: Transaction | ||
200 | }) { | ||
201 | const { base, reporterAccount, flaggedAccount, associateFun, transaction } = options | ||
202 | const auditLogger = auditLoggerFactory('abuse') | ||
203 | |||
204 | const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id }) | ||
205 | const abuseInstance: MAbuseFull = await AbuseModel.create(abuseAttributes, { transaction }) | ||
206 | |||
207 | abuseInstance.ReporterAccount = reporterAccount | ||
208 | abuseInstance.FlaggedAccount = flaggedAccount | ||
209 | |||
210 | const { isOwned } = await associateFun(abuseInstance) | ||
211 | |||
212 | if (isOwned === false) { | ||
213 | await sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction) | ||
214 | } | ||
215 | |||
216 | const abuseJSON = abuseInstance.toFormattedJSON() | ||
217 | auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON)) | ||
218 | |||
219 | Notifier.Instance.notifyOnNewAbuse({ | ||
220 | abuse: abuseJSON, | ||
221 | abuseInstance, | ||
222 | reporter: reporterAccount.Actor.getIdentifier() | ||
223 | }) | ||
224 | |||
225 | logger.info('Abuse report %d created.', abuseInstance.id) | ||
226 | |||
227 | return abuseJSON | ||
84 | } | 228 | } |
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 943a087d2..c567e1c20 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -8,23 +8,18 @@ import { | |||
8 | MUserWithNotificationSetting, | 8 | MUserWithNotificationSetting, |
9 | UserNotificationModelForApi | 9 | UserNotificationModelForApi |
10 | } from '@server/types/models/user' | 10 | } from '@server/types/models/user' |
11 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | ||
11 | import { MVideoImportVideo } from '@server/types/models/video/video-import' | 12 | import { MVideoImportVideo } from '@server/types/models/video/video-import' |
13 | import { Abuse } from '@shared/models' | ||
12 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' | 14 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' |
13 | import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos' | 15 | import { VideoPrivacy, VideoState } from '../../shared/models/videos' |
14 | import { logger } from '../helpers/logger' | 16 | import { logger } from '../helpers/logger' |
15 | import { CONFIG } from '../initializers/config' | 17 | import { CONFIG } from '../initializers/config' |
16 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 18 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
17 | import { UserModel } from '../models/account/user' | 19 | import { UserModel } from '../models/account/user' |
18 | import { UserNotificationModel } from '../models/account/user-notification' | 20 | import { UserNotificationModel } from '../models/account/user-notification' |
19 | import { MAccountServer, MActorFollowFull } from '../types/models' | 21 | import { MAbuseFull, MAccountServer, MActorFollowFull } from '../types/models' |
20 | import { | 22 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' |
21 | MCommentOwnerVideo, | ||
22 | MVideoAbuseVideo, | ||
23 | MVideoAccountLight, | ||
24 | MVideoBlacklistLightVideo, | ||
25 | MVideoBlacklistVideo, | ||
26 | MVideoFullLight | ||
27 | } from '../types/models/video' | ||
28 | import { isBlockedByServerOrAccount } from './blocklist' | 23 | import { isBlockedByServerOrAccount } from './blocklist' |
29 | import { Emailer } from './emailer' | 24 | import { Emailer } from './emailer' |
30 | import { PeerTubeSocket } from './peertube-socket' | 25 | import { PeerTubeSocket } from './peertube-socket' |
@@ -78,9 +73,9 @@ class Notifier { | |||
78 | .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) | 73 | .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) |
79 | } | 74 | } |
80 | 75 | ||
81 | notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void { | 76 | notifyOnNewAbuse (parameters: { abuse: Abuse, abuseInstance: MAbuseFull, reporter: string }): void { |
82 | this.notifyModeratorsOfNewVideoAbuse(parameters) | 77 | this.notifyModeratorsOfNewAbuse(parameters) |
83 | .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err })) | 78 | .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err })) |
84 | } | 79 | } |
85 | 80 | ||
86 | notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { | 81 | notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { |
@@ -354,33 +349,39 @@ class Notifier { | |||
354 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | 349 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) |
355 | } | 350 | } |
356 | 351 | ||
357 | private async notifyModeratorsOfNewVideoAbuse (parameters: { | 352 | private async notifyModeratorsOfNewAbuse (parameters: { |
358 | videoAbuse: VideoAbuse | 353 | abuse: Abuse |
359 | videoAbuseInstance: MVideoAbuseVideo | 354 | abuseInstance: MAbuseFull |
360 | reporter: string | 355 | reporter: string |
361 | }) { | 356 | }) { |
362 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) | 357 | const { abuse, abuseInstance } = parameters |
358 | |||
359 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) | ||
363 | if (moderators.length === 0) return | 360 | if (moderators.length === 0) return |
364 | 361 | ||
365 | logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url) | 362 | const url = abuseInstance.VideoAbuse?.Video?.url || |
363 | abuseInstance.VideoCommentAbuse?.VideoComment?.url || | ||
364 | abuseInstance.FlaggedAccount.Actor.url | ||
365 | |||
366 | logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url) | ||
366 | 367 | ||
367 | function settingGetter (user: MUserWithNotificationSetting) { | 368 | function settingGetter (user: MUserWithNotificationSetting) { |
368 | return user.NotificationSetting.videoAbuseAsModerator | 369 | return user.NotificationSetting.abuseAsModerator |
369 | } | 370 | } |
370 | 371 | ||
371 | async function notificationCreator (user: MUserWithNotificationSetting) { | 372 | async function notificationCreator (user: MUserWithNotificationSetting) { |
372 | const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ | 373 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
373 | type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, | 374 | type: UserNotificationType.NEW_ABUSE_FOR_MODERATORS, |
374 | userId: user.id, | 375 | userId: user.id, |
375 | videoAbuseId: parameters.videoAbuse.id | 376 | abuseId: abuse.id |
376 | }) | 377 | }) |
377 | notification.VideoAbuse = parameters.videoAbuseInstance | 378 | notification.Abuse = abuseInstance |
378 | 379 | ||
379 | return notification | 380 | return notification |
380 | } | 381 | } |
381 | 382 | ||
382 | function emailSender (emails: string[]) { | 383 | function emailSender (emails: string[]) { |
383 | return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters) | 384 | return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters) |
384 | } | 385 | } |
385 | 386 | ||
386 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 387 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
diff --git a/server/lib/user.ts b/server/lib/user.ts index 43eef8ab1..642549879 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -133,7 +133,7 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | | |||
133 | newCommentOnMyVideo: UserNotificationSettingValue.WEB, | 133 | newCommentOnMyVideo: UserNotificationSettingValue.WEB, |
134 | myVideoImportFinished: UserNotificationSettingValue.WEB, | 134 | myVideoImportFinished: UserNotificationSettingValue.WEB, |
135 | myVideoPublished: UserNotificationSettingValue.WEB, | 135 | myVideoPublished: UserNotificationSettingValue.WEB, |
136 | videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 136 | abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
137 | videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 137 | videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
138 | blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 138 | blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
139 | newUserRegistration: UserNotificationSettingValue.WEB, | 139 | newUserRegistration: UserNotificationSettingValue.WEB, |
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts new file mode 100644 index 000000000..966d1f7fb --- /dev/null +++ b/server/middlewares/validators/abuse.ts | |||
@@ -0,0 +1,277 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { | ||
4 | isAbuseFilterValid, | ||
5 | isAbuseModerationCommentValid, | ||
6 | isAbusePredefinedReasonsValid, | ||
7 | isAbusePredefinedReasonValid, | ||
8 | isAbuseReasonValid, | ||
9 | isAbuseStateValid, | ||
10 | isAbuseTimestampCoherent, | ||
11 | isAbuseTimestampValid, | ||
12 | isAbuseVideoIsValid | ||
13 | } from '@server/helpers/custom-validators/abuses' | ||
14 | import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc' | ||
15 | import { doesCommentIdExist } from '@server/helpers/custom-validators/video-comments' | ||
16 | import { logger } from '@server/helpers/logger' | ||
17 | import { doesAbuseExist, doesAccountIdExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares' | ||
18 | import { AbuseCreate } from '@shared/models' | ||
19 | import { areValidationErrors } from './utils' | ||
20 | |||
21 | const abuseReportValidator = [ | ||
22 | body('account.id') | ||
23 | .optional() | ||
24 | .custom(isIdValid) | ||
25 | .withMessage('Should have a valid accountId'), | ||
26 | |||
27 | body('video.id') | ||
28 | .optional() | ||
29 | .custom(isIdOrUUIDValid) | ||
30 | .withMessage('Should have a valid videoId'), | ||
31 | body('video.startAt') | ||
32 | .optional() | ||
33 | .customSanitizer(toIntOrNull) | ||
34 | .custom(isAbuseTimestampValid) | ||
35 | .withMessage('Should have valid starting time value'), | ||
36 | body('video.endAt') | ||
37 | .optional() | ||
38 | .customSanitizer(toIntOrNull) | ||
39 | .custom(isAbuseTimestampValid) | ||
40 | .withMessage('Should have valid ending time value') | ||
41 | .bail() | ||
42 | .custom(isAbuseTimestampCoherent) | ||
43 | .withMessage('Should have a startAt timestamp beginning before endAt'), | ||
44 | |||
45 | body('comment.id') | ||
46 | .optional() | ||
47 | .custom(isIdValid) | ||
48 | .withMessage('Should have a valid commentId'), | ||
49 | |||
50 | body('reason') | ||
51 | .custom(isAbuseReasonValid) | ||
52 | .withMessage('Should have a valid reason'), | ||
53 | |||
54 | body('predefinedReasons') | ||
55 | .optional() | ||
56 | .custom(isAbusePredefinedReasonsValid) | ||
57 | .withMessage('Should have a valid list of predefined reasons'), | ||
58 | |||
59 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
60 | logger.debug('Checking abuseReport parameters', { parameters: req.body }) | ||
61 | |||
62 | if (areValidationErrors(req, res)) return | ||
63 | |||
64 | const body: AbuseCreate = req.body | ||
65 | |||
66 | if (body.video?.id && !await doesVideoExist(body.video.id, res)) return | ||
67 | if (body.account?.id && !await doesAccountIdExist(body.account.id, res)) return | ||
68 | if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return | ||
69 | |||
70 | if (!body.video?.id && !body.account?.id && !body.comment?.id) { | ||
71 | res.status(400) | ||
72 | .json({ error: 'video id or account id or comment id is required.' }) | ||
73 | |||
74 | return | ||
75 | } | ||
76 | |||
77 | return next() | ||
78 | } | ||
79 | ] | ||
80 | |||
81 | const abuseGetValidator = [ | ||
82 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), | ||
83 | |||
84 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
85 | logger.debug('Checking abuseGetValidator parameters', { parameters: req.body }) | ||
86 | |||
87 | if (areValidationErrors(req, res)) return | ||
88 | if (!await doesAbuseExist(req.params.id, res)) return | ||
89 | |||
90 | return next() | ||
91 | } | ||
92 | ] | ||
93 | |||
94 | const abuseUpdateValidator = [ | ||
95 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), | ||
96 | |||
97 | body('state') | ||
98 | .optional() | ||
99 | .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'), | ||
100 | body('moderationComment') | ||
101 | .optional() | ||
102 | .custom(isAbuseModerationCommentValid).withMessage('Should have a valid moderation comment'), | ||
103 | |||
104 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
105 | logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body }) | ||
106 | |||
107 | if (areValidationErrors(req, res)) return | ||
108 | if (!await doesAbuseExist(req.params.id, res)) return | ||
109 | |||
110 | return next() | ||
111 | } | ||
112 | ] | ||
113 | |||
114 | const abuseListValidator = [ | ||
115 | query('id') | ||
116 | .optional() | ||
117 | .custom(isIdValid).withMessage('Should have a valid id'), | ||
118 | query('filter') | ||
119 | .optional() | ||
120 | .custom(isAbuseFilterValid) | ||
121 | .withMessage('Should have a valid filter'), | ||
122 | query('predefinedReason') | ||
123 | .optional() | ||
124 | .custom(isAbusePredefinedReasonValid) | ||
125 | .withMessage('Should have a valid predefinedReason'), | ||
126 | query('search') | ||
127 | .optional() | ||
128 | .custom(exists).withMessage('Should have a valid search'), | ||
129 | query('state') | ||
130 | .optional() | ||
131 | .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'), | ||
132 | query('videoIs') | ||
133 | .optional() | ||
134 | .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'), | ||
135 | query('searchReporter') | ||
136 | .optional() | ||
137 | .custom(exists).withMessage('Should have a valid reporter search'), | ||
138 | query('searchReportee') | ||
139 | .optional() | ||
140 | .custom(exists).withMessage('Should have a valid reportee search'), | ||
141 | query('searchVideo') | ||
142 | .optional() | ||
143 | .custom(exists).withMessage('Should have a valid video search'), | ||
144 | query('searchVideoChannel') | ||
145 | .optional() | ||
146 | .custom(exists).withMessage('Should have a valid video channel search'), | ||
147 | |||
148 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
149 | logger.debug('Checking abuseListValidator parameters', { parameters: req.body }) | ||
150 | |||
151 | if (areValidationErrors(req, res)) return | ||
152 | |||
153 | return next() | ||
154 | } | ||
155 | ] | ||
156 | |||
157 | // FIXME: deprecated in 2.3. Remove these validators | ||
158 | |||
159 | const videoAbuseReportValidator = [ | ||
160 | param('videoId') | ||
161 | .custom(isIdOrUUIDValid) | ||
162 | .not() | ||
163 | .isEmpty() | ||
164 | .withMessage('Should have a valid videoId'), | ||
165 | body('reason') | ||
166 | .custom(isAbuseReasonValid) | ||
167 | .withMessage('Should have a valid reason'), | ||
168 | body('predefinedReasons') | ||
169 | .optional() | ||
170 | .custom(isAbusePredefinedReasonsValid) | ||
171 | .withMessage('Should have a valid list of predefined reasons'), | ||
172 | body('startAt') | ||
173 | .optional() | ||
174 | .customSanitizer(toIntOrNull) | ||
175 | .custom(isAbuseTimestampValid) | ||
176 | .withMessage('Should have valid starting time value'), | ||
177 | body('endAt') | ||
178 | .optional() | ||
179 | .customSanitizer(toIntOrNull) | ||
180 | .custom(isAbuseTimestampValid) | ||
181 | .withMessage('Should have valid ending time value'), | ||
182 | |||
183 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
184 | logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) | ||
185 | |||
186 | if (areValidationErrors(req, res)) return | ||
187 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
188 | |||
189 | return next() | ||
190 | } | ||
191 | ] | ||
192 | |||
193 | const videoAbuseGetValidator = [ | ||
194 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
195 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), | ||
196 | |||
197 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
198 | logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) | ||
199 | |||
200 | if (areValidationErrors(req, res)) return | ||
201 | if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return | ||
202 | |||
203 | return next() | ||
204 | } | ||
205 | ] | ||
206 | |||
207 | const videoAbuseUpdateValidator = [ | ||
208 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
209 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), | ||
210 | body('state') | ||
211 | .optional() | ||
212 | .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), | ||
213 | body('moderationComment') | ||
214 | .optional() | ||
215 | .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), | ||
216 | |||
217 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
218 | logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) | ||
219 | |||
220 | if (areValidationErrors(req, res)) return | ||
221 | if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return | ||
222 | |||
223 | return next() | ||
224 | } | ||
225 | ] | ||
226 | |||
227 | const videoAbuseListValidator = [ | ||
228 | query('id') | ||
229 | .optional() | ||
230 | .custom(isIdValid).withMessage('Should have a valid id'), | ||
231 | query('predefinedReason') | ||
232 | .optional() | ||
233 | .custom(isAbusePredefinedReasonValid) | ||
234 | .withMessage('Should have a valid predefinedReason'), | ||
235 | query('search') | ||
236 | .optional() | ||
237 | .custom(exists).withMessage('Should have a valid search'), | ||
238 | query('state') | ||
239 | .optional() | ||
240 | .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), | ||
241 | query('videoIs') | ||
242 | .optional() | ||
243 | .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'), | ||
244 | query('searchReporter') | ||
245 | .optional() | ||
246 | .custom(exists).withMessage('Should have a valid reporter search'), | ||
247 | query('searchReportee') | ||
248 | .optional() | ||
249 | .custom(exists).withMessage('Should have a valid reportee search'), | ||
250 | query('searchVideo') | ||
251 | .optional() | ||
252 | .custom(exists).withMessage('Should have a valid video search'), | ||
253 | query('searchVideoChannel') | ||
254 | .optional() | ||
255 | .custom(exists).withMessage('Should have a valid video channel search'), | ||
256 | |||
257 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
258 | logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body }) | ||
259 | |||
260 | if (areValidationErrors(req, res)) return | ||
261 | |||
262 | return next() | ||
263 | } | ||
264 | ] | ||
265 | |||
266 | // --------------------------------------------------------------------------- | ||
267 | |||
268 | export { | ||
269 | abuseListValidator, | ||
270 | abuseReportValidator, | ||
271 | abuseGetValidator, | ||
272 | abuseUpdateValidator, | ||
273 | videoAbuseReportValidator, | ||
274 | videoAbuseGetValidator, | ||
275 | videoAbuseUpdateValidator, | ||
276 | videoAbuseListValidator | ||
277 | } | ||
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 @@ | |||
1 | export * from './abuse' | ||
1 | export * from './account' | 2 | export * from './account' |
2 | export * from './blocklist' | 3 | export * from './blocklist' |
3 | export * from './oembed' | 4 | 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' | |||
5 | const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) | 5 | const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) |
6 | const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS) | 6 | const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS) |
7 | const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) | 7 | const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) |
8 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) | 8 | const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES) |
9 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) | 9 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) |
10 | const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) | 10 | const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) |
11 | const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) | 11 | const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) |
@@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM | |||
28 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 28 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
29 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 29 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
30 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) | 30 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) |
31 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) | 31 | const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) |
32 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | 32 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) |
33 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) | 33 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) |
34 | const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) | 34 | const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) |
@@ -52,7 +52,7 @@ const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COL | |||
52 | 52 | ||
53 | export { | 53 | export { |
54 | usersSortValidator, | 54 | usersSortValidator, |
55 | videoAbusesSortValidator, | 55 | abusesSortValidator, |
56 | videoChannelsSortValidator, | 56 | videoChannelsSortValidator, |
57 | videoImportsSortValidator, | 57 | videoImportsSortValidator, |
58 | videosSearchSortValidator, | 58 | videosSearchSortValidator, |
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts index fbfcb0a4c..21a7be08d 100644 --- a/server/middlewares/validators/user-notifications.ts +++ b/server/middlewares/validators/user-notifications.ts | |||
@@ -25,8 +25,8 @@ const updateNotificationSettingsValidator = [ | |||
25 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), | 25 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), |
26 | body('newCommentOnMyVideo') | 26 | body('newCommentOnMyVideo') |
27 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), | 27 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), |
28 | body('videoAbuseAsModerator') | 28 | body('abuseAsModerator') |
29 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'), | 29 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid abuse as moderator notification setting'), |
30 | body('videoAutoBlacklistAsModerator') | 30 | body('videoAutoBlacklistAsModerator') |
31 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'), | 31 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'), |
32 | body('blacklistOnMyVideo') | 32 | body('blacklistOnMyVideo') |
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 @@ | |||
1 | export * from './video-abuses' | ||
2 | export * from './video-blacklist' | 1 | export * from './video-blacklist' |
3 | export * from './video-captions' | 2 | export * from './video-captions' |
4 | export * from './video-channels' | 3 | 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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | ||
4 | import { | ||
5 | isAbuseVideoIsValid, | ||
6 | isVideoAbuseModerationCommentValid, | ||
7 | isVideoAbuseReasonValid, | ||
8 | isVideoAbuseStateValid, | ||
9 | isVideoAbusePredefinedReasonsValid, | ||
10 | isVideoAbusePredefinedReasonValid, | ||
11 | isVideoAbuseTimestampValid, | ||
12 | isVideoAbuseTimestampCoherent | ||
13 | } from '../../../helpers/custom-validators/video-abuses' | ||
14 | import { logger } from '../../../helpers/logger' | ||
15 | import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares' | ||
16 | import { areValidationErrors } from '../utils' | ||
17 | |||
18 | const videoAbuseReportValidator = [ | ||
19 | param('videoId') | ||
20 | .custom(isIdOrUUIDValid) | ||
21 | .not() | ||
22 | .isEmpty() | ||
23 | .withMessage('Should have a valid videoId'), | ||
24 | body('reason') | ||
25 | .custom(isVideoAbuseReasonValid) | ||
26 | .withMessage('Should have a valid reason'), | ||
27 | body('predefinedReasons') | ||
28 | .optional() | ||
29 | .custom(isVideoAbusePredefinedReasonsValid) | ||
30 | .withMessage('Should have a valid list of predefined reasons'), | ||
31 | body('startAt') | ||
32 | .optional() | ||
33 | .customSanitizer(toIntOrNull) | ||
34 | .custom(isVideoAbuseTimestampValid) | ||
35 | .withMessage('Should have valid starting time value'), | ||
36 | body('endAt') | ||
37 | .optional() | ||
38 | .customSanitizer(toIntOrNull) | ||
39 | .custom(isVideoAbuseTimestampValid) | ||
40 | .withMessage('Should have valid ending time value') | ||
41 | .bail() | ||
42 | .custom(isVideoAbuseTimestampCoherent) | ||
43 | .withMessage('Should have a startAt timestamp beginning before endAt'), | ||
44 | |||
45 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
46 | logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) | ||
47 | |||
48 | if (areValidationErrors(req, res)) return | ||
49 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
50 | |||
51 | return next() | ||
52 | } | ||
53 | ] | ||
54 | |||
55 | const videoAbuseGetValidator = [ | ||
56 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
57 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), | ||
58 | |||
59 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
60 | logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) | ||
61 | |||
62 | if (areValidationErrors(req, res)) return | ||
63 | if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return | ||
64 | |||
65 | return next() | ||
66 | } | ||
67 | ] | ||
68 | |||
69 | const videoAbuseUpdateValidator = [ | ||
70 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
71 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), | ||
72 | body('state') | ||
73 | .optional() | ||
74 | .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'), | ||
75 | body('moderationComment') | ||
76 | .optional() | ||
77 | .custom(isVideoAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), | ||
78 | |||
79 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
80 | logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) | ||
81 | |||
82 | if (areValidationErrors(req, res)) return | ||
83 | if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return | ||
84 | |||
85 | return next() | ||
86 | } | ||
87 | ] | ||
88 | |||
89 | const videoAbuseListValidator = [ | ||
90 | query('id') | ||
91 | .optional() | ||
92 | .custom(isIdValid).withMessage('Should have a valid id'), | ||
93 | query('predefinedReason') | ||
94 | .optional() | ||
95 | .custom(isVideoAbusePredefinedReasonValid) | ||
96 | .withMessage('Should have a valid predefinedReason'), | ||
97 | query('search') | ||
98 | .optional() | ||
99 | .custom(exists).withMessage('Should have a valid search'), | ||
100 | query('state') | ||
101 | .optional() | ||
102 | .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'), | ||
103 | query('videoIs') | ||
104 | .optional() | ||
105 | .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'), | ||
106 | query('searchReporter') | ||
107 | .optional() | ||
108 | .custom(exists).withMessage('Should have a valid reporter search'), | ||
109 | query('searchReportee') | ||
110 | .optional() | ||
111 | .custom(exists).withMessage('Should have a valid reportee search'), | ||
112 | query('searchVideo') | ||
113 | .optional() | ||
114 | .custom(exists).withMessage('Should have a valid video search'), | ||
115 | query('searchVideoChannel') | ||
116 | .optional() | ||
117 | .custom(exists).withMessage('Should have a valid video channel search'), | ||
118 | |||
119 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
120 | logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body }) | ||
121 | |||
122 | if (areValidationErrors(req, res)) return | ||
123 | |||
124 | return next() | ||
125 | } | ||
126 | ] | ||
127 | |||
128 | // --------------------------------------------------------------------------- | ||
129 | |||
130 | export { | ||
131 | videoAbuseListValidator, | ||
132 | videoAbuseReportValidator, | ||
133 | videoAbuseGetValidator, | ||
134 | videoAbuseUpdateValidator | ||
135 | } | ||
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index ef019fcf9..77f5c6ff3 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -3,13 +3,16 @@ import { body, param } from 'express-validator' | |||
3 | import { MUserAccountUrl } from '@server/types/models' | 3 | import { MUserAccountUrl } from '@server/types/models' |
4 | import { UserRight } from '../../../../shared' | 4 | import { UserRight } from '../../../../shared' |
5 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | 5 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' |
6 | import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' | 6 | import { |
7 | doesVideoCommentExist, | ||
8 | doesVideoCommentThreadExist, | ||
9 | isValidVideoCommentText | ||
10 | } from '../../../helpers/custom-validators/video-comments' | ||
7 | import { logger } from '../../../helpers/logger' | 11 | import { logger } from '../../../helpers/logger' |
8 | import { doesVideoExist } from '../../../helpers/middlewares' | 12 | import { doesVideoExist } from '../../../helpers/middlewares' |
9 | import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' | 13 | import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' |
10 | import { Hooks } from '../../../lib/plugins/hooks' | 14 | import { Hooks } from '../../../lib/plugins/hooks' |
11 | import { VideoCommentModel } from '../../../models/video/video-comment' | 15 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' |
12 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight, MVideoId } from '../../../types/models/video' | ||
13 | import { areValidationErrors } from '../utils' | 16 | import { areValidationErrors } from '../utils' |
14 | 17 | ||
15 | const listVideoCommentThreadsValidator = [ | 18 | const listVideoCommentThreadsValidator = [ |
@@ -120,67 +123,10 @@ export { | |||
120 | 123 | ||
121 | // --------------------------------------------------------------------------- | 124 | // --------------------------------------------------------------------------- |
122 | 125 | ||
123 | async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
124 | const id = parseInt(idArg + '', 10) | ||
125 | const videoComment = await VideoCommentModel.loadById(id) | ||
126 | |||
127 | if (!videoComment) { | ||
128 | res.status(404) | ||
129 | .json({ error: 'Video comment thread not found' }) | ||
130 | .end() | ||
131 | |||
132 | return false | ||
133 | } | ||
134 | |||
135 | if (videoComment.videoId !== video.id) { | ||
136 | res.status(400) | ||
137 | .json({ error: 'Video comment is not associated to this video.' }) | ||
138 | .end() | ||
139 | |||
140 | return false | ||
141 | } | ||
142 | |||
143 | if (videoComment.inReplyToCommentId !== null) { | ||
144 | res.status(400) | ||
145 | .json({ error: 'Video comment is not a thread.' }) | ||
146 | .end() | ||
147 | |||
148 | return false | ||
149 | } | ||
150 | |||
151 | res.locals.videoCommentThread = videoComment | ||
152 | return true | ||
153 | } | ||
154 | |||
155 | async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
156 | const id = parseInt(idArg + '', 10) | ||
157 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
158 | |||
159 | if (!videoComment) { | ||
160 | res.status(404) | ||
161 | .json({ error: 'Video comment thread not found' }) | ||
162 | .end() | ||
163 | |||
164 | return false | ||
165 | } | ||
166 | |||
167 | if (videoComment.videoId !== video.id) { | ||
168 | res.status(400) | ||
169 | .json({ error: 'Video comment is not associated to this video.' }) | ||
170 | .end() | ||
171 | |||
172 | return false | ||
173 | } | ||
174 | |||
175 | res.locals.videoCommentFull = videoComment | ||
176 | return true | ||
177 | } | ||
178 | |||
179 | function isVideoCommentsEnabled (video: MVideo, res: express.Response) { | 126 | function isVideoCommentsEnabled (video: MVideo, res: express.Response) { |
180 | if (video.commentsEnabled !== true) { | 127 | if (video.commentsEnabled !== true) { |
181 | res.status(409) | 128 | res.status(409) |
182 | .json({ error: 'Video comments are disabled for this video.' }) | 129 | .json({ error: 'Video comments are disabled for this video.' }) |
183 | .end() | ||
184 | 130 | ||
185 | return false | 131 | return false |
186 | } | 132 | } |
@@ -192,7 +138,7 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC | |||
192 | if (videoComment.isDeleted()) { | 138 | if (videoComment.isDeleted()) { |
193 | res.status(409) | 139 | res.status(409) |
194 | .json({ error: 'This comment is already deleted' }) | 140 | .json({ error: 'This comment is already deleted' }) |
195 | .end() | 141 | |
196 | return false | 142 | return false |
197 | } | 143 | } |
198 | 144 | ||
@@ -240,7 +186,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon | |||
240 | if (!acceptedResult || acceptedResult.accepted !== true) { | 186 | if (!acceptedResult || acceptedResult.accepted !== true) { |
241 | logger.info('Refused local comment.', { acceptedResult, acceptParameters }) | 187 | logger.info('Refused local comment.', { acceptedResult, acceptParameters }) |
242 | res.status(403) | 188 | res.status(403) |
243 | .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) | 189 | .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) |
244 | 190 | ||
245 | return false | 191 | return false |
246 | } | 192 | } |
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts new file mode 100644 index 000000000..5fddcf3c4 --- /dev/null +++ b/server/models/abuse/abuse-query-builder.ts | |||
@@ -0,0 +1,154 @@ | |||
1 | |||
2 | import { exists } from '@server/helpers/custom-validators/misc' | ||
3 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' | ||
4 | import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils' | ||
5 | |||
6 | export type BuildAbusesQueryOptions = { | ||
7 | start: number | ||
8 | count: number | ||
9 | sort: string | ||
10 | |||
11 | // search | ||
12 | search?: string | ||
13 | searchReporter?: string | ||
14 | searchReportee?: string | ||
15 | |||
16 | // video releated | ||
17 | searchVideo?: string | ||
18 | searchVideoChannel?: string | ||
19 | videoIs?: AbuseVideoIs | ||
20 | |||
21 | // filters | ||
22 | id?: number | ||
23 | predefinedReasonId?: number | ||
24 | filter?: AbuseFilter | ||
25 | |||
26 | state?: AbuseState | ||
27 | |||
28 | // accountIds | ||
29 | serverAccountId: number | ||
30 | userAccountId: number | ||
31 | } | ||
32 | |||
33 | function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | 'id') { | ||
34 | const whereAnd: string[] = [] | ||
35 | const replacements: any = {} | ||
36 | |||
37 | const joins = [ | ||
38 | 'LEFT JOIN "videoAbuse" ON "videoAbuse"."abuseId" = "abuse"."id"', | ||
39 | 'LEFT JOIN "video" ON "videoAbuse"."videoId" = "video"."id"', | ||
40 | 'LEFT JOIN "videoBlacklist" ON "videoBlacklist"."videoId" = "video"."id"', | ||
41 | 'LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"', | ||
42 | 'LEFT JOIN "account" "reporterAccount" ON "reporterAccount"."id" = "abuse"."reporterAccountId"', | ||
43 | 'LEFT JOIN "account" "flaggedAccount" ON "flaggedAccount"."id" = "abuse"."reporterAccountId"', | ||
44 | 'LEFT JOIN "commentAbuse" ON "commentAbuse"."abuseId" = "abuse"."id"', | ||
45 | 'LEFT JOIN "videoComment" ON "commentAbuse"."videoCommentId" = "videoComment"."id"' | ||
46 | ] | ||
47 | |||
48 | whereAnd.push('"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') | ||
49 | |||
50 | if (options.search) { | ||
51 | const searchWhereOr = [ | ||
52 | '"video"."name" ILIKE :search', | ||
53 | '"videoChannel"."name" ILIKE :search', | ||
54 | `"videoAbuse"."deletedVideo"->>'name' ILIKE :search`, | ||
55 | `"videoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE :search`, | ||
56 | '"reporterAccount"."name" ILIKE :search', | ||
57 | '"flaggedAccount"."name" ILIKE :search' | ||
58 | ] | ||
59 | |||
60 | replacements.search = `%${options.search}%` | ||
61 | whereAnd.push('(' + searchWhereOr.join(' OR ') + ')') | ||
62 | } | ||
63 | |||
64 | if (options.searchVideo) { | ||
65 | whereAnd.push('"video"."name" ILIKE :searchVideo') | ||
66 | replacements.searchVideo = `%${options.searchVideo}%` | ||
67 | } | ||
68 | |||
69 | if (options.searchVideoChannel) { | ||
70 | whereAnd.push('"videoChannel"."name" ILIKE :searchVideoChannel') | ||
71 | replacements.searchVideoChannel = `%${options.searchVideoChannel}%` | ||
72 | } | ||
73 | |||
74 | if (options.id) { | ||
75 | whereAnd.push('"abuse"."id" = :id') | ||
76 | replacements.id = options.id | ||
77 | } | ||
78 | |||
79 | if (options.state) { | ||
80 | whereAnd.push('"abuse"."state" = :state') | ||
81 | replacements.state = options.state | ||
82 | } | ||
83 | |||
84 | if (options.videoIs === 'deleted') { | ||
85 | whereAnd.push('"videoAbuse"."deletedVideo" IS NOT NULL') | ||
86 | } else if (options.videoIs === 'blacklisted') { | ||
87 | whereAnd.push('"videoBlacklist"."id" IS NOT NULL') | ||
88 | } | ||
89 | |||
90 | if (options.predefinedReasonId) { | ||
91 | whereAnd.push(':predefinedReasonId = ANY("abuse"."predefinedReasons")') | ||
92 | replacements.predefinedReasonId = options.predefinedReasonId | ||
93 | } | ||
94 | |||
95 | if (options.filter === 'video') { | ||
96 | whereAnd.push('"videoAbuse"."id" IS NOT NULL') | ||
97 | } else if (options.filter === 'comment') { | ||
98 | whereAnd.push('"commentAbuse"."id" IS NOT NULL') | ||
99 | } else if (options.filter === 'account') { | ||
100 | whereAnd.push('"videoAbuse"."id" IS NULL AND "commentAbuse"."id" IS NULL') | ||
101 | } | ||
102 | |||
103 | if (options.searchReporter) { | ||
104 | whereAnd.push('"reporterAccount"."name" ILIKE :searchReporter') | ||
105 | replacements.searchReporter = `%${options.searchReporter}%` | ||
106 | } | ||
107 | |||
108 | if (options.searchReportee) { | ||
109 | whereAnd.push('"flaggedAccount"."name" ILIKE :searchReportee') | ||
110 | replacements.searchReportee = `%${options.searchReportee}%` | ||
111 | } | ||
112 | |||
113 | const prefix = type === 'count' | ||
114 | ? 'SELECT COUNT("abuse"."id") AS "total"' | ||
115 | : 'SELECT "abuse"."id" ' | ||
116 | |||
117 | let suffix = '' | ||
118 | if (type !== 'count') { | ||
119 | |||
120 | if (options.sort) { | ||
121 | const order = buildAbuseOrder(options.sort) | ||
122 | suffix += `${order} ` | ||
123 | } | ||
124 | |||
125 | if (exists(options.count)) { | ||
126 | const count = parseInt(options.count + '', 10) | ||
127 | suffix += `LIMIT ${count} ` | ||
128 | } | ||
129 | |||
130 | if (exists(options.start)) { | ||
131 | const start = parseInt(options.start + '', 10) | ||
132 | suffix += `OFFSET ${start} ` | ||
133 | } | ||
134 | } | ||
135 | |||
136 | const where = whereAnd.length !== 0 | ||
137 | ? `WHERE ${whereAnd.join(' AND ')}` | ||
138 | : '' | ||
139 | |||
140 | return { | ||
141 | query: `${prefix} FROM "abuse" ${joins.join(' ')} ${where} ${suffix}`, | ||
142 | replacements | ||
143 | } | ||
144 | } | ||
145 | |||
146 | function buildAbuseOrder (value: string) { | ||
147 | const { direction, field } = buildDirectionAndField(value) | ||
148 | |||
149 | return `ORDER BY "abuse"."${field}" ${direction}` | ||
150 | } | ||
151 | |||
152 | export { | ||
153 | buildAbuseListQuery | ||
154 | } | ||
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts new file mode 100644 index 000000000..bd96cf79c --- /dev/null +++ b/server/models/abuse/abuse.ts | |||
@@ -0,0 +1,515 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { invert } from 'lodash' | ||
3 | import { literal, Op, QueryTypes, WhereOptions } from 'sequelize' | ||
4 | import { | ||
5 | AllowNull, | ||
6 | BelongsTo, | ||
7 | Column, | ||
8 | CreatedAt, | ||
9 | DataType, | ||
10 | Default, | ||
11 | ForeignKey, | ||
12 | HasOne, | ||
13 | Is, | ||
14 | Model, | ||
15 | Scopes, | ||
16 | Table, | ||
17 | UpdatedAt | ||
18 | } from 'sequelize-typescript' | ||
19 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' | ||
20 | import { | ||
21 | Abuse, | ||
22 | AbuseFilter, | ||
23 | AbuseObject, | ||
24 | AbusePredefinedReasons, | ||
25 | abusePredefinedReasonsMap, | ||
26 | AbusePredefinedReasonsString, | ||
27 | AbuseState, | ||
28 | AbuseVideoIs, | ||
29 | VideoAbuse, | ||
30 | VideoCommentAbuse | ||
31 | } from '@shared/models' | ||
32 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
33 | import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' | ||
34 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | ||
35 | import { getSort, throwIfNotValid } from '../utils' | ||
36 | import { ThumbnailModel } from '../video/thumbnail' | ||
37 | import { VideoModel } from '../video/video' | ||
38 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' | ||
40 | import { VideoCommentModel } from '../video/video-comment' | ||
41 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' | ||
42 | import { VideoAbuseModel } from './video-abuse' | ||
43 | import { VideoCommentAbuseModel } from './video-comment-abuse' | ||
44 | |||
45 | export enum ScopeNames { | ||
46 | FOR_API = 'FOR_API' | ||
47 | } | ||
48 | |||
49 | @Scopes(() => ({ | ||
50 | [ScopeNames.FOR_API]: () => { | ||
51 | return { | ||
52 | attributes: { | ||
53 | include: [ | ||
54 | [ | ||
55 | // we don't care about this count for deleted videos, so there are not included | ||
56 | literal( | ||
57 | '(' + | ||
58 | 'SELECT count(*) ' + | ||
59 | 'FROM "videoAbuse" ' + | ||
60 | 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' + | ||
61 | ')' | ||
62 | ), | ||
63 | 'countReportsForVideo' | ||
64 | ], | ||
65 | [ | ||
66 | // we don't care about this count for deleted videos, so there are not included | ||
67 | literal( | ||
68 | '(' + | ||
69 | 'SELECT t.nth ' + | ||
70 | 'FROM ( ' + | ||
71 | 'SELECT id, ' + | ||
72 | 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + | ||
73 | 'FROM "videoAbuse" ' + | ||
74 | ') t ' + | ||
75 | 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' + | ||
76 | ')' | ||
77 | ), | ||
78 | 'nthReportForVideo' | ||
79 | ], | ||
80 | [ | ||
81 | literal( | ||
82 | '(' + | ||
83 | 'SELECT count("abuse"."id") ' + | ||
84 | 'FROM "abuse" ' + | ||
85 | 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' + | ||
86 | ')' | ||
87 | ), | ||
88 | 'countReportsForReporter' | ||
89 | ], | ||
90 | [ | ||
91 | literal( | ||
92 | '(' + | ||
93 | 'SELECT count("abuse"."id") ' + | ||
94 | 'FROM "abuse" ' + | ||
95 | 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' + | ||
96 | ')' | ||
97 | ), | ||
98 | 'countReportsForReportee' | ||
99 | ] | ||
100 | ] | ||
101 | }, | ||
102 | include: [ | ||
103 | { | ||
104 | model: AccountModel.scope({ | ||
105 | method: [ | ||
106 | AccountScopeNames.SUMMARY, | ||
107 | { actorRequired: false } as AccountSummaryOptions | ||
108 | ] | ||
109 | }), | ||
110 | as: 'ReporterAccount' | ||
111 | }, | ||
112 | { | ||
113 | model: AccountModel.scope({ | ||
114 | method: [ | ||
115 | AccountScopeNames.SUMMARY, | ||
116 | { actorRequired: false } as AccountSummaryOptions | ||
117 | ] | ||
118 | }), | ||
119 | as: 'FlaggedAccount' | ||
120 | }, | ||
121 | { | ||
122 | model: VideoCommentAbuseModel.unscoped(), | ||
123 | include: [ | ||
124 | { | ||
125 | model: VideoCommentModel.unscoped(), | ||
126 | include: [ | ||
127 | { | ||
128 | model: VideoModel.unscoped(), | ||
129 | attributes: [ 'name', 'id', 'uuid' ] | ||
130 | } | ||
131 | ] | ||
132 | } | ||
133 | ] | ||
134 | }, | ||
135 | { | ||
136 | model: VideoAbuseModel.unscoped(), | ||
137 | include: [ | ||
138 | { | ||
139 | attributes: [ 'id', 'uuid', 'name', 'nsfw' ], | ||
140 | model: VideoModel.unscoped(), | ||
141 | include: [ | ||
142 | { | ||
143 | attributes: [ 'filename', 'fileUrl', 'type' ], | ||
144 | model: ThumbnailModel | ||
145 | }, | ||
146 | { | ||
147 | model: VideoChannelModel.scope({ | ||
148 | method: [ | ||
149 | VideoChannelScopeNames.SUMMARY, | ||
150 | { withAccount: false, actorRequired: false } as ChannelSummaryOptions | ||
151 | ] | ||
152 | }), | ||
153 | required: false | ||
154 | }, | ||
155 | { | ||
156 | attributes: [ 'id', 'reason', 'unfederated' ], | ||
157 | required: false, | ||
158 | model: VideoBlacklistModel | ||
159 | } | ||
160 | ] | ||
161 | } | ||
162 | ] | ||
163 | } | ||
164 | ] | ||
165 | } | ||
166 | } | ||
167 | })) | ||
168 | @Table({ | ||
169 | tableName: 'abuse', | ||
170 | indexes: [ | ||
171 | { | ||
172 | fields: [ 'reporterAccountId' ] | ||
173 | }, | ||
174 | { | ||
175 | fields: [ 'flaggedAccountId' ] | ||
176 | } | ||
177 | ] | ||
178 | }) | ||
179 | export class AbuseModel extends Model<AbuseModel> { | ||
180 | |||
181 | @AllowNull(false) | ||
182 | @Default(null) | ||
183 | @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) | ||
184 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max)) | ||
185 | reason: string | ||
186 | |||
187 | @AllowNull(false) | ||
188 | @Default(null) | ||
189 | @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) | ||
190 | @Column | ||
191 | state: AbuseState | ||
192 | |||
193 | @AllowNull(true) | ||
194 | @Default(null) | ||
195 | @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true)) | ||
196 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max)) | ||
197 | moderationComment: string | ||
198 | |||
199 | @AllowNull(true) | ||
200 | @Default(null) | ||
201 | @Column(DataType.ARRAY(DataType.INTEGER)) | ||
202 | predefinedReasons: AbusePredefinedReasons[] | ||
203 | |||
204 | @CreatedAt | ||
205 | createdAt: Date | ||
206 | |||
207 | @UpdatedAt | ||
208 | updatedAt: Date | ||
209 | |||
210 | @ForeignKey(() => AccountModel) | ||
211 | @Column | ||
212 | reporterAccountId: number | ||
213 | |||
214 | @BelongsTo(() => AccountModel, { | ||
215 | foreignKey: { | ||
216 | name: 'reporterAccountId', | ||
217 | allowNull: true | ||
218 | }, | ||
219 | as: 'ReporterAccount', | ||
220 | onDelete: 'set null' | ||
221 | }) | ||
222 | ReporterAccount: AccountModel | ||
223 | |||
224 | @ForeignKey(() => AccountModel) | ||
225 | @Column | ||
226 | flaggedAccountId: number | ||
227 | |||
228 | @BelongsTo(() => AccountModel, { | ||
229 | foreignKey: { | ||
230 | name: 'flaggedAccountId', | ||
231 | allowNull: true | ||
232 | }, | ||
233 | as: 'FlaggedAccount', | ||
234 | onDelete: 'set null' | ||
235 | }) | ||
236 | FlaggedAccount: AccountModel | ||
237 | |||
238 | @HasOne(() => VideoCommentAbuseModel, { | ||
239 | foreignKey: { | ||
240 | name: 'abuseId', | ||
241 | allowNull: false | ||
242 | }, | ||
243 | onDelete: 'cascade' | ||
244 | }) | ||
245 | VideoCommentAbuse: VideoCommentAbuseModel | ||
246 | |||
247 | @HasOne(() => VideoAbuseModel, { | ||
248 | foreignKey: { | ||
249 | name: 'abuseId', | ||
250 | allowNull: false | ||
251 | }, | ||
252 | onDelete: 'cascade' | ||
253 | }) | ||
254 | VideoAbuse: VideoAbuseModel | ||
255 | |||
256 | // FIXME: deprecated in 2.3. Remove these validators | ||
257 | static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> { | ||
258 | const videoWhere: WhereOptions = {} | ||
259 | |||
260 | if (videoId) videoWhere.videoId = videoId | ||
261 | if (uuid) videoWhere.deletedVideo = { uuid } | ||
262 | |||
263 | const query = { | ||
264 | include: [ | ||
265 | { | ||
266 | model: VideoAbuseModel, | ||
267 | required: true, | ||
268 | where: videoWhere | ||
269 | } | ||
270 | ], | ||
271 | where: { | ||
272 | id | ||
273 | } | ||
274 | } | ||
275 | return AbuseModel.findOne(query) | ||
276 | } | ||
277 | |||
278 | static loadById (id: number): Bluebird<MAbuse> { | ||
279 | const query = { | ||
280 | where: { | ||
281 | id | ||
282 | } | ||
283 | } | ||
284 | |||
285 | return AbuseModel.findOne(query) | ||
286 | } | ||
287 | |||
288 | static async listForApi (parameters: { | ||
289 | start: number | ||
290 | count: number | ||
291 | sort: string | ||
292 | |||
293 | filter?: AbuseFilter | ||
294 | |||
295 | serverAccountId: number | ||
296 | user?: MUserAccountId | ||
297 | |||
298 | id?: number | ||
299 | predefinedReason?: AbusePredefinedReasonsString | ||
300 | state?: AbuseState | ||
301 | videoIs?: AbuseVideoIs | ||
302 | |||
303 | search?: string | ||
304 | searchReporter?: string | ||
305 | searchReportee?: string | ||
306 | searchVideo?: string | ||
307 | searchVideoChannel?: string | ||
308 | }) { | ||
309 | const { | ||
310 | start, | ||
311 | count, | ||
312 | sort, | ||
313 | search, | ||
314 | user, | ||
315 | serverAccountId, | ||
316 | state, | ||
317 | videoIs, | ||
318 | predefinedReason, | ||
319 | searchReportee, | ||
320 | searchVideo, | ||
321 | filter, | ||
322 | searchVideoChannel, | ||
323 | searchReporter, | ||
324 | id | ||
325 | } = parameters | ||
326 | |||
327 | const userAccountId = user ? user.Account.id : undefined | ||
328 | const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined | ||
329 | |||
330 | const queryOptions: BuildAbusesQueryOptions = { | ||
331 | start, | ||
332 | count, | ||
333 | sort, | ||
334 | id, | ||
335 | filter, | ||
336 | predefinedReasonId, | ||
337 | search, | ||
338 | state, | ||
339 | videoIs, | ||
340 | searchReportee, | ||
341 | searchVideo, | ||
342 | searchVideoChannel, | ||
343 | searchReporter, | ||
344 | serverAccountId, | ||
345 | userAccountId | ||
346 | } | ||
347 | |||
348 | const [ total, data ] = await Promise.all([ | ||
349 | AbuseModel.internalCountForApi(queryOptions), | ||
350 | AbuseModel.internalListForApi(queryOptions) | ||
351 | ]) | ||
352 | |||
353 | return { total, data } | ||
354 | } | ||
355 | |||
356 | toFormattedJSON (this: MAbuseFormattable): Abuse { | ||
357 | const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
358 | |||
359 | const countReportsForVideo = this.get('countReportsForVideo') as number | ||
360 | const nthReportForVideo = this.get('nthReportForVideo') as number | ||
361 | |||
362 | const countReportsForReporter = this.get('countReportsForReporter') as number | ||
363 | const countReportsForReportee = this.get('countReportsForReportee') as number | ||
364 | |||
365 | let video: VideoAbuse = null | ||
366 | let comment: VideoCommentAbuse = null | ||
367 | |||
368 | if (this.VideoAbuse) { | ||
369 | const abuseModel = this.VideoAbuse | ||
370 | const entity = abuseModel.Video || abuseModel.deletedVideo | ||
371 | |||
372 | video = { | ||
373 | id: entity.id, | ||
374 | uuid: entity.uuid, | ||
375 | name: entity.name, | ||
376 | nsfw: entity.nsfw, | ||
377 | |||
378 | startAt: abuseModel.startAt, | ||
379 | endAt: abuseModel.endAt, | ||
380 | |||
381 | deleted: !abuseModel.Video, | ||
382 | blacklisted: abuseModel.Video?.isBlacklisted() || false, | ||
383 | thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), | ||
384 | |||
385 | channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel, | ||
386 | |||
387 | countReports: countReportsForVideo, | ||
388 | nthReport: nthReportForVideo | ||
389 | } | ||
390 | } | ||
391 | |||
392 | if (this.VideoCommentAbuse) { | ||
393 | const abuseModel = this.VideoCommentAbuse | ||
394 | const entity = abuseModel.VideoComment | ||
395 | |||
396 | comment = { | ||
397 | id: entity.id, | ||
398 | threadId: entity.getThreadId(), | ||
399 | |||
400 | text: entity.text ?? '', | ||
401 | |||
402 | deleted: entity.isDeleted(), | ||
403 | |||
404 | video: { | ||
405 | id: entity.Video.id, | ||
406 | name: entity.Video.name, | ||
407 | uuid: entity.Video.uuid | ||
408 | } | ||
409 | } | ||
410 | } | ||
411 | |||
412 | return { | ||
413 | id: this.id, | ||
414 | reason: this.reason, | ||
415 | predefinedReasons, | ||
416 | |||
417 | reporterAccount: this.ReporterAccount | ||
418 | ? this.ReporterAccount.toFormattedJSON() | ||
419 | : null, | ||
420 | |||
421 | flaggedAccount: this.FlaggedAccount | ||
422 | ? this.FlaggedAccount.toFormattedJSON() | ||
423 | : null, | ||
424 | |||
425 | state: { | ||
426 | id: this.state, | ||
427 | label: AbuseModel.getStateLabel(this.state) | ||
428 | }, | ||
429 | |||
430 | moderationComment: this.moderationComment, | ||
431 | |||
432 | video, | ||
433 | comment, | ||
434 | |||
435 | createdAt: this.createdAt, | ||
436 | updatedAt: this.updatedAt, | ||
437 | |||
438 | countReportsForReporter: (countReportsForReporter || 0), | ||
439 | countReportsForReportee: (countReportsForReportee || 0), | ||
440 | |||
441 | // FIXME: deprecated in 2.3, remove this | ||
442 | startAt: null, | ||
443 | endAt: null, | ||
444 | count: countReportsForVideo || 0, | ||
445 | nth: nthReportForVideo || 0 | ||
446 | } | ||
447 | } | ||
448 | |||
449 | toActivityPubObject (this: MAbuseAP): AbuseObject { | ||
450 | const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
451 | |||
452 | const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url | ||
453 | |||
454 | const startAt = this.VideoAbuse?.startAt | ||
455 | const endAt = this.VideoAbuse?.endAt | ||
456 | |||
457 | return { | ||
458 | type: 'Flag' as 'Flag', | ||
459 | content: this.reason, | ||
460 | object, | ||
461 | tag: predefinedReasons.map(r => ({ | ||
462 | type: 'Hashtag' as 'Hashtag', | ||
463 | name: r | ||
464 | })), | ||
465 | startAt, | ||
466 | endAt | ||
467 | } | ||
468 | } | ||
469 | |||
470 | private static async internalCountForApi (parameters: BuildAbusesQueryOptions) { | ||
471 | const { query, replacements } = buildAbuseListQuery(parameters, 'count') | ||
472 | const options = { | ||
473 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
474 | replacements | ||
475 | } | ||
476 | |||
477 | const [ { total } ] = await AbuseModel.sequelize.query<{ total: string }>(query, options) | ||
478 | if (total === null) return 0 | ||
479 | |||
480 | return parseInt(total, 10) | ||
481 | } | ||
482 | |||
483 | private static async internalListForApi (parameters: BuildAbusesQueryOptions) { | ||
484 | const { query, replacements } = buildAbuseListQuery(parameters, 'id') | ||
485 | const options = { | ||
486 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
487 | replacements | ||
488 | } | ||
489 | |||
490 | const rows = await AbuseModel.sequelize.query<{ id: string }>(query, options) | ||
491 | const ids = rows.map(r => r.id) | ||
492 | |||
493 | if (ids.length === 0) return [] | ||
494 | |||
495 | return AbuseModel.scope(ScopeNames.FOR_API) | ||
496 | .findAll({ | ||
497 | order: getSort(parameters.sort), | ||
498 | where: { | ||
499 | id: { | ||
500 | [Op.in]: ids | ||
501 | } | ||
502 | } | ||
503 | }) | ||
504 | } | ||
505 | |||
506 | private static getStateLabel (id: number) { | ||
507 | return ABUSE_STATES[id] || 'Unknown' | ||
508 | } | ||
509 | |||
510 | private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] { | ||
511 | return (predefinedReasons || []) | ||
512 | .filter(r => r in AbusePredefinedReasons) | ||
513 | .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString) | ||
514 | } | ||
515 | } | ||
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 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoDetails } from '@shared/models' | ||
3 | import { VideoModel } from '../video/video' | ||
4 | import { AbuseModel } from './abuse' | ||
5 | |||
6 | @Table({ | ||
7 | tableName: 'videoAbuse', | ||
8 | indexes: [ | ||
9 | { | ||
10 | fields: [ 'abuseId' ] | ||
11 | }, | ||
12 | { | ||
13 | fields: [ 'videoId' ] | ||
14 | } | ||
15 | ] | ||
16 | }) | ||
17 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | ||
18 | |||
19 | @CreatedAt | ||
20 | createdAt: Date | ||
21 | |||
22 | @UpdatedAt | ||
23 | updatedAt: Date | ||
24 | |||
25 | @AllowNull(true) | ||
26 | @Default(null) | ||
27 | @Column | ||
28 | startAt: number | ||
29 | |||
30 | @AllowNull(true) | ||
31 | @Default(null) | ||
32 | @Column | ||
33 | endAt: number | ||
34 | |||
35 | @AllowNull(true) | ||
36 | @Default(null) | ||
37 | @Column(DataType.JSONB) | ||
38 | deletedVideo: VideoDetails | ||
39 | |||
40 | @ForeignKey(() => AbuseModel) | ||
41 | @Column | ||
42 | abuseId: number | ||
43 | |||
44 | @BelongsTo(() => AbuseModel, { | ||
45 | foreignKey: { | ||
46 | allowNull: false | ||
47 | }, | ||
48 | onDelete: 'cascade' | ||
49 | }) | ||
50 | Abuse: AbuseModel | ||
51 | |||
52 | @ForeignKey(() => VideoModel) | ||
53 | @Column | ||
54 | videoId: number | ||
55 | |||
56 | @BelongsTo(() => VideoModel, { | ||
57 | foreignKey: { | ||
58 | allowNull: true | ||
59 | }, | ||
60 | onDelete: 'set null' | ||
61 | }) | ||
62 | Video: VideoModel | ||
63 | } | ||
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts new file mode 100644 index 000000000..8b34009b4 --- /dev/null +++ b/server/models/abuse/video-comment-abuse.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoCommentModel } from '../video/video-comment' | ||
3 | import { AbuseModel } from './abuse' | ||
4 | |||
5 | @Table({ | ||
6 | tableName: 'commentAbuse', | ||
7 | indexes: [ | ||
8 | { | ||
9 | fields: [ 'abuseId' ] | ||
10 | }, | ||
11 | { | ||
12 | fields: [ 'videoCommentId' ] | ||
13 | } | ||
14 | ] | ||
15 | }) | ||
16 | export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> { | ||
17 | |||
18 | @CreatedAt | ||
19 | createdAt: Date | ||
20 | |||
21 | @UpdatedAt | ||
22 | updatedAt: Date | ||
23 | |||
24 | @ForeignKey(() => AbuseModel) | ||
25 | @Column | ||
26 | abuseId: number | ||
27 | |||
28 | @BelongsTo(() => AbuseModel, { | ||
29 | foreignKey: { | ||
30 | allowNull: false | ||
31 | }, | ||
32 | onDelete: 'cascade' | ||
33 | }) | ||
34 | Abuse: AbuseModel | ||
35 | |||
36 | @ForeignKey(() => VideoCommentModel) | ||
37 | @Column | ||
38 | videoCommentId: number | ||
39 | |||
40 | @BelongsTo(() => VideoCommentModel, { | ||
41 | foreignKey: { | ||
42 | allowNull: true | ||
43 | }, | ||
44 | onDelete: 'set null' | ||
45 | }) | ||
46 | VideoComment: VideoCommentModel | ||
47 | } | ||
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 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AccountModel } from './account' | ||
3 | import { getSort, searchAttribute } from '../utils' | ||
4 | import { AccountBlock } from '../../../shared/models/blocklist' | ||
5 | import { Op } from 'sequelize' | ||
6 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { Op } from 'sequelize' | ||
3 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
7 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' | 4 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' |
5 | import { AccountBlock } from '../../../shared/models' | ||
8 | import { ActorModel } from '../activitypub/actor' | 6 | import { ActorModel } from '../activitypub/actor' |
9 | import { ServerModel } from '../server/server' | 7 | import { ServerModel } from '../server/server' |
8 | import { getSort, searchAttribute } from '../utils' | ||
9 | import { AccountModel } from './account' | ||
10 | 10 | ||
11 | enum ScopeNames { | 11 | enum ScopeNames { |
12 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | 12 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 4395d179a..f97519b14 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -42,6 +42,7 @@ export enum ScopeNames { | |||
42 | } | 42 | } |
43 | 43 | ||
44 | export type SummaryOptions = { | 44 | export type SummaryOptions = { |
45 | actorRequired?: boolean // Default: true | ||
45 | whereActor?: WhereOptions | 46 | whereActor?: WhereOptions |
46 | withAccountBlockerIds?: number[] | 47 | withAccountBlockerIds?: number[] |
47 | } | 48 | } |
@@ -65,12 +66,12 @@ export type SummaryOptions = { | |||
65 | } | 66 | } |
66 | 67 | ||
67 | const query: FindOptions = { | 68 | const query: FindOptions = { |
68 | attributes: [ 'id', 'name' ], | 69 | attributes: [ 'id', 'name', 'actorId' ], |
69 | include: [ | 70 | include: [ |
70 | { | 71 | { |
71 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 72 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], |
72 | model: ActorModel.unscoped(), | 73 | model: ActorModel.unscoped(), |
73 | required: true, | 74 | required: options.actorRequired ?? true, |
74 | where: whereActor, | 75 | where: whereActor, |
75 | include: [ | 76 | include: [ |
76 | serverInclude, | 77 | serverInclude, |
@@ -388,6 +389,10 @@ export class AccountModel extends Model<AccountModel> { | |||
388 | .findAll(query) | 389 | .findAll(query) |
389 | } | 390 | } |
390 | 391 | ||
392 | getClientUrl () { | ||
393 | return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier() | ||
394 | } | ||
395 | |||
391 | toFormattedJSON (this: MAccountFormattable): Account { | 396 | toFormattedJSON (this: MAccountFormattable): Account { |
392 | const actor = this.Actor.toFormattedJSON() | 397 | const actor = this.Actor.toFormattedJSON() |
393 | const account = { | 398 | const account = { |
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index b69b47265..d8f3f13da 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts | |||
@@ -51,11 +51,11 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM | |||
51 | @AllowNull(false) | 51 | @AllowNull(false) |
52 | @Default(null) | 52 | @Default(null) |
53 | @Is( | 53 | @Is( |
54 | 'UserNotificationSettingVideoAbuseAsModerator', | 54 | 'UserNotificationSettingAbuseAsModerator', |
55 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') | 55 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator') |
56 | ) | 56 | ) |
57 | @Column | 57 | @Column |
58 | videoAbuseAsModerator: UserNotificationSettingValue | 58 | abuseAsModerator: UserNotificationSettingValue |
59 | 59 | ||
60 | @AllowNull(false) | 60 | @AllowNull(false) |
61 | @Default(null) | 61 | @Default(null) |
@@ -166,7 +166,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM | |||
166 | return { | 166 | return { |
167 | newCommentOnMyVideo: this.newCommentOnMyVideo, | 167 | newCommentOnMyVideo: this.newCommentOnMyVideo, |
168 | newVideoFromSubscription: this.newVideoFromSubscription, | 168 | newVideoFromSubscription: this.newVideoFromSubscription, |
169 | videoAbuseAsModerator: this.videoAbuseAsModerator, | 169 | abuseAsModerator: this.abuseAsModerator, |
170 | videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, | 170 | videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, |
171 | blacklistOnMyVideo: this.blacklistOnMyVideo, | 171 | blacklistOnMyVideo: this.blacklistOnMyVideo, |
172 | myVideoPublished: this.myVideoPublished, | 172 | myVideoPublished: this.myVideoPublished, |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 30985bb0f..2945bf709 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -1,22 +1,24 @@ | |||
1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | ||
2 | import { UserNotification, UserNotificationType } from '../../../shared' | 4 | import { UserNotification, UserNotificationType } from '../../../shared' |
3 | import { getSort, throwIfNotValid } from '../utils' | ||
4 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 5 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
5 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | 6 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' |
6 | import { UserModel } from './user' | 7 | import { AbuseModel } from '../abuse/abuse' |
7 | import { VideoModel } from '../video/video' | 8 | import { VideoAbuseModel } from '../abuse/video-abuse' |
8 | import { VideoCommentModel } from '../video/video-comment' | 9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
9 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | ||
10 | import { VideoChannelModel } from '../video/video-channel' | ||
11 | import { AccountModel } from './account' | ||
12 | import { VideoAbuseModel } from '../video/video-abuse' | ||
13 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
14 | import { VideoImportModel } from '../video/video-import' | ||
15 | import { ActorModel } from '../activitypub/actor' | 10 | import { ActorModel } from '../activitypub/actor' |
16 | import { ActorFollowModel } from '../activitypub/actor-follow' | 11 | import { ActorFollowModel } from '../activitypub/actor-follow' |
17 | import { AvatarModel } from '../avatar/avatar' | 12 | import { AvatarModel } from '../avatar/avatar' |
18 | import { ServerModel } from '../server/server' | 13 | import { ServerModel } from '../server/server' |
19 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | 14 | import { getSort, throwIfNotValid } from '../utils' |
15 | import { VideoModel } from '../video/video' | ||
16 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
17 | import { VideoChannelModel } from '../video/video-channel' | ||
18 | import { VideoCommentModel } from '../video/video-comment' | ||
19 | import { VideoImportModel } from '../video/video-import' | ||
20 | import { AccountModel } from './account' | ||
21 | import { UserModel } from './user' | ||
20 | 22 | ||
21 | enum ScopeNames { | 23 | enum ScopeNames { |
22 | WITH_ALL = 'WITH_ALL' | 24 | WITH_ALL = 'WITH_ALL' |
@@ -87,9 +89,41 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
87 | 89 | ||
88 | { | 90 | { |
89 | attributes: [ 'id' ], | 91 | attributes: [ 'id' ], |
90 | model: VideoAbuseModel.unscoped(), | 92 | model: AbuseModel.unscoped(), |
91 | required: false, | 93 | required: false, |
92 | include: [ buildVideoInclude(true) ] | 94 | include: [ |
95 | { | ||
96 | attributes: [ 'id' ], | ||
97 | model: VideoAbuseModel.unscoped(), | ||
98 | required: false, | ||
99 | include: [ buildVideoInclude(true) ] | ||
100 | }, | ||
101 | { | ||
102 | attributes: [ 'id' ], | ||
103 | model: VideoCommentAbuseModel.unscoped(), | ||
104 | required: false, | ||
105 | include: [ | ||
106 | { | ||
107 | attributes: [ 'id', 'originCommentId' ], | ||
108 | model: VideoCommentModel, | ||
109 | required: true, | ||
110 | include: [ | ||
111 | { | ||
112 | attributes: [ 'id', 'name', 'uuid' ], | ||
113 | model: VideoModel.unscoped(), | ||
114 | required: true | ||
115 | } | ||
116 | ] | ||
117 | } | ||
118 | ] | ||
119 | }, | ||
120 | { | ||
121 | model: AccountModel, | ||
122 | as: 'FlaggedAccount', | ||
123 | required: true, | ||
124 | include: [ buildActorWithAvatarInclude() ] | ||
125 | } | ||
126 | ] | ||
93 | }, | 127 | }, |
94 | 128 | ||
95 | { | 129 | { |
@@ -179,9 +213,9 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
179 | } | 213 | } |
180 | }, | 214 | }, |
181 | { | 215 | { |
182 | fields: [ 'videoAbuseId' ], | 216 | fields: [ 'abuseId' ], |
183 | where: { | 217 | where: { |
184 | videoAbuseId: { | 218 | abuseId: { |
185 | [Op.ne]: null | 219 | [Op.ne]: null |
186 | } | 220 | } |
187 | } | 221 | } |
@@ -276,17 +310,17 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
276 | }) | 310 | }) |
277 | Comment: VideoCommentModel | 311 | Comment: VideoCommentModel |
278 | 312 | ||
279 | @ForeignKey(() => VideoAbuseModel) | 313 | @ForeignKey(() => AbuseModel) |
280 | @Column | 314 | @Column |
281 | videoAbuseId: number | 315 | abuseId: number |
282 | 316 | ||
283 | @BelongsTo(() => VideoAbuseModel, { | 317 | @BelongsTo(() => AbuseModel, { |
284 | foreignKey: { | 318 | foreignKey: { |
285 | allowNull: true | 319 | allowNull: true |
286 | }, | 320 | }, |
287 | onDelete: 'cascade' | 321 | onDelete: 'cascade' |
288 | }) | 322 | }) |
289 | VideoAbuse: VideoAbuseModel | 323 | Abuse: AbuseModel |
290 | 324 | ||
291 | @ForeignKey(() => VideoBlacklistModel) | 325 | @ForeignKey(() => VideoBlacklistModel) |
292 | @Column | 326 | @Column |
@@ -397,10 +431,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
397 | video: this.formatVideo(this.Comment.Video) | 431 | video: this.formatVideo(this.Comment.Video) |
398 | } : undefined | 432 | } : undefined |
399 | 433 | ||
400 | const videoAbuse = this.VideoAbuse ? { | 434 | const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined |
401 | id: this.VideoAbuse.id, | ||
402 | video: this.formatVideo(this.VideoAbuse.Video) | ||
403 | } : undefined | ||
404 | 435 | ||
405 | const videoBlacklist = this.VideoBlacklist ? { | 436 | const videoBlacklist = this.VideoBlacklist ? { |
406 | id: this.VideoBlacklist.id, | 437 | id: this.VideoBlacklist.id, |
@@ -439,7 +470,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
439 | video, | 470 | video, |
440 | videoImport, | 471 | videoImport, |
441 | comment, | 472 | comment, |
442 | videoAbuse, | 473 | abuse, |
443 | videoBlacklist, | 474 | videoBlacklist, |
444 | account, | 475 | account, |
445 | actorFollow, | 476 | actorFollow, |
@@ -456,6 +487,29 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
456 | } | 487 | } |
457 | } | 488 | } |
458 | 489 | ||
490 | formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { | ||
491 | const commentAbuse = abuse.VideoCommentAbuse?.VideoComment ? { | ||
492 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), | ||
493 | |||
494 | video: { | ||
495 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, | ||
496 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, | ||
497 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid | ||
498 | } | ||
499 | } : undefined | ||
500 | |||
501 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined | ||
502 | |||
503 | const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined | ||
504 | |||
505 | return { | ||
506 | id: abuse.id, | ||
507 | video: videoAbuse, | ||
508 | comment: commentAbuse, | ||
509 | account: accountAbuse | ||
510 | } | ||
511 | } | ||
512 | |||
459 | formatActor ( | 513 | formatActor ( |
460 | this: UserNotificationModelForApi, | 514 | this: UserNotificationModelForApi, |
461 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor | 515 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index de193131a..5f45f8e7c 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -19,7 +19,7 @@ import { | |||
19 | Table, | 19 | Table, |
20 | UpdatedAt | 20 | UpdatedAt |
21 | } from 'sequelize-typescript' | 21 | } from 'sequelize-typescript' |
22 | import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' | 22 | import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' |
23 | import { User, UserRole } from '../../../shared/models/users' | 23 | import { User, UserRole } from '../../../shared/models/users' |
24 | import { | 24 | import { |
25 | isNoInstanceConfigWarningModal, | 25 | isNoInstanceConfigWarningModal, |
@@ -168,28 +168,26 @@ enum ScopeNames { | |||
168 | '(' + | 168 | '(' + |
169 | `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + | 169 | `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + |
170 | 'FROM (' + | 170 | 'FROM (' + |
171 | 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + | 171 | 'SELECT COUNT("abuse"."id") AS "abuses", ' + |
172 | `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` + | 172 | `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + |
173 | 'FROM "videoAbuse" ' + | 173 | 'FROM "abuse" ' + |
174 | 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + | 174 | 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' + |
175 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
176 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
177 | 'WHERE "account"."userId" = "UserModel"."id"' + | 175 | 'WHERE "account"."userId" = "UserModel"."id"' + |
178 | ') t' + | 176 | ') t' + |
179 | ')' | 177 | ')' |
180 | ), | 178 | ), |
181 | 'videoAbusesCount' | 179 | 'abusesCount' |
182 | ], | 180 | ], |
183 | [ | 181 | [ |
184 | literal( | 182 | literal( |
185 | '(' + | 183 | '(' + |
186 | 'SELECT COUNT("videoAbuse"."id") ' + | 184 | 'SELECT COUNT("abuse"."id") ' + |
187 | 'FROM "videoAbuse" ' + | 185 | 'FROM "abuse" ' + |
188 | 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' + | 186 | 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' + |
189 | 'WHERE "account"."userId" = "UserModel"."id"' + | 187 | 'WHERE "account"."userId" = "UserModel"."id"' + |
190 | ')' | 188 | ')' |
191 | ), | 189 | ), |
192 | 'videoAbusesCreatedCount' | 190 | 'abusesCreatedCount' |
193 | ], | 191 | ], |
194 | [ | 192 | [ |
195 | literal( | 193 | literal( |
@@ -780,8 +778,8 @@ export class UserModel extends Model<UserModel> { | |||
780 | const videoQuotaUsed = this.get('videoQuotaUsed') | 778 | const videoQuotaUsed = this.get('videoQuotaUsed') |
781 | const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') | 779 | const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') |
782 | const videosCount = this.get('videosCount') | 780 | const videosCount = this.get('videosCount') |
783 | const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':') | 781 | const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':') |
784 | const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount') | 782 | const abusesCreatedCount = this.get('abusesCreatedCount') |
785 | const videoCommentsCount = this.get('videoCommentsCount') | 783 | const videoCommentsCount = this.get('videoCommentsCount') |
786 | 784 | ||
787 | const json: User = { | 785 | const json: User = { |
@@ -815,14 +813,14 @@ export class UserModel extends Model<UserModel> { | |||
815 | videosCount: videosCount !== undefined | 813 | videosCount: videosCount !== undefined |
816 | ? parseInt(videosCount + '', 10) | 814 | ? parseInt(videosCount + '', 10) |
817 | : undefined, | 815 | : undefined, |
818 | videoAbusesCount: videoAbusesCount | 816 | abusesCount: abusesCount |
819 | ? parseInt(videoAbusesCount, 10) | 817 | ? parseInt(abusesCount, 10) |
820 | : undefined, | 818 | : undefined, |
821 | videoAbusesAcceptedCount: videoAbusesAcceptedCount | 819 | abusesAcceptedCount: abusesAcceptedCount |
822 | ? parseInt(videoAbusesAcceptedCount, 10) | 820 | ? parseInt(abusesAcceptedCount, 10) |
823 | : undefined, | 821 | : undefined, |
824 | videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined | 822 | abusesCreatedCount: abusesCreatedCount !== undefined |
825 | ? parseInt(videoAbusesCreatedCount + '', 10) | 823 | ? parseInt(abusesCreatedCount + '', 10) |
826 | : undefined, | 824 | : undefined, |
827 | videoCommentsCount: videoCommentsCount !== undefined | 825 | videoCommentsCount: videoCommentsCount !== undefined |
828 | ? parseInt(videoCommentsCount + '', 10) | 826 | ? parseInt(videoCommentsCount + '', 10) |
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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { Op } from 'sequelize' | ||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 3 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
4 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | ||
5 | import { ServerBlock } from '@shared/models' | ||
2 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
3 | import { ServerModel } from './server' | ||
4 | import { ServerBlock } from '../../../shared/models/blocklist' | ||
5 | import { getSort, searchAttribute } from '../utils' | 7 | import { getSort, searchAttribute } from '../utils' |
6 | import * as Bluebird from 'bluebird' | 8 | import { ServerModel } from './server' |
7 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | ||
8 | import { Op } from 'sequelize' | ||
9 | 9 | ||
10 | enum ScopeNames { | 10 | enum ScopeNames { |
11 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 11 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts deleted file mode 100644 index 1319332f0..000000000 --- a/server/models/video/video-abuse.ts +++ /dev/null | |||
@@ -1,479 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { literal, Op } from 'sequelize' | ||
3 | import { | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | Default, | ||
10 | ForeignKey, | ||
11 | Is, | ||
12 | Model, | ||
13 | Scopes, | ||
14 | Table, | ||
15 | UpdatedAt | ||
16 | } from 'sequelize-typescript' | ||
17 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' | ||
18 | import { | ||
19 | VideoAbuseState, | ||
20 | VideoDetails, | ||
21 | VideoAbusePredefinedReasons, | ||
22 | VideoAbusePredefinedReasonsString, | ||
23 | videoAbusePredefinedReasonsMap | ||
24 | } from '../../../shared' | ||
25 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | ||
26 | import { VideoAbuse } from '../../../shared/models/videos' | ||
27 | import { | ||
28 | isVideoAbuseModerationCommentValid, | ||
29 | isVideoAbuseReasonValid, | ||
30 | isVideoAbuseStateValid | ||
31 | } from '../../helpers/custom-validators/video-abuses' | ||
32 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' | ||
33 | import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models' | ||
34 | import { AccountModel } from '../account/account' | ||
35 | import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' | ||
36 | import { ThumbnailModel } from './thumbnail' | ||
37 | import { VideoModel } from './video' | ||
38 | import { VideoBlacklistModel } from './video-blacklist' | ||
39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | ||
40 | import { invert } from 'lodash' | ||
41 | |||
42 | export enum ScopeNames { | ||
43 | FOR_API = 'FOR_API' | ||
44 | } | ||
45 | |||
46 | @Scopes(() => ({ | ||
47 | [ScopeNames.FOR_API]: (options: { | ||
48 | // search | ||
49 | search?: string | ||
50 | searchReporter?: string | ||
51 | searchReportee?: string | ||
52 | searchVideo?: string | ||
53 | searchVideoChannel?: string | ||
54 | |||
55 | // filters | ||
56 | id?: number | ||
57 | predefinedReasonId?: number | ||
58 | |||
59 | state?: VideoAbuseState | ||
60 | videoIs?: VideoAbuseVideoIs | ||
61 | |||
62 | // accountIds | ||
63 | serverAccountId: number | ||
64 | userAccountId: number | ||
65 | }) => { | ||
66 | const where = { | ||
67 | reporterAccountId: { | ||
68 | [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') | ||
69 | } | ||
70 | } | ||
71 | |||
72 | if (options.search) { | ||
73 | Object.assign(where, { | ||
74 | [Op.or]: [ | ||
75 | { | ||
76 | [Op.and]: [ | ||
77 | { videoId: { [Op.not]: null } }, | ||
78 | searchAttribute(options.search, '$Video.name$') | ||
79 | ] | ||
80 | }, | ||
81 | { | ||
82 | [Op.and]: [ | ||
83 | { videoId: { [Op.not]: null } }, | ||
84 | searchAttribute(options.search, '$Video.VideoChannel.name$') | ||
85 | ] | ||
86 | }, | ||
87 | { | ||
88 | [Op.and]: [ | ||
89 | { deletedVideo: { [Op.not]: null } }, | ||
90 | { deletedVideo: searchAttribute(options.search, 'name') } | ||
91 | ] | ||
92 | }, | ||
93 | { | ||
94 | [Op.and]: [ | ||
95 | { deletedVideo: { [Op.not]: null } }, | ||
96 | { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } } | ||
97 | ] | ||
98 | }, | ||
99 | searchAttribute(options.search, '$Account.name$') | ||
100 | ] | ||
101 | }) | ||
102 | } | ||
103 | |||
104 | if (options.id) Object.assign(where, { id: options.id }) | ||
105 | if (options.state) Object.assign(where, { state: options.state }) | ||
106 | |||
107 | if (options.videoIs === 'deleted') { | ||
108 | Object.assign(where, { | ||
109 | deletedVideo: { | ||
110 | [Op.not]: null | ||
111 | } | ||
112 | }) | ||
113 | } | ||
114 | |||
115 | if (options.predefinedReasonId) { | ||
116 | Object.assign(where, { | ||
117 | predefinedReasons: { | ||
118 | [Op.contains]: [ options.predefinedReasonId ] | ||
119 | } | ||
120 | }) | ||
121 | } | ||
122 | |||
123 | const onlyBlacklisted = options.videoIs === 'blacklisted' | ||
124 | |||
125 | return { | ||
126 | attributes: { | ||
127 | include: [ | ||
128 | [ | ||
129 | // we don't care about this count for deleted videos, so there are not included | ||
130 | literal( | ||
131 | '(' + | ||
132 | 'SELECT count(*) ' + | ||
133 | 'FROM "videoAbuse" ' + | ||
134 | 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' + | ||
135 | ')' | ||
136 | ), | ||
137 | 'countReportsForVideo' | ||
138 | ], | ||
139 | [ | ||
140 | // we don't care about this count for deleted videos, so there are not included | ||
141 | literal( | ||
142 | '(' + | ||
143 | 'SELECT t.nth ' + | ||
144 | 'FROM ( ' + | ||
145 | 'SELECT id, ' + | ||
146 | 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + | ||
147 | 'FROM "videoAbuse" ' + | ||
148 | ') t ' + | ||
149 | 'WHERE t.id = "VideoAbuseModel".id ' + | ||
150 | ')' | ||
151 | ), | ||
152 | 'nthReportForVideo' | ||
153 | ], | ||
154 | [ | ||
155 | literal( | ||
156 | '(' + | ||
157 | 'SELECT count("videoAbuse"."id") ' + | ||
158 | 'FROM "videoAbuse" ' + | ||
159 | 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + | ||
160 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
161 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + | ||
162 | 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' + | ||
163 | ')' | ||
164 | ), | ||
165 | 'countReportsForReporter__video' | ||
166 | ], | ||
167 | [ | ||
168 | literal( | ||
169 | '(' + | ||
170 | 'SELECT count(DISTINCT "videoAbuse"."id") ' + | ||
171 | 'FROM "videoAbuse" ' + | ||
172 | `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` + | ||
173 | ')' | ||
174 | ), | ||
175 | 'countReportsForReporter__deletedVideo' | ||
176 | ], | ||
177 | [ | ||
178 | literal( | ||
179 | '(' + | ||
180 | 'SELECT count(DISTINCT "videoAbuse"."id") ' + | ||
181 | 'FROM "videoAbuse" ' + | ||
182 | 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + | ||
183 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
184 | 'INNER JOIN "account" ON ' + | ||
185 | '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' + | ||
186 | `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + | ||
187 | ')' | ||
188 | ), | ||
189 | 'countReportsForReportee__video' | ||
190 | ], | ||
191 | [ | ||
192 | literal( | ||
193 | '(' + | ||
194 | 'SELECT count(DISTINCT "videoAbuse"."id") ' + | ||
195 | 'FROM "videoAbuse" ' + | ||
196 | `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` + | ||
197 | `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` + | ||
198 | `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + | ||
199 | ')' | ||
200 | ), | ||
201 | 'countReportsForReportee__deletedVideo' | ||
202 | ] | ||
203 | ] | ||
204 | }, | ||
205 | include: [ | ||
206 | { | ||
207 | model: AccountModel, | ||
208 | required: true, | ||
209 | where: searchAttribute(options.searchReporter, 'name') | ||
210 | }, | ||
211 | { | ||
212 | model: VideoModel, | ||
213 | required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel), | ||
214 | where: searchAttribute(options.searchVideo, 'name'), | ||
215 | include: [ | ||
216 | { | ||
217 | model: ThumbnailModel | ||
218 | }, | ||
219 | { | ||
220 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
221 | where: searchAttribute(options.searchVideoChannel, 'name'), | ||
222 | include: [ | ||
223 | { | ||
224 | model: AccountModel, | ||
225 | where: searchAttribute(options.searchReportee, 'name') | ||
226 | } | ||
227 | ] | ||
228 | }, | ||
229 | { | ||
230 | attributes: [ 'id', 'reason', 'unfederated' ], | ||
231 | model: VideoBlacklistModel, | ||
232 | required: onlyBlacklisted | ||
233 | } | ||
234 | ] | ||
235 | } | ||
236 | ], | ||
237 | where | ||
238 | } | ||
239 | } | ||
240 | })) | ||
241 | @Table({ | ||
242 | tableName: 'videoAbuse', | ||
243 | indexes: [ | ||
244 | { | ||
245 | fields: [ 'videoId' ] | ||
246 | }, | ||
247 | { | ||
248 | fields: [ 'reporterAccountId' ] | ||
249 | } | ||
250 | ] | ||
251 | }) | ||
252 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | ||
253 | |||
254 | @AllowNull(false) | ||
255 | @Default(null) | ||
256 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) | ||
257 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) | ||
258 | reason: string | ||
259 | |||
260 | @AllowNull(false) | ||
261 | @Default(null) | ||
262 | @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state')) | ||
263 | @Column | ||
264 | state: VideoAbuseState | ||
265 | |||
266 | @AllowNull(true) | ||
267 | @Default(null) | ||
268 | @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true)) | ||
269 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) | ||
270 | moderationComment: string | ||
271 | |||
272 | @AllowNull(true) | ||
273 | @Default(null) | ||
274 | @Column(DataType.JSONB) | ||
275 | deletedVideo: VideoDetails | ||
276 | |||
277 | @AllowNull(true) | ||
278 | @Default(null) | ||
279 | @Column(DataType.ARRAY(DataType.INTEGER)) | ||
280 | predefinedReasons: VideoAbusePredefinedReasons[] | ||
281 | |||
282 | @AllowNull(true) | ||
283 | @Default(null) | ||
284 | @Column | ||
285 | startAt: number | ||
286 | |||
287 | @AllowNull(true) | ||
288 | @Default(null) | ||
289 | @Column | ||
290 | endAt: number | ||
291 | |||
292 | @CreatedAt | ||
293 | createdAt: Date | ||
294 | |||
295 | @UpdatedAt | ||
296 | updatedAt: Date | ||
297 | |||
298 | @ForeignKey(() => AccountModel) | ||
299 | @Column | ||
300 | reporterAccountId: number | ||
301 | |||
302 | @BelongsTo(() => AccountModel, { | ||
303 | foreignKey: { | ||
304 | allowNull: true | ||
305 | }, | ||
306 | onDelete: 'set null' | ||
307 | }) | ||
308 | Account: AccountModel | ||
309 | |||
310 | @ForeignKey(() => VideoModel) | ||
311 | @Column | ||
312 | videoId: number | ||
313 | |||
314 | @BelongsTo(() => VideoModel, { | ||
315 | foreignKey: { | ||
316 | allowNull: true | ||
317 | }, | ||
318 | onDelete: 'set null' | ||
319 | }) | ||
320 | Video: VideoModel | ||
321 | |||
322 | static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> { | ||
323 | const videoAttributes = {} | ||
324 | if (videoId) videoAttributes['videoId'] = videoId | ||
325 | if (uuid) videoAttributes['deletedVideo'] = { uuid } | ||
326 | |||
327 | const query = { | ||
328 | where: { | ||
329 | id, | ||
330 | ...videoAttributes | ||
331 | } | ||
332 | } | ||
333 | return VideoAbuseModel.findOne(query) | ||
334 | } | ||
335 | |||
336 | static listForApi (parameters: { | ||
337 | start: number | ||
338 | count: number | ||
339 | sort: string | ||
340 | |||
341 | serverAccountId: number | ||
342 | user?: MUserAccountId | ||
343 | |||
344 | id?: number | ||
345 | predefinedReason?: VideoAbusePredefinedReasonsString | ||
346 | state?: VideoAbuseState | ||
347 | videoIs?: VideoAbuseVideoIs | ||
348 | |||
349 | search?: string | ||
350 | searchReporter?: string | ||
351 | searchReportee?: string | ||
352 | searchVideo?: string | ||
353 | searchVideoChannel?: string | ||
354 | }) { | ||
355 | const { | ||
356 | start, | ||
357 | count, | ||
358 | sort, | ||
359 | search, | ||
360 | user, | ||
361 | serverAccountId, | ||
362 | state, | ||
363 | videoIs, | ||
364 | predefinedReason, | ||
365 | searchReportee, | ||
366 | searchVideo, | ||
367 | searchVideoChannel, | ||
368 | searchReporter, | ||
369 | id | ||
370 | } = parameters | ||
371 | |||
372 | const userAccountId = user ? user.Account.id : undefined | ||
373 | const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined | ||
374 | |||
375 | const query = { | ||
376 | offset: start, | ||
377 | limit: count, | ||
378 | order: getSort(sort), | ||
379 | col: 'VideoAbuseModel.id', | ||
380 | distinct: true | ||
381 | } | ||
382 | |||
383 | const filters = { | ||
384 | id, | ||
385 | predefinedReasonId, | ||
386 | search, | ||
387 | state, | ||
388 | videoIs, | ||
389 | searchReportee, | ||
390 | searchVideo, | ||
391 | searchVideoChannel, | ||
392 | searchReporter, | ||
393 | serverAccountId, | ||
394 | userAccountId | ||
395 | } | ||
396 | |||
397 | return VideoAbuseModel | ||
398 | .scope([ | ||
399 | { method: [ ScopeNames.FOR_API, filters ] } | ||
400 | ]) | ||
401 | .findAndCountAll(query) | ||
402 | .then(({ rows, count }) => { | ||
403 | return { total: count, data: rows } | ||
404 | }) | ||
405 | } | ||
406 | |||
407 | toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { | ||
408 | const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
409 | const countReportsForVideo = this.get('countReportsForVideo') as number | ||
410 | const nthReportForVideo = this.get('nthReportForVideo') as number | ||
411 | const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number | ||
412 | const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number | ||
413 | const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number | ||
414 | const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number | ||
415 | |||
416 | const video = this.Video | ||
417 | ? this.Video | ||
418 | : this.deletedVideo | ||
419 | |||
420 | return { | ||
421 | id: this.id, | ||
422 | reason: this.reason, | ||
423 | predefinedReasons, | ||
424 | reporterAccount: this.Account.toFormattedJSON(), | ||
425 | state: { | ||
426 | id: this.state, | ||
427 | label: VideoAbuseModel.getStateLabel(this.state) | ||
428 | }, | ||
429 | moderationComment: this.moderationComment, | ||
430 | video: { | ||
431 | id: video.id, | ||
432 | uuid: video.uuid, | ||
433 | name: video.name, | ||
434 | nsfw: video.nsfw, | ||
435 | deleted: !this.Video, | ||
436 | blacklisted: this.Video?.isBlacklisted() || false, | ||
437 | thumbnailPath: this.Video?.getMiniatureStaticPath(), | ||
438 | channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel | ||
439 | }, | ||
440 | createdAt: this.createdAt, | ||
441 | updatedAt: this.updatedAt, | ||
442 | startAt: this.startAt, | ||
443 | endAt: this.endAt, | ||
444 | count: countReportsForVideo || 0, | ||
445 | nth: nthReportForVideo || 0, | ||
446 | countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), | ||
447 | countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0) | ||
448 | } | ||
449 | } | ||
450 | |||
451 | toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject { | ||
452 | const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
453 | |||
454 | const startAt = this.startAt | ||
455 | const endAt = this.endAt | ||
456 | |||
457 | return { | ||
458 | type: 'Flag' as 'Flag', | ||
459 | content: this.reason, | ||
460 | object: this.Video.url, | ||
461 | tag: predefinedReasons.map(r => ({ | ||
462 | type: 'Hashtag' as 'Hashtag', | ||
463 | name: r | ||
464 | })), | ||
465 | startAt, | ||
466 | endAt | ||
467 | } | ||
468 | } | ||
469 | |||
470 | private static getStateLabel (id: number) { | ||
471 | return VIDEO_ABUSE_STATES[id] || 'Unknown' | ||
472 | } | ||
473 | |||
474 | private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] { | ||
475 | return (predefinedReasons || []) | ||
476 | .filter(r => r in VideoAbusePredefinedReasons) | ||
477 | .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString) | ||
478 | } | ||
479 | } | ||
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 9cee64229..03a3cdf81 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -61,6 +61,7 @@ type AvailableWithStatsOptions = { | |||
61 | } | 61 | } |
62 | 62 | ||
63 | export type SummaryOptions = { | 63 | export type SummaryOptions = { |
64 | actorRequired?: boolean // Default: true | ||
64 | withAccount?: boolean // Default: false | 65 | withAccount?: boolean // Default: false |
65 | withAccountBlockerIds?: number[] | 66 | withAccountBlockerIds?: number[] |
66 | } | 67 | } |
@@ -121,7 +122,7 @@ export type SummaryOptions = { | |||
121 | { | 122 | { |
122 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 123 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], |
123 | model: ActorModel.unscoped(), | 124 | model: ActorModel.unscoped(), |
124 | required: true, | 125 | required: options.actorRequired ?? true, |
125 | include: [ | 126 | include: [ |
126 | { | 127 | { |
127 | attributes: [ 'host' ], | 128 | attributes: [ 'host' ], |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 90625d987..75b914b8c 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,7 +1,20 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { uniq } from 'lodash' | 2 | import { uniq } from 'lodash' |
3 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' | 3 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' |
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { |
5 | AllowNull, | ||
6 | BelongsTo, | ||
7 | Column, | ||
8 | CreatedAt, | ||
9 | DataType, | ||
10 | ForeignKey, | ||
11 | HasMany, | ||
12 | Is, | ||
13 | Model, | ||
14 | Scopes, | ||
15 | Table, | ||
16 | UpdatedAt | ||
17 | } from 'sequelize-typescript' | ||
5 | import { getServerActor } from '@server/models/application/application' | 18 | import { getServerActor } from '@server/models/application/application' |
6 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 19 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
7 | import { VideoPrivacy } from '@shared/models' | 20 | import { VideoPrivacy } from '@shared/models' |
@@ -24,6 +37,7 @@ import { | |||
24 | MCommentOwnerVideoReply, | 37 | MCommentOwnerVideoReply, |
25 | MVideoImmutable | 38 | MVideoImmutable |
26 | } from '../../types/models/video' | 39 | } from '../../types/models/video' |
40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | ||
27 | import { AccountModel } from '../account/account' | 41 | import { AccountModel } from '../account/account' |
28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 42 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
29 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' | 43 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' |
@@ -224,6 +238,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
224 | }) | 238 | }) |
225 | Account: AccountModel | 239 | Account: AccountModel |
226 | 240 | ||
241 | @HasMany(() => VideoCommentAbuseModel, { | ||
242 | foreignKey: { | ||
243 | name: 'videoCommentId', | ||
244 | allowNull: true | ||
245 | }, | ||
246 | onDelete: 'set null' | ||
247 | }) | ||
248 | CommentAbuses: VideoCommentAbuseModel[] | ||
249 | |||
227 | static loadById (id: number, t?: Transaction): Bluebird<MComment> { | 250 | static loadById (id: number, t?: Transaction): Bluebird<MComment> { |
228 | const query: FindOptions = { | 251 | const query: FindOptions = { |
229 | where: { | 252 | where: { |
@@ -632,7 +655,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
632 | id: this.id, | 655 | id: this.id, |
633 | url: this.url, | 656 | url: this.url, |
634 | text: this.text, | 657 | text: this.text, |
635 | threadId: this.originCommentId || this.id, | 658 | threadId: this.getThreadId(), |
636 | inReplyToCommentId: this.inReplyToCommentId || null, | 659 | inReplyToCommentId: this.inReplyToCommentId || null, |
637 | videoId: this.videoId, | 660 | videoId: this.videoId, |
638 | createdAt: this.createdAt, | 661 | createdAt: this.createdAt, |
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index 984b0e6af..466890364 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts | |||
@@ -327,7 +327,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) | |||
327 | attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') | 327 | attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') |
328 | } | 328 | } |
329 | 329 | ||
330 | order = buildOrder(model, options.sort) | 330 | order = buildOrder(options.sort) |
331 | suffix += `${order} ` | 331 | suffix += `${order} ` |
332 | } | 332 | } |
333 | 333 | ||
@@ -357,7 +357,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) | |||
357 | return { query, replacements, order } | 357 | return { query, replacements, order } |
358 | } | 358 | } |
359 | 359 | ||
360 | function buildOrder (model: typeof Model, value: string) { | 360 | function buildOrder (value: string) { |
361 | const { direction, field } = buildDirectionAndField(value) | 361 | const { direction, field } = buildDirectionAndField(value) |
362 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | 362 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) |
363 | 363 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e2718300e..43609587c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { remove } from 'fs-extra' | ||
2 | import { maxBy, minBy, pick } from 'lodash' | 3 | import { maxBy, minBy, pick } from 'lodash' |
3 | import { join } from 'path' | 4 | import { join } from 'path' |
4 | import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 5 | import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
@@ -23,10 +24,18 @@ import { | |||
23 | Table, | 24 | Table, |
24 | UpdatedAt | 25 | UpdatedAt |
25 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
26 | import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
28 | import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video' | ||
29 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
30 | import { getServerActor } from '@server/models/application/application' | ||
31 | import { ModelCache } from '@server/models/model-cache' | ||
32 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
33 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' | ||
27 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 34 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
28 | import { Video, VideoDetails } from '../../../shared/models/videos' | 35 | import { Video, VideoDetails } from '../../../shared/models/videos' |
36 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
29 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 37 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
38 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
30 | import { peertubeTruncate } from '../../helpers/core-utils' | 39 | import { peertubeTruncate } from '../../helpers/core-utils' |
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 40 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 41 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
@@ -43,6 +52,7 @@ import { | |||
43 | } from '../../helpers/custom-validators/videos' | 52 | } from '../../helpers/custom-validators/videos' |
44 | import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' | 53 | import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' |
45 | import { logger } from '../../helpers/logger' | 54 | import { logger } from '../../helpers/logger' |
55 | import { CONFIG } from '../../initializers/config' | ||
46 | import { | 56 | import { |
47 | ACTIVITY_PUB, | 57 | ACTIVITY_PUB, |
48 | API_VERSION, | 58 | API_VERSION, |
@@ -59,40 +69,6 @@ import { | |||
59 | WEBSERVER | 69 | WEBSERVER |
60 | } from '../../initializers/constants' | 70 | } from '../../initializers/constants' |
61 | import { sendDeleteVideo } from '../../lib/activitypub/send' | 71 | import { sendDeleteVideo } from '../../lib/activitypub/send' |
62 | import { AccountModel } from '../account/account' | ||
63 | import { AccountVideoRateModel } from '../account/account-video-rate' | ||
64 | import { ActorModel } from '../activitypub/actor' | ||
65 | import { AvatarModel } from '../avatar/avatar' | ||
66 | import { ServerModel } from '../server/server' | ||
67 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | ||
68 | import { TagModel } from './tag' | ||
69 | import { VideoAbuseModel } from './video-abuse' | ||
70 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | ||
71 | import { VideoCommentModel } from './video-comment' | ||
72 | import { VideoFileModel } from './video-file' | ||
73 | import { VideoShareModel } from './video-share' | ||
74 | import { VideoTagModel } from './video-tag' | ||
75 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | ||
76 | import { VideoCaptionModel } from './video-caption' | ||
77 | import { VideoBlacklistModel } from './video-blacklist' | ||
78 | import { remove } from 'fs-extra' | ||
79 | import { VideoViewModel } from './video-view' | ||
80 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
81 | import { | ||
82 | videoFilesModelToFormattedJSON, | ||
83 | VideoFormattingJSONOptions, | ||
84 | videoModelToActivityPubObject, | ||
85 | videoModelToFormattedDetailsJSON, | ||
86 | videoModelToFormattedJSON | ||
87 | } from './video-format-utils' | ||
88 | import { UserVideoHistoryModel } from '../account/user-video-history' | ||
89 | import { VideoImportModel } from './video-import' | ||
90 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | ||
91 | import { VideoPlaylistElementModel } from './video-playlist-element' | ||
92 | import { CONFIG } from '../../initializers/config' | ||
93 | import { ThumbnailModel } from './thumbnail' | ||
94 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
95 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
96 | import { | 72 | import { |
97 | MChannel, | 73 | MChannel, |
98 | MChannelAccountDefault, | 74 | MChannelAccountDefault, |
@@ -118,15 +94,39 @@ import { | |||
118 | MVideoWithFile, | 94 | MVideoWithFile, |
119 | MVideoWithRights | 95 | MVideoWithRights |
120 | } from '../../types/models' | 96 | } from '../../types/models' |
121 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' | ||
122 | import { MThumbnail } from '../../types/models/video/thumbnail' | 97 | import { MThumbnail } from '../../types/models/video/thumbnail' |
123 | import { VideoFile } from '@shared/models/videos/video-file.model' | 98 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' |
124 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 99 | import { VideoAbuseModel } from '../abuse/video-abuse' |
125 | import { ModelCache } from '@server/models/model-cache' | 100 | import { AccountModel } from '../account/account' |
101 | import { AccountVideoRateModel } from '../account/account-video-rate' | ||
102 | import { UserVideoHistoryModel } from '../account/user-video-history' | ||
103 | import { ActorModel } from '../activitypub/actor' | ||
104 | import { AvatarModel } from '../avatar/avatar' | ||
105 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
106 | import { ServerModel } from '../server/server' | ||
107 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | ||
108 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | ||
109 | import { TagModel } from './tag' | ||
110 | import { ThumbnailModel } from './thumbnail' | ||
111 | import { VideoBlacklistModel } from './video-blacklist' | ||
112 | import { VideoCaptionModel } from './video-caption' | ||
113 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | ||
114 | import { VideoCommentModel } from './video-comment' | ||
115 | import { VideoFileModel } from './video-file' | ||
116 | import { | ||
117 | videoFilesModelToFormattedJSON, | ||
118 | VideoFormattingJSONOptions, | ||
119 | videoModelToActivityPubObject, | ||
120 | videoModelToFormattedDetailsJSON, | ||
121 | videoModelToFormattedJSON | ||
122 | } from './video-format-utils' | ||
123 | import { VideoImportModel } from './video-import' | ||
124 | import { VideoPlaylistElementModel } from './video-playlist-element' | ||
126 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' | 125 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' |
127 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 126 | import { VideoShareModel } from './video-share' |
128 | import { getServerActor } from '@server/models/application/application' | 127 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
129 | import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video" | 128 | import { VideoTagModel } from './video-tag' |
129 | import { VideoViewModel } from './video-view' | ||
130 | 130 | ||
131 | export enum ScopeNames { | 131 | export enum ScopeNames { |
132 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | 132 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', |
@@ -803,14 +803,14 @@ export class VideoModel extends Model<VideoModel> { | |||
803 | static async saveEssentialDataToAbuses (instance: VideoModel, options) { | 803 | static async saveEssentialDataToAbuses (instance: VideoModel, options) { |
804 | const tasks: Promise<any>[] = [] | 804 | const tasks: Promise<any>[] = [] |
805 | 805 | ||
806 | logger.info('Saving video abuses details of video %s.', instance.url) | ||
807 | |||
808 | if (!Array.isArray(instance.VideoAbuses)) { | 806 | if (!Array.isArray(instance.VideoAbuses)) { |
809 | instance.VideoAbuses = await instance.$get('VideoAbuses') | 807 | instance.VideoAbuses = await instance.$get('VideoAbuses') |
810 | 808 | ||
811 | if (instance.VideoAbuses.length === 0) return undefined | 809 | if (instance.VideoAbuses.length === 0) return undefined |
812 | } | 810 | } |
813 | 811 | ||
812 | logger.info('Saving video abuses details of video %s.', instance.url) | ||
813 | |||
814 | const details = instance.toFormattedDetailsJSON() | 814 | const details = instance.toFormattedDetailsJSON() |
815 | 815 | ||
816 | for (const abuse of instance.VideoAbuses) { | 816 | for (const abuse of instance.VideoAbuses) { |
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts new file mode 100644 index 000000000..8964c0ab2 --- /dev/null +++ b/server/tests/api/check-params/abuses.ts | |||
@@ -0,0 +1,269 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { AbuseCreate, AbuseState } from '@shared/models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createUser, | ||
8 | deleteAbuse, | ||
9 | flushAndRunServer, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | updateAbuse, | ||
15 | uploadVideo, | ||
16 | userLogin | ||
17 | } from '../../../../shared/extra-utils' | ||
18 | import { | ||
19 | checkBadCountPagination, | ||
20 | checkBadSortPagination, | ||
21 | checkBadStartPagination | ||
22 | } from '../../../../shared/extra-utils/requests/check-api-params' | ||
23 | |||
24 | describe('Test abuses API validators', function () { | ||
25 | const basePath = '/api/v1/abuses/' | ||
26 | |||
27 | let server: ServerInfo | ||
28 | let userAccessToken = '' | ||
29 | let abuseId: number | ||
30 | |||
31 | // --------------------------------------------------------------- | ||
32 | |||
33 | before(async function () { | ||
34 | this.timeout(30000) | ||
35 | |||
36 | server = await flushAndRunServer(1) | ||
37 | |||
38 | await setAccessTokensToServers([ server ]) | ||
39 | |||
40 | const username = 'user1' | ||
41 | const password = 'my super password' | ||
42 | await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password }) | ||
43 | userAccessToken = await userLogin(server, { username, password }) | ||
44 | |||
45 | const res = await uploadVideo(server.url, server.accessToken, {}) | ||
46 | server.video = res.body.video | ||
47 | }) | ||
48 | |||
49 | describe('When listing abuses', function () { | ||
50 | const path = basePath | ||
51 | |||
52 | it('Should fail with a bad start pagination', async function () { | ||
53 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
54 | }) | ||
55 | |||
56 | it('Should fail with a bad count pagination', async function () { | ||
57 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
58 | }) | ||
59 | |||
60 | it('Should fail with an incorrect sort', async function () { | ||
61 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
62 | }) | ||
63 | |||
64 | it('Should fail with a non authenticated user', async function () { | ||
65 | await makeGetRequest({ | ||
66 | url: server.url, | ||
67 | path, | ||
68 | statusCodeExpected: 401 | ||
69 | }) | ||
70 | }) | ||
71 | |||
72 | it('Should fail with a non admin user', async function () { | ||
73 | await makeGetRequest({ | ||
74 | url: server.url, | ||
75 | path, | ||
76 | token: userAccessToken, | ||
77 | statusCodeExpected: 403 | ||
78 | }) | ||
79 | }) | ||
80 | |||
81 | it('Should fail with a bad id filter', async function () { | ||
82 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } }) | ||
83 | }) | ||
84 | |||
85 | it('Should fail with a bad filter', async function () { | ||
86 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } }) | ||
87 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } }) | ||
88 | }) | ||
89 | |||
90 | it('Should fail with bad predefined reason', async function () { | ||
91 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } }) | ||
92 | }) | ||
93 | |||
94 | it('Should fail with a bad state filter', async function () { | ||
95 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } }) | ||
96 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } }) | ||
97 | }) | ||
98 | |||
99 | it('Should fail with a bad videoIs filter', async function () { | ||
100 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } }) | ||
101 | }) | ||
102 | |||
103 | it('Should succeed with the correct params', async function () { | ||
104 | const query = { | ||
105 | id: 13, | ||
106 | predefinedReason: 'violentOrRepulsive', | ||
107 | filter: 'comment', | ||
108 | state: 2, | ||
109 | videoIs: 'deleted' | ||
110 | } | ||
111 | |||
112 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query, statusCodeExpected: 200 }) | ||
113 | }) | ||
114 | }) | ||
115 | |||
116 | describe('When reporting an abuse', function () { | ||
117 | const path = basePath | ||
118 | |||
119 | it('Should fail with nothing', async function () { | ||
120 | const fields = {} | ||
121 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with a wrong video', async function () { | ||
125 | const fields = { video: { id: 'blabla' }, reason: 'my super reason' } | ||
126 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields }) | ||
127 | }) | ||
128 | |||
129 | it('Should fail with an unknown video', async function () { | ||
130 | const fields = { video: { id: 42 }, reason: 'my super reason' } | ||
131 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 }) | ||
132 | }) | ||
133 | |||
134 | it('Should fail with a wrong comment', async function () { | ||
135 | const fields = { comment: { id: 'blabla' }, reason: 'my super reason' } | ||
136 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields }) | ||
137 | }) | ||
138 | |||
139 | it('Should fail with an unknown comment', async function () { | ||
140 | const fields = { comment: { id: 42 }, reason: 'my super reason' } | ||
141 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 }) | ||
142 | }) | ||
143 | |||
144 | it('Should fail with a wrong account', async function () { | ||
145 | const fields = { account: { id: 'blabla' }, reason: 'my super reason' } | ||
146 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields }) | ||
147 | }) | ||
148 | |||
149 | it('Should fail with an unknown account', async function () { | ||
150 | const fields = { account: { id: 42 }, reason: 'my super reason' } | ||
151 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 }) | ||
152 | }) | ||
153 | |||
154 | it('Should fail with not account, comment or video', async function () { | ||
155 | const fields = { reason: 'my super reason' } | ||
156 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 400 }) | ||
157 | }) | ||
158 | |||
159 | it('Should fail with a non authenticated user', async function () { | ||
160 | const fields = { video: { id: server.video.id }, reason: 'my super reason' } | ||
161 | |||
162 | await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) | ||
163 | }) | ||
164 | |||
165 | it('Should fail with a reason too short', async function () { | ||
166 | const fields = { video: { id: server.video.id }, reason: 'h' } | ||
167 | |||
168 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
169 | }) | ||
170 | |||
171 | it('Should fail with a too big reason', async function () { | ||
172 | const fields = { video: { id: server.video.id }, reason: 'super'.repeat(605) } | ||
173 | |||
174 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
175 | }) | ||
176 | |||
177 | it('Should succeed with the correct parameters (basic)', async function () { | ||
178 | const fields: AbuseCreate = { video: { id: server.video.id }, reason: 'my super reason' } | ||
179 | |||
180 | const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) | ||
181 | abuseId = res.body.abuse.id | ||
182 | }) | ||
183 | |||
184 | it('Should fail with a wrong predefined reason', async function () { | ||
185 | const fields = { video: { id: server.video.id }, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] } | ||
186 | |||
187 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
188 | }) | ||
189 | |||
190 | it('Should fail with negative timestamps', async function () { | ||
191 | const fields = { video: { id: server.video.id, startAt: -1 }, reason: 'my super reason' } | ||
192 | |||
193 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
194 | }) | ||
195 | |||
196 | it('Should fail mith misordered startAt/endAt', async function () { | ||
197 | const fields = { video: { id: server.video.id, startAt: 5, endAt: 1 }, reason: 'my super reason' } | ||
198 | |||
199 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
200 | }) | ||
201 | |||
202 | it('Should succeed with the corret parameters (advanced)', async function () { | ||
203 | const fields: AbuseCreate = { | ||
204 | video: { | ||
205 | id: server.video.id, | ||
206 | startAt: 1, | ||
207 | endAt: 5 | ||
208 | }, | ||
209 | reason: 'my super reason', | ||
210 | predefinedReasons: [ 'serverRules' ] | ||
211 | } | ||
212 | |||
213 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) | ||
214 | }) | ||
215 | }) | ||
216 | |||
217 | describe('When updating an abuse', function () { | ||
218 | |||
219 | it('Should fail with a non authenticated user', async function () { | ||
220 | await updateAbuse(server.url, 'blabla', abuseId, {}, 401) | ||
221 | }) | ||
222 | |||
223 | it('Should fail with a non admin user', async function () { | ||
224 | await updateAbuse(server.url, userAccessToken, abuseId, {}, 403) | ||
225 | }) | ||
226 | |||
227 | it('Should fail with a bad abuse id', async function () { | ||
228 | await updateAbuse(server.url, server.accessToken, 45, {}, 404) | ||
229 | }) | ||
230 | |||
231 | it('Should fail with a bad state', async function () { | ||
232 | const body = { state: 5 } | ||
233 | await updateAbuse(server.url, server.accessToken, abuseId, body, 400) | ||
234 | }) | ||
235 | |||
236 | it('Should fail with a bad moderation comment', async function () { | ||
237 | const body = { moderationComment: 'b'.repeat(3001) } | ||
238 | await updateAbuse(server.url, server.accessToken, abuseId, body, 400) | ||
239 | }) | ||
240 | |||
241 | it('Should succeed with the correct params', async function () { | ||
242 | const body = { state: AbuseState.ACCEPTED } | ||
243 | await updateAbuse(server.url, server.accessToken, abuseId, body) | ||
244 | }) | ||
245 | }) | ||
246 | |||
247 | describe('When deleting a video abuse', function () { | ||
248 | |||
249 | it('Should fail with a non authenticated user', async function () { | ||
250 | await deleteAbuse(server.url, 'blabla', abuseId, 401) | ||
251 | }) | ||
252 | |||
253 | it('Should fail with a non admin user', async function () { | ||
254 | await deleteAbuse(server.url, userAccessToken, abuseId, 403) | ||
255 | }) | ||
256 | |||
257 | it('Should fail with a bad abuse id', async function () { | ||
258 | await deleteAbuse(server.url, server.accessToken, 45, 404) | ||
259 | }) | ||
260 | |||
261 | it('Should succeed with the correct params', async function () { | ||
262 | await deleteAbuse(server.url, server.accessToken, abuseId) | ||
263 | }) | ||
264 | }) | ||
265 | |||
266 | after(async function () { | ||
267 | await cleanupTests([ server ]) | ||
268 | }) | ||
269 | }) | ||
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 93ffd98b1..0ee1f27aa 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import './abuses' | ||
1 | import './accounts' | 2 | import './accounts' |
2 | import './blocklist' | 3 | import './blocklist' |
3 | import './bulk' | 4 | import './bulk' |
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts index 2048fa667..883b1d29c 100644 --- a/server/tests/api/check-params/user-notifications.ts +++ b/server/tests/api/check-params/user-notifications.ts | |||
@@ -164,7 +164,7 @@ describe('Test user notifications API validators', function () { | |||
164 | const correctFields: UserNotificationSetting = { | 164 | const correctFields: UserNotificationSetting = { |
165 | newVideoFromSubscription: UserNotificationSettingValue.WEB, | 165 | newVideoFromSubscription: UserNotificationSettingValue.WEB, |
166 | newCommentOnMyVideo: UserNotificationSettingValue.WEB, | 166 | newCommentOnMyVideo: UserNotificationSettingValue.WEB, |
167 | videoAbuseAsModerator: UserNotificationSettingValue.WEB, | 167 | abuseAsModerator: UserNotificationSettingValue.WEB, |
168 | videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB, | 168 | videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB, |
169 | blacklistOnMyVideo: UserNotificationSettingValue.WEB, | 169 | blacklistOnMyVideo: UserNotificationSettingValue.WEB, |
170 | myVideoImportFinished: UserNotificationSettingValue.WEB, | 170 | myVideoImportFinished: UserNotificationSettingValue.WEB, |
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts index 557bf20eb..3b361ca79 100644 --- a/server/tests/api/check-params/video-abuses.ts +++ b/server/tests/api/check-params/video-abuses.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | import { AbuseState, VideoAbuseCreate } from '@shared/models' | |
5 | import { | 5 | import { |
6 | cleanupTests, | 6 | cleanupTests, |
7 | createUser, | 7 | createUser, |
@@ -20,7 +20,8 @@ import { | |||
20 | checkBadSortPagination, | 20 | checkBadSortPagination, |
21 | checkBadStartPagination | 21 | checkBadStartPagination |
22 | } from '../../../../shared/extra-utils/requests/check-api-params' | 22 | } from '../../../../shared/extra-utils/requests/check-api-params' |
23 | import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos' | 23 | |
24 | // FIXME: deprecated in 2.3. Remove this controller | ||
24 | 25 | ||
25 | describe('Test video abuses API validators', function () { | 26 | describe('Test video abuses API validators', function () { |
26 | let server: ServerInfo | 27 | let server: ServerInfo |
@@ -136,7 +137,7 @@ describe('Test video abuses API validators', function () { | |||
136 | const fields = { reason: 'my super reason' } | 137 | const fields = { reason: 'my super reason' } |
137 | 138 | ||
138 | const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) | 139 | const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) |
139 | videoAbuseId = res.body.videoAbuse.id | 140 | videoAbuseId = res.body.abuse.id |
140 | }) | 141 | }) |
141 | 142 | ||
142 | it('Should fail with a wrong predefined reason', async function () { | 143 | it('Should fail with a wrong predefined reason', async function () { |
@@ -151,12 +152,6 @@ describe('Test video abuses API validators', function () { | |||
151 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 152 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
152 | }) | 153 | }) |
153 | 154 | ||
154 | it('Should fail mith misordered startAt/endAt', async function () { | ||
155 | const fields = { reason: 'my super reason', startAt: 5, endAt: 1 } | ||
156 | |||
157 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
158 | }) | ||
159 | |||
160 | it('Should succeed with the corret parameters (advanced)', async function () { | 155 | it('Should succeed with the corret parameters (advanced)', async function () { |
161 | const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 } | 156 | const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 } |
162 | 157 | ||
@@ -190,7 +185,7 @@ describe('Test video abuses API validators', function () { | |||
190 | }) | 185 | }) |
191 | 186 | ||
192 | it('Should succeed with the correct params', async function () { | 187 | it('Should succeed with the correct params', async function () { |
193 | const body = { state: VideoAbuseState.ACCEPTED } | 188 | const body = { state: AbuseState.ACCEPTED } |
194 | await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body) | 189 | await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body) |
195 | }) | 190 | }) |
196 | }) | 191 | }) |
diff --git a/server/tests/api/ci-4.sh b/server/tests/api/ci-4.sh index 14a014f07..4998de364 100644 --- a/server/tests/api/ci-4.sh +++ b/server/tests/api/ci-4.sh | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | set -eu | 3 | set -eu |
4 | 4 | ||
5 | activitypubFiles=$(find server/tests/api/moderation -type f | grep -v index.ts | xargs echo) | ||
5 | redundancyFiles=$(find server/tests/api/redundancy -type f | grep -v index.ts | xargs echo) | 6 | redundancyFiles=$(find server/tests/api/redundancy -type f | grep -v index.ts | xargs echo) |
6 | activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo) | 7 | activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo) |
7 | 8 | ||
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts index bac77ab2e..b62e2f5f7 100644 --- a/server/tests/api/index.ts +++ b/server/tests/api/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | // Order of the tests we want to execute | 1 | // Order of the tests we want to execute |
2 | import './activitypub' | 2 | import './activitypub' |
3 | import './check-params' | 3 | import './check-params' |
4 | import './moderation' | ||
4 | import './notifications' | 5 | import './notifications' |
5 | import './redundancy' | 6 | import './redundancy' |
6 | import './search' | 7 | import './search' |
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts new file mode 100644 index 000000000..f186f7ea0 --- /dev/null +++ b/server/tests/api/moderation/abuses.ts | |||
@@ -0,0 +1,777 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { Abuse, AbuseFilter, AbusePredefinedReasonsString, AbuseState, VideoComment, Account } from '@shared/models' | ||
6 | import { | ||
7 | addVideoCommentThread, | ||
8 | cleanupTests, | ||
9 | createUser, | ||
10 | deleteAbuse, | ||
11 | deleteVideoComment, | ||
12 | flushAndRunMultipleServers, | ||
13 | getAbusesList, | ||
14 | getVideoCommentThreads, | ||
15 | getVideoIdFromUUID, | ||
16 | getVideosList, | ||
17 | immutableAssign, | ||
18 | removeVideo, | ||
19 | reportAbuse, | ||
20 | ServerInfo, | ||
21 | setAccessTokensToServers, | ||
22 | updateAbuse, | ||
23 | uploadVideo, | ||
24 | uploadVideoAndGetId, | ||
25 | userLogin, | ||
26 | getAccount, | ||
27 | removeUser, | ||
28 | generateUserAccessToken | ||
29 | } from '../../../../shared/extra-utils/index' | ||
30 | import { doubleFollow } from '../../../../shared/extra-utils/server/follows' | ||
31 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
32 | import { | ||
33 | addAccountToServerBlocklist, | ||
34 | addServerToServerBlocklist, | ||
35 | removeAccountFromServerBlocklist, | ||
36 | removeServerFromServerBlocklist | ||
37 | } from '../../../../shared/extra-utils/users/blocklist' | ||
38 | |||
39 | const expect = chai.expect | ||
40 | |||
41 | describe('Test abuses', function () { | ||
42 | let servers: ServerInfo[] = [] | ||
43 | let abuseServer1: Abuse | ||
44 | let abuseServer2: Abuse | ||
45 | |||
46 | before(async function () { | ||
47 | this.timeout(50000) | ||
48 | |||
49 | // Run servers | ||
50 | servers = await flushAndRunMultipleServers(2) | ||
51 | |||
52 | // Get the access tokens | ||
53 | await setAccessTokensToServers(servers) | ||
54 | |||
55 | // Server 1 and server 2 follow each other | ||
56 | await doubleFollow(servers[0], servers[1]) | ||
57 | }) | ||
58 | |||
59 | describe('Video abuses', function () { | ||
60 | |||
61 | before(async function () { | ||
62 | this.timeout(50000) | ||
63 | |||
64 | // Upload some videos on each servers | ||
65 | const video1Attributes = { | ||
66 | name: 'my super name for server 1', | ||
67 | description: 'my super description for server 1' | ||
68 | } | ||
69 | await uploadVideo(servers[0].url, servers[0].accessToken, video1Attributes) | ||
70 | |||
71 | const video2Attributes = { | ||
72 | name: 'my super name for server 2', | ||
73 | description: 'my super description for server 2' | ||
74 | } | ||
75 | await uploadVideo(servers[1].url, servers[1].accessToken, video2Attributes) | ||
76 | |||
77 | // Wait videos propagation, server 2 has transcoding enabled | ||
78 | await waitJobs(servers) | ||
79 | |||
80 | const res = await getVideosList(servers[0].url) | ||
81 | const videos = res.body.data | ||
82 | |||
83 | expect(videos.length).to.equal(2) | ||
84 | |||
85 | servers[0].video = videos.find(video => video.name === 'my super name for server 1') | ||
86 | servers[1].video = videos.find(video => video.name === 'my super name for server 2') | ||
87 | }) | ||
88 | |||
89 | it('Should not have abuses', async function () { | ||
90 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
91 | |||
92 | expect(res.body.total).to.equal(0) | ||
93 | expect(res.body.data).to.be.an('array') | ||
94 | expect(res.body.data.length).to.equal(0) | ||
95 | }) | ||
96 | |||
97 | it('Should report abuse on a local video', async function () { | ||
98 | this.timeout(15000) | ||
99 | |||
100 | const reason = 'my super bad reason' | ||
101 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[0].video.id, reason }) | ||
102 | |||
103 | // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2 | ||
104 | await waitJobs(servers) | ||
105 | }) | ||
106 | |||
107 | it('Should have 1 video abuses on server 1 and 0 on server 2', async function () { | ||
108 | const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
109 | |||
110 | expect(res1.body.total).to.equal(1) | ||
111 | expect(res1.body.data).to.be.an('array') | ||
112 | expect(res1.body.data.length).to.equal(1) | ||
113 | |||
114 | const abuse: Abuse = res1.body.data[0] | ||
115 | expect(abuse.reason).to.equal('my super bad reason') | ||
116 | |||
117 | expect(abuse.reporterAccount.name).to.equal('root') | ||
118 | expect(abuse.reporterAccount.host).to.equal(servers[0].host) | ||
119 | |||
120 | expect(abuse.video.id).to.equal(servers[0].video.id) | ||
121 | expect(abuse.video.channel).to.exist | ||
122 | |||
123 | expect(abuse.comment).to.be.null | ||
124 | |||
125 | expect(abuse.flaggedAccount.name).to.equal('root') | ||
126 | expect(abuse.flaggedAccount.host).to.equal(servers[0].host) | ||
127 | |||
128 | expect(abuse.video.countReports).to.equal(1) | ||
129 | expect(abuse.video.nthReport).to.equal(1) | ||
130 | |||
131 | expect(abuse.countReportsForReporter).to.equal(1) | ||
132 | expect(abuse.countReportsForReportee).to.equal(1) | ||
133 | |||
134 | const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken }) | ||
135 | expect(res2.body.total).to.equal(0) | ||
136 | expect(res2.body.data).to.be.an('array') | ||
137 | expect(res2.body.data.length).to.equal(0) | ||
138 | }) | ||
139 | |||
140 | it('Should report abuse on a remote video', async function () { | ||
141 | this.timeout(10000) | ||
142 | |||
143 | const reason = 'my super bad reason 2' | ||
144 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[1].video.id, reason }) | ||
145 | |||
146 | // We wait requests propagation | ||
147 | await waitJobs(servers) | ||
148 | }) | ||
149 | |||
150 | it('Should have 2 video abuses on server 1 and 1 on server 2', async function () { | ||
151 | const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
152 | |||
153 | expect(res1.body.total).to.equal(2) | ||
154 | expect(res1.body.data.length).to.equal(2) | ||
155 | |||
156 | const abuse1: Abuse = res1.body.data[0] | ||
157 | expect(abuse1.reason).to.equal('my super bad reason') | ||
158 | expect(abuse1.reporterAccount.name).to.equal('root') | ||
159 | expect(abuse1.reporterAccount.host).to.equal(servers[0].host) | ||
160 | |||
161 | expect(abuse1.video.id).to.equal(servers[0].video.id) | ||
162 | expect(abuse1.video.countReports).to.equal(1) | ||
163 | expect(abuse1.video.nthReport).to.equal(1) | ||
164 | |||
165 | expect(abuse1.comment).to.be.null | ||
166 | |||
167 | expect(abuse1.flaggedAccount.name).to.equal('root') | ||
168 | expect(abuse1.flaggedAccount.host).to.equal(servers[0].host) | ||
169 | |||
170 | expect(abuse1.state.id).to.equal(AbuseState.PENDING) | ||
171 | expect(abuse1.state.label).to.equal('Pending') | ||
172 | expect(abuse1.moderationComment).to.be.null | ||
173 | |||
174 | const abuse2: Abuse = res1.body.data[1] | ||
175 | expect(abuse2.reason).to.equal('my super bad reason 2') | ||
176 | |||
177 | expect(abuse2.reporterAccount.name).to.equal('root') | ||
178 | expect(abuse2.reporterAccount.host).to.equal(servers[0].host) | ||
179 | |||
180 | expect(abuse2.video.id).to.equal(servers[1].video.id) | ||
181 | |||
182 | expect(abuse2.comment).to.be.null | ||
183 | |||
184 | expect(abuse2.flaggedAccount.name).to.equal('root') | ||
185 | expect(abuse2.flaggedAccount.host).to.equal(servers[1].host) | ||
186 | |||
187 | expect(abuse2.state.id).to.equal(AbuseState.PENDING) | ||
188 | expect(abuse2.state.label).to.equal('Pending') | ||
189 | expect(abuse2.moderationComment).to.be.null | ||
190 | |||
191 | const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken }) | ||
192 | expect(res2.body.total).to.equal(1) | ||
193 | expect(res2.body.data.length).to.equal(1) | ||
194 | |||
195 | abuseServer2 = res2.body.data[0] | ||
196 | expect(abuseServer2.reason).to.equal('my super bad reason 2') | ||
197 | expect(abuseServer2.reporterAccount.name).to.equal('root') | ||
198 | expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) | ||
199 | |||
200 | expect(abuse2.flaggedAccount.name).to.equal('root') | ||
201 | expect(abuse2.flaggedAccount.host).to.equal(servers[1].host) | ||
202 | |||
203 | expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) | ||
204 | expect(abuseServer2.state.label).to.equal('Pending') | ||
205 | expect(abuseServer2.moderationComment).to.be.null | ||
206 | }) | ||
207 | |||
208 | it('Should hide video abuses from blocked accounts', async function () { | ||
209 | this.timeout(10000) | ||
210 | |||
211 | { | ||
212 | const videoId = await getVideoIdFromUUID(servers[1].url, servers[0].video.uuid) | ||
213 | await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'will mute this' }) | ||
214 | await waitJobs(servers) | ||
215 | |||
216 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
217 | expect(res.body.total).to.equal(3) | ||
218 | } | ||
219 | |||
220 | const accountToBlock = 'root@' + servers[1].host | ||
221 | |||
222 | { | ||
223 | await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) | ||
224 | |||
225 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
226 | expect(res.body.total).to.equal(2) | ||
227 | |||
228 | const abuse = res.body.data.find(a => a.reason === 'will mute this') | ||
229 | expect(abuse).to.be.undefined | ||
230 | } | ||
231 | |||
232 | { | ||
233 | await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) | ||
234 | |||
235 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
236 | expect(res.body.total).to.equal(3) | ||
237 | } | ||
238 | }) | ||
239 | |||
240 | it('Should hide video abuses from blocked servers', async function () { | ||
241 | const serverToBlock = servers[1].host | ||
242 | |||
243 | { | ||
244 | await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host) | ||
245 | |||
246 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
247 | expect(res.body.total).to.equal(2) | ||
248 | |||
249 | const abuse = res.body.data.find(a => a.reason === 'will mute this') | ||
250 | expect(abuse).to.be.undefined | ||
251 | } | ||
252 | |||
253 | { | ||
254 | await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock) | ||
255 | |||
256 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
257 | expect(res.body.total).to.equal(3) | ||
258 | } | ||
259 | }) | ||
260 | |||
261 | it('Should keep the video abuse when deleting the video', async function () { | ||
262 | this.timeout(10000) | ||
263 | |||
264 | await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid) | ||
265 | |||
266 | await waitJobs(servers) | ||
267 | |||
268 | const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken }) | ||
269 | expect(res.body.total).to.equal(2, "wrong number of videos returned") | ||
270 | expect(res.body.data).to.have.lengthOf(2, "wrong number of videos returned") | ||
271 | |||
272 | const abuse: Abuse = res.body.data[0] | ||
273 | expect(abuse.id).to.equal(abuseServer2.id, "wrong origin server id for first video") | ||
274 | expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id") | ||
275 | expect(abuse.video.channel).to.exist | ||
276 | expect(abuse.video.deleted).to.be.true | ||
277 | }) | ||
278 | |||
279 | it('Should include counts of reports from reporter and reportee', async function () { | ||
280 | this.timeout(10000) | ||
281 | |||
282 | // register a second user to have two reporters/reportees | ||
283 | const user = { username: 'user2', password: 'password' } | ||
284 | await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user }) | ||
285 | const userAccessToken = await userLogin(servers[0], user) | ||
286 | |||
287 | // upload a third video via this user | ||
288 | const video3Attributes = { | ||
289 | name: 'my second super name for server 1', | ||
290 | description: 'my second super description for server 1' | ||
291 | } | ||
292 | await uploadVideo(servers[0].url, userAccessToken, video3Attributes) | ||
293 | |||
294 | const res1 = await getVideosList(servers[0].url) | ||
295 | const videos = res1.body.data | ||
296 | const video3 = videos.find(video => video.name === 'my second super name for server 1') | ||
297 | |||
298 | // resume with the test | ||
299 | const reason3 = 'my super bad reason 3' | ||
300 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video3.id, reason: reason3 }) | ||
301 | |||
302 | const reason4 = 'my super bad reason 4' | ||
303 | await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: servers[0].video.id, reason: reason4 }) | ||
304 | |||
305 | { | ||
306 | const res2 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
307 | const abuses = res2.body.data as Abuse[] | ||
308 | |||
309 | const abuseVideo3 = res2.body.data.find(a => a.video.id === video3.id) | ||
310 | expect(abuseVideo3).to.not.be.undefined | ||
311 | expect(abuseVideo3.video.countReports).to.equal(1, "wrong reports count for video 3") | ||
312 | expect(abuseVideo3.video.nthReport).to.equal(1, "wrong report position in report list for video 3") | ||
313 | expect(abuseVideo3.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse") | ||
314 | expect(abuseVideo3.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse") | ||
315 | |||
316 | const abuseServer1 = abuses.find(a => a.video.id === servers[0].video.id) | ||
317 | expect(abuseServer1.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse") | ||
318 | } | ||
319 | }) | ||
320 | |||
321 | it('Should list predefined reasons as well as timestamps for the reported video', async function () { | ||
322 | this.timeout(10000) | ||
323 | |||
324 | const reason5 = 'my super bad reason 5' | ||
325 | const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] | ||
326 | const createdAbuse = (await reportAbuse({ | ||
327 | url: servers[0].url, | ||
328 | token: servers[0].accessToken, | ||
329 | videoId: servers[0].video.id, | ||
330 | reason: reason5, | ||
331 | predefinedReasons: predefinedReasons5, | ||
332 | startAt: 1, | ||
333 | endAt: 5 | ||
334 | })).body.abuse | ||
335 | |||
336 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
337 | |||
338 | { | ||
339 | const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id) | ||
340 | expect(abuse.reason).to.equals(reason5) | ||
341 | expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported") | ||
342 | expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported") | ||
343 | expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported") | ||
344 | } | ||
345 | }) | ||
346 | |||
347 | it('Should delete the video abuse', async function () { | ||
348 | this.timeout(10000) | ||
349 | |||
350 | await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id) | ||
351 | |||
352 | await waitJobs(servers) | ||
353 | |||
354 | { | ||
355 | const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken }) | ||
356 | expect(res.body.total).to.equal(1) | ||
357 | expect(res.body.data.length).to.equal(1) | ||
358 | expect(res.body.data[0].id).to.not.equal(abuseServer2.id) | ||
359 | } | ||
360 | |||
361 | { | ||
362 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | ||
363 | expect(res.body.total).to.equal(6) | ||
364 | } | ||
365 | }) | ||
366 | |||
367 | it('Should list and filter video abuses', async function () { | ||
368 | this.timeout(10000) | ||
369 | |||
370 | async function list (query: Omit<Parameters<typeof getAbusesList>[0], 'url' | 'token'>) { | ||
371 | const options = { | ||
372 | url: servers[0].url, | ||
373 | token: servers[0].accessToken | ||
374 | } | ||
375 | |||
376 | Object.assign(options, query) | ||
377 | |||
378 | const res = await getAbusesList(options) | ||
379 | |||
380 | return res.body.data as Abuse[] | ||
381 | } | ||
382 | |||
383 | expect(await list({ id: 56 })).to.have.lengthOf(0) | ||
384 | expect(await list({ id: 1 })).to.have.lengthOf(1) | ||
385 | |||
386 | expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4) | ||
387 | expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0) | ||
388 | |||
389 | expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1) | ||
390 | |||
391 | expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4) | ||
392 | expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0) | ||
393 | |||
394 | expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) | ||
395 | expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5) | ||
396 | |||
397 | expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5) | ||
398 | expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) | ||
399 | |||
400 | expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) | ||
401 | expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) | ||
402 | |||
403 | expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0) | ||
404 | expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6) | ||
405 | |||
406 | expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) | ||
407 | expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0) | ||
408 | }) | ||
409 | }) | ||
410 | |||
411 | describe('Comment abuses', function () { | ||
412 | |||
413 | async function getComment (url: string, videoIdArg: number | string) { | ||
414 | const videoId = typeof videoIdArg === 'string' | ||
415 | ? await getVideoIdFromUUID(url, videoIdArg) | ||
416 | : videoIdArg | ||
417 | |||
418 | const res = await getVideoCommentThreads(url, videoId, 0, 5) | ||
419 | |||
420 | return res.body.data[0] as VideoComment | ||
421 | } | ||
422 | |||
423 | before(async function () { | ||
424 | this.timeout(50000) | ||
425 | |||
426 | servers[0].video = await uploadVideoAndGetId({ server: servers[0], videoName: 'server 1' }) | ||
427 | servers[1].video = await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' }) | ||
428 | |||
429 | await addVideoCommentThread(servers[0].url, servers[0].accessToken, servers[0].video.id, 'comment server 1') | ||
430 | await addVideoCommentThread(servers[1].url, servers[1].accessToken, servers[1].video.id, 'comment server 2') | ||
431 | |||
432 | await waitJobs(servers) | ||
433 | }) | ||
434 | |||
435 | it('Should report abuse on a comment', async function () { | ||
436 | this.timeout(15000) | ||
437 | |||
438 | const comment = await getComment(servers[0].url, servers[0].video.id) | ||
439 | |||
440 | const reason = 'it is a bad comment' | ||
441 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason }) | ||
442 | |||
443 | await waitJobs(servers) | ||
444 | }) | ||
445 | |||
446 | it('Should have 1 comment abuse on server 1 and 0 on server 2', async function () { | ||
447 | { | ||
448 | const comment = await getComment(servers[0].url, servers[0].video.id) | ||
449 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' }) | ||
450 | |||
451 | expect(res.body.total).to.equal(1) | ||
452 | expect(res.body.data).to.have.lengthOf(1) | ||
453 | |||
454 | const abuse: Abuse = res.body.data[0] | ||
455 | expect(abuse.reason).to.equal('it is a bad comment') | ||
456 | |||
457 | expect(abuse.reporterAccount.name).to.equal('root') | ||
458 | expect(abuse.reporterAccount.host).to.equal(servers[0].host) | ||
459 | |||
460 | expect(abuse.video).to.be.null | ||
461 | |||
462 | expect(abuse.comment.deleted).to.be.false | ||
463 | expect(abuse.comment.id).to.equal(comment.id) | ||
464 | expect(abuse.comment.text).to.equal(comment.text) | ||
465 | expect(abuse.comment.video.name).to.equal('server 1') | ||
466 | expect(abuse.comment.video.id).to.equal(servers[0].video.id) | ||
467 | expect(abuse.comment.video.uuid).to.equal(servers[0].video.uuid) | ||
468 | |||
469 | expect(abuse.countReportsForReporter).to.equal(5) | ||
470 | expect(abuse.countReportsForReportee).to.equal(5) | ||
471 | } | ||
472 | |||
473 | { | ||
474 | const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' }) | ||
475 | expect(res.body.total).to.equal(0) | ||
476 | expect(res.body.data.length).to.equal(0) | ||
477 | } | ||
478 | }) | ||
479 | |||
480 | it('Should report abuse on a remote comment', async function () { | ||
481 | this.timeout(10000) | ||
482 | |||
483 | const comment = await getComment(servers[0].url, servers[1].video.uuid) | ||
484 | |||
485 | const reason = 'it is a really bad comment' | ||
486 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason }) | ||
487 | |||
488 | await waitJobs(servers) | ||
489 | }) | ||
490 | |||
491 | it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () { | ||
492 | const commentServer2 = await getComment(servers[0].url, servers[1].video.id) | ||
493 | |||
494 | const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' }) | ||
495 | expect(res1.body.total).to.equal(2) | ||
496 | expect(res1.body.data.length).to.equal(2) | ||
497 | |||
498 | const abuse: Abuse = res1.body.data[0] | ||
499 | expect(abuse.reason).to.equal('it is a bad comment') | ||
500 | expect(abuse.countReportsForReporter).to.equal(6) | ||
501 | expect(abuse.countReportsForReportee).to.equal(5) | ||
502 | |||
503 | const abuse2: Abuse = res1.body.data[1] | ||
504 | |||
505 | expect(abuse2.reason).to.equal('it is a really bad comment') | ||
506 | |||
507 | expect(abuse2.reporterAccount.name).to.equal('root') | ||
508 | expect(abuse2.reporterAccount.host).to.equal(servers[0].host) | ||
509 | |||
510 | expect(abuse2.video).to.be.null | ||
511 | |||
512 | expect(abuse2.comment.deleted).to.be.false | ||
513 | expect(abuse2.comment.id).to.equal(commentServer2.id) | ||
514 | expect(abuse2.comment.text).to.equal(commentServer2.text) | ||
515 | expect(abuse2.comment.video.name).to.equal('server 2') | ||
516 | expect(abuse2.comment.video.uuid).to.equal(servers[1].video.uuid) | ||
517 | |||
518 | expect(abuse2.state.id).to.equal(AbuseState.PENDING) | ||
519 | expect(abuse2.state.label).to.equal('Pending') | ||
520 | |||
521 | expect(abuse2.moderationComment).to.be.null | ||
522 | |||
523 | expect(abuse2.countReportsForReporter).to.equal(6) | ||
524 | expect(abuse2.countReportsForReportee).to.equal(2) | ||
525 | |||
526 | const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' }) | ||
527 | expect(res2.body.total).to.equal(1) | ||
528 | expect(res2.body.data.length).to.equal(1) | ||
529 | |||
530 | abuseServer2 = res2.body.data[0] | ||
531 | expect(abuseServer2.reason).to.equal('it is a really bad comment') | ||
532 | expect(abuseServer2.reporterAccount.name).to.equal('root') | ||
533 | expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) | ||
534 | |||
535 | expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) | ||
536 | expect(abuseServer2.state.label).to.equal('Pending') | ||
537 | |||
538 | expect(abuseServer2.moderationComment).to.be.null | ||
539 | |||
540 | expect(abuseServer2.countReportsForReporter).to.equal(1) | ||
541 | expect(abuseServer2.countReportsForReportee).to.equal(1) | ||
542 | }) | ||
543 | |||
544 | it('Should keep the comment abuse when deleting the comment', async function () { | ||
545 | this.timeout(10000) | ||
546 | |||
547 | const commentServer2 = await getComment(servers[0].url, servers[1].video.id) | ||
548 | |||
549 | await deleteVideoComment(servers[0].url, servers[0].accessToken, servers[1].video.uuid, commentServer2.id) | ||
550 | |||
551 | await waitJobs(servers) | ||
552 | |||
553 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' }) | ||
554 | expect(res.body.total).to.equal(2) | ||
555 | expect(res.body.data).to.have.lengthOf(2) | ||
556 | |||
557 | const abuse = (res.body.data as Abuse[]).find(a => a.comment?.id === commentServer2.id) | ||
558 | expect(abuse).to.not.be.undefined | ||
559 | |||
560 | expect(abuse.comment.text).to.be.empty | ||
561 | expect(abuse.comment.video.name).to.equal('server 2') | ||
562 | expect(abuse.comment.deleted).to.be.true | ||
563 | }) | ||
564 | |||
565 | it('Should delete the comment abuse', async function () { | ||
566 | this.timeout(10000) | ||
567 | |||
568 | await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id) | ||
569 | |||
570 | await waitJobs(servers) | ||
571 | |||
572 | { | ||
573 | const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' }) | ||
574 | expect(res.body.total).to.equal(0) | ||
575 | expect(res.body.data.length).to.equal(0) | ||
576 | } | ||
577 | |||
578 | { | ||
579 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' }) | ||
580 | expect(res.body.total).to.equal(2) | ||
581 | } | ||
582 | }) | ||
583 | |||
584 | it('Should list and filter video abuses', async function () { | ||
585 | { | ||
586 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment', searchReportee: 'foo' }) | ||
587 | expect(res.body.total).to.equal(0) | ||
588 | } | ||
589 | |||
590 | { | ||
591 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment', searchReportee: 'ot' }) | ||
592 | expect(res.body.total).to.equal(2) | ||
593 | } | ||
594 | |||
595 | { | ||
596 | const baseParams = { url: servers[0].url, token: servers[0].accessToken, filter: 'comment' as AbuseFilter, start: 1, count: 1 } | ||
597 | |||
598 | const res1 = await getAbusesList(immutableAssign(baseParams, { sort: 'createdAt' })) | ||
599 | expect(res1.body.data).to.have.lengthOf(1) | ||
600 | expect(res1.body.data[0].comment.text).to.be.empty | ||
601 | |||
602 | const res2 = await getAbusesList(immutableAssign(baseParams, { sort: '-createdAt' })) | ||
603 | expect(res2.body.data).to.have.lengthOf(1) | ||
604 | expect(res2.body.data[0].comment.text).to.equal('comment server 1') | ||
605 | } | ||
606 | }) | ||
607 | }) | ||
608 | |||
609 | describe('Account abuses', function () { | ||
610 | |||
611 | async function getAccountFromServer (url: string, name: string, server: ServerInfo) { | ||
612 | const res = await getAccount(url, name + '@' + server.host) | ||
613 | |||
614 | return res.body as Account | ||
615 | } | ||
616 | |||
617 | before(async function () { | ||
618 | this.timeout(50000) | ||
619 | |||
620 | await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: 'user_1', password: 'donald' }) | ||
621 | |||
622 | const token = await generateUserAccessToken(servers[1], 'user_2') | ||
623 | await uploadVideo(servers[1].url, token, { name: 'super video' }) | ||
624 | |||
625 | await waitJobs(servers) | ||
626 | }) | ||
627 | |||
628 | it('Should report abuse on an account', async function () { | ||
629 | this.timeout(15000) | ||
630 | |||
631 | const account = await getAccountFromServer(servers[0].url, 'user_1', servers[0]) | ||
632 | |||
633 | const reason = 'it is a bad account' | ||
634 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId: account.id, reason }) | ||
635 | |||
636 | await waitJobs(servers) | ||
637 | }) | ||
638 | |||
639 | it('Should have 1 account abuse on server 1 and 0 on server 2', async function () { | ||
640 | { | ||
641 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' }) | ||
642 | |||
643 | expect(res.body.total).to.equal(1) | ||
644 | expect(res.body.data).to.have.lengthOf(1) | ||
645 | |||
646 | const abuse: Abuse = res.body.data[0] | ||
647 | expect(abuse.reason).to.equal('it is a bad account') | ||
648 | |||
649 | expect(abuse.reporterAccount.name).to.equal('root') | ||
650 | expect(abuse.reporterAccount.host).to.equal(servers[0].host) | ||
651 | |||
652 | expect(abuse.video).to.be.null | ||
653 | expect(abuse.comment).to.be.null | ||
654 | |||
655 | expect(abuse.flaggedAccount.name).to.equal('user_1') | ||
656 | expect(abuse.flaggedAccount.host).to.equal(servers[0].host) | ||
657 | } | ||
658 | |||
659 | { | ||
660 | const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' }) | ||
661 | expect(res.body.total).to.equal(0) | ||
662 | expect(res.body.data.length).to.equal(0) | ||
663 | } | ||
664 | }) | ||
665 | |||
666 | it('Should report abuse on a remote account', async function () { | ||
667 | this.timeout(10000) | ||
668 | |||
669 | const account = await getAccountFromServer(servers[0].url, 'user_2', servers[1]) | ||
670 | |||
671 | const reason = 'it is a really bad account' | ||
672 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId: account.id, reason }) | ||
673 | |||
674 | await waitJobs(servers) | ||
675 | }) | ||
676 | |||
677 | it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () { | ||
678 | const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' }) | ||
679 | expect(res1.body.total).to.equal(2) | ||
680 | expect(res1.body.data.length).to.equal(2) | ||
681 | |||
682 | const abuse: Abuse = res1.body.data[0] | ||
683 | expect(abuse.reason).to.equal('it is a bad account') | ||
684 | |||
685 | const abuse2: Abuse = res1.body.data[1] | ||
686 | expect(abuse2.reason).to.equal('it is a really bad account') | ||
687 | |||
688 | expect(abuse2.reporterAccount.name).to.equal('root') | ||
689 | expect(abuse2.reporterAccount.host).to.equal(servers[0].host) | ||
690 | |||
691 | expect(abuse2.video).to.be.null | ||
692 | expect(abuse2.comment).to.be.null | ||
693 | |||
694 | expect(abuse2.state.id).to.equal(AbuseState.PENDING) | ||
695 | expect(abuse2.state.label).to.equal('Pending') | ||
696 | |||
697 | expect(abuse2.moderationComment).to.be.null | ||
698 | |||
699 | const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' }) | ||
700 | expect(res2.body.total).to.equal(1) | ||
701 | expect(res2.body.data.length).to.equal(1) | ||
702 | |||
703 | abuseServer2 = res2.body.data[0] | ||
704 | |||
705 | expect(abuseServer2.reason).to.equal('it is a really bad account') | ||
706 | |||
707 | expect(abuseServer2.reporterAccount.name).to.equal('root') | ||
708 | expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) | ||
709 | |||
710 | expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) | ||
711 | expect(abuseServer2.state.label).to.equal('Pending') | ||
712 | |||
713 | expect(abuseServer2.moderationComment).to.be.null | ||
714 | }) | ||
715 | |||
716 | it('Should keep the account abuse when deleting the account', async function () { | ||
717 | this.timeout(10000) | ||
718 | |||
719 | const account = await getAccountFromServer(servers[1].url, 'user_2', servers[1]) | ||
720 | await removeUser(servers[1].url, account.userId, servers[1].accessToken) | ||
721 | |||
722 | await waitJobs(servers) | ||
723 | |||
724 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' }) | ||
725 | expect(res.body.total).to.equal(2) | ||
726 | expect(res.body.data).to.have.lengthOf(2) | ||
727 | |||
728 | const abuse = (res.body.data as Abuse[]).find(a => a.reason === 'it is a really bad account') | ||
729 | expect(abuse).to.not.be.undefined | ||
730 | }) | ||
731 | |||
732 | it('Should delete the account abuse', async function () { | ||
733 | this.timeout(10000) | ||
734 | |||
735 | await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id) | ||
736 | |||
737 | await waitJobs(servers) | ||
738 | |||
739 | { | ||
740 | const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' }) | ||
741 | expect(res.body.total).to.equal(0) | ||
742 | expect(res.body.data.length).to.equal(0) | ||
743 | } | ||
744 | |||
745 | { | ||
746 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' }) | ||
747 | expect(res.body.total).to.equal(2) | ||
748 | |||
749 | abuseServer1 = res.body.data[0] | ||
750 | } | ||
751 | }) | ||
752 | }) | ||
753 | |||
754 | describe('Common actions on abuses', function () { | ||
755 | |||
756 | it('Should update the state of an abuse', async function () { | ||
757 | const body = { state: AbuseState.REJECTED } | ||
758 | await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body) | ||
759 | |||
760 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id }) | ||
761 | expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED) | ||
762 | }) | ||
763 | |||
764 | it('Should add a moderation comment', async function () { | ||
765 | const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' } | ||
766 | await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body) | ||
767 | |||
768 | const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id }) | ||
769 | expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED) | ||
770 | expect(res.body.data[0].moderationComment).to.equal('It is valid') | ||
771 | }) | ||
772 | }) | ||
773 | |||
774 | after(async function () { | ||
775 | await cleanupTests(servers) | ||
776 | }) | ||
777 | }) | ||
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/moderation/blocklist.ts index 8c9107a50..8c9107a50 100644 --- a/server/tests/api/users/blocklist.ts +++ b/server/tests/api/moderation/blocklist.ts | |||
diff --git a/server/tests/api/moderation/index.ts b/server/tests/api/moderation/index.ts new file mode 100644 index 000000000..cb018d88e --- /dev/null +++ b/server/tests/api/moderation/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './abuses' | ||
2 | export * from './blocklist' | ||
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts index b90732a7a..a8517600a 100644 --- a/server/tests/api/notifications/moderation-notifications.ts +++ b/server/tests/api/notifications/moderation-notifications.ts | |||
@@ -3,15 +3,21 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { v4 as uuidv4 } from 'uuid' | 4 | import { v4 as uuidv4 } from 'uuid' |
5 | import { | 5 | import { |
6 | addVideoCommentThread, | ||
6 | addVideoToBlacklist, | 7 | addVideoToBlacklist, |
7 | cleanupTests, | 8 | cleanupTests, |
9 | createUser, | ||
8 | follow, | 10 | follow, |
11 | generateUserAccessToken, | ||
12 | getAccount, | ||
9 | getCustomConfig, | 13 | getCustomConfig, |
14 | getVideoCommentThreads, | ||
15 | getVideoIdFromUUID, | ||
10 | immutableAssign, | 16 | immutableAssign, |
11 | MockInstancesIndex, | 17 | MockInstancesIndex, |
12 | registerUser, | 18 | registerUser, |
13 | removeVideoFromBlacklist, | 19 | removeVideoFromBlacklist, |
14 | reportVideoAbuse, | 20 | reportAbuse, |
15 | unfollow, | 21 | unfollow, |
16 | updateCustomConfig, | 22 | updateCustomConfig, |
17 | updateCustomSubConfig, | 23 | updateCustomSubConfig, |
@@ -23,7 +29,9 @@ import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | |||
23 | import { | 29 | import { |
24 | checkAutoInstanceFollowing, | 30 | checkAutoInstanceFollowing, |
25 | CheckerBaseParams, | 31 | CheckerBaseParams, |
32 | checkNewAccountAbuseForModerators, | ||
26 | checkNewBlacklistOnMyVideo, | 33 | checkNewBlacklistOnMyVideo, |
34 | checkNewCommentAbuseForModerators, | ||
27 | checkNewInstanceFollower, | 35 | checkNewInstanceFollower, |
28 | checkNewVideoAbuseForModerators, | 36 | checkNewVideoAbuseForModerators, |
29 | checkNewVideoFromSubscription, | 37 | checkNewVideoFromSubscription, |
@@ -74,12 +82,12 @@ describe('Test moderation notifications', function () { | |||
74 | 82 | ||
75 | const name = 'video for abuse ' + uuidv4() | 83 | const name = 'video for abuse ' + uuidv4() |
76 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 84 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
77 | const uuid = resVideo.body.video.uuid | 85 | const video = resVideo.body.video |
78 | 86 | ||
79 | await reportVideoAbuse(servers[0].url, servers[0].accessToken, uuid, 'super reason') | 87 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video.id, reason: 'super reason' }) |
80 | 88 | ||
81 | await waitJobs(servers) | 89 | await waitJobs(servers) |
82 | await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') | 90 | await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence') |
83 | }) | 91 | }) |
84 | 92 | ||
85 | it('Should send a notification to moderators on remote video abuse', async function () { | 93 | it('Should send a notification to moderators on remote video abuse', async function () { |
@@ -87,14 +95,77 @@ describe('Test moderation notifications', function () { | |||
87 | 95 | ||
88 | const name = 'video for abuse ' + uuidv4() | 96 | const name = 'video for abuse ' + uuidv4() |
89 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 97 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
90 | const uuid = resVideo.body.video.uuid | 98 | const video = resVideo.body.video |
99 | |||
100 | await waitJobs(servers) | ||
101 | |||
102 | const videoId = await getVideoIdFromUUID(servers[1].url, video.uuid) | ||
103 | await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'super reason' }) | ||
104 | |||
105 | await waitJobs(servers) | ||
106 | await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence') | ||
107 | }) | ||
108 | |||
109 | it('Should send a notification to moderators on local comment abuse', async function () { | ||
110 | this.timeout(10000) | ||
111 | |||
112 | const name = 'video for abuse ' + uuidv4() | ||
113 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
114 | const video = resVideo.body.video | ||
115 | const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + uuidv4()) | ||
116 | const comment = resComment.body.comment | ||
117 | |||
118 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason: 'super reason' }) | ||
119 | |||
120 | await waitJobs(servers) | ||
121 | await checkNewCommentAbuseForModerators(baseParams, video.uuid, name, 'presence') | ||
122 | }) | ||
123 | |||
124 | it('Should send a notification to moderators on remote comment abuse', async function () { | ||
125 | this.timeout(10000) | ||
126 | |||
127 | const name = 'video for abuse ' + uuidv4() | ||
128 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | ||
129 | const video = resVideo.body.video | ||
130 | await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + uuidv4()) | ||
131 | |||
132 | await waitJobs(servers) | ||
133 | |||
134 | const resComments = await getVideoCommentThreads(servers[1].url, video.uuid, 0, 5) | ||
135 | const commentId = resComments.body.data[0].id | ||
136 | await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, commentId, reason: 'super reason' }) | ||
137 | |||
138 | await waitJobs(servers) | ||
139 | await checkNewCommentAbuseForModerators(baseParams, video.uuid, name, 'presence') | ||
140 | }) | ||
141 | |||
142 | it('Should send a notification to moderators on local account abuse', async function () { | ||
143 | this.timeout(10000) | ||
144 | |||
145 | const username = 'user' + new Date().getTime() | ||
146 | const resUser = await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username, password: 'donald' }) | ||
147 | const accountId = resUser.body.user.account.id | ||
148 | |||
149 | await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId, reason: 'super reason' }) | ||
150 | |||
151 | await waitJobs(servers) | ||
152 | await checkNewAccountAbuseForModerators(baseParams, username, 'presence') | ||
153 | }) | ||
154 | |||
155 | it('Should send a notification to moderators on remote account abuse', async function () { | ||
156 | this.timeout(10000) | ||
157 | |||
158 | const username = 'user' + new Date().getTime() | ||
159 | const tmpToken = await generateUserAccessToken(servers[0], username) | ||
160 | await uploadVideo(servers[0].url, tmpToken, { name: 'super video' }) | ||
91 | 161 | ||
92 | await waitJobs(servers) | 162 | await waitJobs(servers) |
93 | 163 | ||
94 | await reportVideoAbuse(servers[1].url, servers[1].accessToken, uuid, 'super reason') | 164 | const resAccount = await getAccount(servers[1].url, username + '@' + servers[0].host) |
165 | await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, accountId: resAccount.body.id, reason: 'super reason' }) | ||
95 | 166 | ||
96 | await waitJobs(servers) | 167 | await waitJobs(servers) |
97 | await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') | 168 | await checkNewAccountAbuseForModerators(baseParams, username, 'presence') |
98 | }) | 169 | }) |
99 | }) | 170 | }) |
100 | 171 | ||
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts index 95b64a459..b01a91d48 100644 --- a/server/tests/api/server/email.ts +++ b/server/tests/api/server/email.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { | 5 | import { |
6 | addVideoToBlacklist, | 6 | addVideoToBlacklist, |
7 | askResetPassword, | 7 | askResetPassword, |
@@ -11,7 +11,7 @@ import { | |||
11 | createUser, | 11 | createUser, |
12 | flushAndRunServer, | 12 | flushAndRunServer, |
13 | removeVideoFromBlacklist, | 13 | removeVideoFromBlacklist, |
14 | reportVideoAbuse, | 14 | reportAbuse, |
15 | resetPassword, | 15 | resetPassword, |
16 | ServerInfo, | 16 | ServerInfo, |
17 | setAccessTokensToServers, | 17 | setAccessTokensToServers, |
@@ -30,10 +30,15 @@ describe('Test emails', function () { | |||
30 | let userId: number | 30 | let userId: number |
31 | let userId2: number | 31 | let userId2: number |
32 | let userAccessToken: string | 32 | let userAccessToken: string |
33 | |||
33 | let videoUUID: string | 34 | let videoUUID: string |
35 | let videoId: number | ||
36 | |||
34 | let videoUserUUID: string | 37 | let videoUserUUID: string |
38 | |||
35 | let verificationString: string | 39 | let verificationString: string |
36 | let verificationString2: string | 40 | let verificationString2: string |
41 | |||
37 | const emails: object[] = [] | 42 | const emails: object[] = [] |
38 | const user = { | 43 | const user = { |
39 | username: 'user_1', | 44 | username: 'user_1', |
@@ -76,6 +81,7 @@ describe('Test emails', function () { | |||
76 | } | 81 | } |
77 | const res = await uploadVideo(server.url, server.accessToken, attributes) | 82 | const res = await uploadVideo(server.url, server.accessToken, attributes) |
78 | videoUUID = res.body.video.uuid | 83 | videoUUID = res.body.video.uuid |
84 | videoId = res.body.video.id | ||
79 | } | 85 | } |
80 | }) | 86 | }) |
81 | 87 | ||
@@ -174,12 +180,12 @@ describe('Test emails', function () { | |||
174 | }) | 180 | }) |
175 | }) | 181 | }) |
176 | 182 | ||
177 | describe('When creating a video abuse', function () { | 183 | describe('When creating an abuse', function () { |
178 | it('Should send the notification email', async function () { | 184 | it('Should send the notification email', async function () { |
179 | this.timeout(10000) | 185 | this.timeout(10000) |
180 | 186 | ||
181 | const reason = 'my super bad reason' | 187 | const reason = 'my super bad reason' |
182 | await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason) | 188 | await reportAbuse({ url: server.url, token: server.accessToken, videoId, reason }) |
183 | 189 | ||
184 | await waitJobs(server) | 190 | await waitJobs(server) |
185 | expect(emails).to.have.lengthOf(3) | 191 | expect(emails).to.have.lengthOf(3) |
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index fcd022429..a244a6edb 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import './users-verification' | ||
2 | import './blocklist' | ||
3 | import './user-subscriptions' | 1 | import './user-subscriptions' |
4 | import './users' | 2 | import './users' |
5 | import './users-multiple-servers' | 3 | import './users-multiple-servers' |
4 | import './users-verification' | ||
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 0a66bd1ce..ea74bde6a 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
5 | import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index' | 4 | import * as chai from 'chai' |
5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' | ||
6 | import { CustomConfig } from '@shared/models/server' | ||
6 | import { | 7 | import { |
7 | addVideoCommentThread, | 8 | addVideoCommentThread, |
8 | blockUser, | 9 | blockUser, |
@@ -10,6 +11,7 @@ import { | |||
10 | createUser, | 11 | createUser, |
11 | deleteMe, | 12 | deleteMe, |
12 | flushAndRunServer, | 13 | flushAndRunServer, |
14 | getAbusesList, | ||
13 | getAccountRatings, | 15 | getAccountRatings, |
14 | getBlacklistedVideosList, | 16 | getBlacklistedVideosList, |
15 | getCustomConfig, | 17 | getCustomConfig, |
@@ -19,7 +21,6 @@ import { | |||
19 | getUserInformation, | 21 | getUserInformation, |
20 | getUsersList, | 22 | getUsersList, |
21 | getUsersListPaginationAndSort, | 23 | getUsersListPaginationAndSort, |
22 | getVideoAbusesList, | ||
23 | getVideoChannel, | 24 | getVideoChannel, |
24 | getVideosList, | 25 | getVideosList, |
25 | installPlugin, | 26 | installPlugin, |
@@ -29,15 +30,15 @@ import { | |||
29 | registerUserWithChannel, | 30 | registerUserWithChannel, |
30 | removeUser, | 31 | removeUser, |
31 | removeVideo, | 32 | removeVideo, |
32 | reportVideoAbuse, | 33 | reportAbuse, |
33 | ServerInfo, | 34 | ServerInfo, |
34 | testImage, | 35 | testImage, |
35 | unblockUser, | 36 | unblockUser, |
37 | updateAbuse, | ||
36 | updateCustomSubConfig, | 38 | updateCustomSubConfig, |
37 | updateMyAvatar, | 39 | updateMyAvatar, |
38 | updateMyUser, | 40 | updateMyUser, |
39 | updateUser, | 41 | updateUser, |
40 | updateVideoAbuse, | ||
41 | uploadVideo, | 42 | uploadVideo, |
42 | userLogin, | 43 | userLogin, |
43 | waitJobs | 44 | waitJobs |
@@ -46,7 +47,6 @@ import { follow } from '../../../../shared/extra-utils/server/follows' | |||
46 | import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 47 | import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
47 | import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' | 48 | import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' |
48 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 49 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
49 | import { CustomConfig } from '@shared/models/server' | ||
50 | 50 | ||
51 | const expect = chai.expect | 51 | const expect = chai.expect |
52 | 52 | ||
@@ -302,10 +302,10 @@ describe('Test users', function () { | |||
302 | expect(userGet.videosCount).to.equal(0) | 302 | expect(userGet.videosCount).to.equal(0) |
303 | expect(userGet.videoCommentsCount).to.be.a('number') | 303 | expect(userGet.videoCommentsCount).to.be.a('number') |
304 | expect(userGet.videoCommentsCount).to.equal(0) | 304 | expect(userGet.videoCommentsCount).to.equal(0) |
305 | expect(userGet.videoAbusesCount).to.be.a('number') | 305 | expect(userGet.abusesCount).to.be.a('number') |
306 | expect(userGet.videoAbusesCount).to.equal(0) | 306 | expect(userGet.abusesCount).to.equal(0) |
307 | expect(userGet.videoAbusesAcceptedCount).to.be.a('number') | 307 | expect(userGet.abusesAcceptedCount).to.be.a('number') |
308 | expect(userGet.videoAbusesAcceptedCount).to.equal(0) | 308 | expect(userGet.abusesAcceptedCount).to.equal(0) |
309 | }) | 309 | }) |
310 | }) | 310 | }) |
311 | 311 | ||
@@ -895,9 +895,9 @@ describe('Test users', function () { | |||
895 | 895 | ||
896 | expect(user.videosCount).to.equal(0) | 896 | expect(user.videosCount).to.equal(0) |
897 | expect(user.videoCommentsCount).to.equal(0) | 897 | expect(user.videoCommentsCount).to.equal(0) |
898 | expect(user.videoAbusesCount).to.equal(0) | 898 | expect(user.abusesCount).to.equal(0) |
899 | expect(user.videoAbusesCreatedCount).to.equal(0) | 899 | expect(user.abusesCreatedCount).to.equal(0) |
900 | expect(user.videoAbusesAcceptedCount).to.equal(0) | 900 | expect(user.abusesAcceptedCount).to.equal(0) |
901 | }) | 901 | }) |
902 | 902 | ||
903 | it('Should report correct videos count', async function () { | 903 | it('Should report correct videos count', async function () { |
@@ -924,26 +924,26 @@ describe('Test users', function () { | |||
924 | expect(user.videoCommentsCount).to.equal(1) | 924 | expect(user.videoCommentsCount).to.equal(1) |
925 | }) | 925 | }) |
926 | 926 | ||
927 | it('Should report correct video abuses counts', async function () { | 927 | it('Should report correct abuses counts', async function () { |
928 | const reason = 'my super bad reason' | 928 | const reason = 'my super bad reason' |
929 | await reportVideoAbuse(server.url, user17AccessToken, videoId, reason) | 929 | await reportAbuse({ url: server.url, token: user17AccessToken, videoId, reason }) |
930 | 930 | ||
931 | const res1 = await getVideoAbusesList({ url: server.url, token: server.accessToken }) | 931 | const res1 = await getAbusesList({ url: server.url, token: server.accessToken }) |
932 | const abuseId = res1.body.data[0].id | 932 | const abuseId = res1.body.data[0].id |
933 | 933 | ||
934 | const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true) | 934 | const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true) |
935 | const user2: User = res2.body | 935 | const user2: User = res2.body |
936 | 936 | ||
937 | expect(user2.videoAbusesCount).to.equal(1) // number of incriminations | 937 | expect(user2.abusesCount).to.equal(1) // number of incriminations |
938 | expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created | 938 | expect(user2.abusesCreatedCount).to.equal(1) // number of reports created |
939 | 939 | ||
940 | const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED } | 940 | const body: AbuseUpdate = { state: AbuseState.ACCEPTED } |
941 | await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body) | 941 | await updateAbuse(server.url, server.accessToken, abuseId, body) |
942 | 942 | ||
943 | const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true) | 943 | const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true) |
944 | const user3: User = res3.body | 944 | const user3: User = res3.body |
945 | 945 | ||
946 | expect(user3.videoAbusesAcceptedCount).to.equal(1) // number of reports created accepted | 946 | expect(user3.abusesAcceptedCount).to.equal(1) // number of reports created accepted |
947 | }) | 947 | }) |
948 | }) | 948 | }) |
949 | 949 | ||
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts index 7383bd991..baeb543e0 100644 --- a/server/tests/api/videos/video-abuse.ts +++ b/server/tests/api/videos/video-abuse.ts | |||
@@ -1,21 +1,21 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
5 | import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos' | 4 | import * as chai from 'chai' |
5 | import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models' | ||
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
8 | createUser, | ||
8 | deleteVideoAbuse, | 9 | deleteVideoAbuse, |
9 | flushAndRunMultipleServers, | 10 | flushAndRunMultipleServers, |
10 | getVideoAbusesList, | 11 | getVideoAbusesList, |
11 | getVideosList, | 12 | getVideosList, |
13 | removeVideo, | ||
12 | reportVideoAbuse, | 14 | reportVideoAbuse, |
13 | ServerInfo, | 15 | ServerInfo, |
14 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
15 | updateVideoAbuse, | 17 | updateVideoAbuse, |
16 | uploadVideo, | 18 | uploadVideo, |
17 | removeVideo, | ||
18 | createUser, | ||
19 | userLogin | 19 | userLogin |
20 | } from '../../../../shared/extra-utils/index' | 20 | } from '../../../../shared/extra-utils/index' |
21 | import { doubleFollow } from '../../../../shared/extra-utils/server/follows' | 21 | import { doubleFollow } from '../../../../shared/extra-utils/server/follows' |
@@ -29,9 +29,11 @@ import { | |||
29 | 29 | ||
30 | const expect = chai.expect | 30 | const expect = chai.expect |
31 | 31 | ||
32 | // FIXME: deprecated in 2.3. Remove this controller | ||
33 | |||
32 | describe('Test video abuses', function () { | 34 | describe('Test video abuses', function () { |
33 | let servers: ServerInfo[] = [] | 35 | let servers: ServerInfo[] = [] |
34 | let abuseServer2: VideoAbuse | 36 | let abuseServer2: Abuse |
35 | 37 | ||
36 | before(async function () { | 38 | before(async function () { |
37 | this.timeout(50000) | 39 | this.timeout(50000) |
@@ -95,14 +97,14 @@ describe('Test video abuses', function () { | |||
95 | expect(res1.body.data).to.be.an('array') | 97 | expect(res1.body.data).to.be.an('array') |
96 | expect(res1.body.data.length).to.equal(1) | 98 | expect(res1.body.data.length).to.equal(1) |
97 | 99 | ||
98 | const abuse: VideoAbuse = res1.body.data[0] | 100 | const abuse: Abuse = res1.body.data[0] |
99 | expect(abuse.reason).to.equal('my super bad reason') | 101 | expect(abuse.reason).to.equal('my super bad reason') |
100 | expect(abuse.reporterAccount.name).to.equal('root') | 102 | expect(abuse.reporterAccount.name).to.equal('root') |
101 | expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port) | 103 | expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port) |
102 | expect(abuse.video.id).to.equal(servers[0].video.id) | 104 | expect(abuse.video.id).to.equal(servers[0].video.id) |
103 | expect(abuse.video.channel).to.exist | 105 | expect(abuse.video.channel).to.exist |
104 | expect(abuse.count).to.equal(1) | 106 | expect(abuse.video.countReports).to.equal(1) |
105 | expect(abuse.nth).to.equal(1) | 107 | expect(abuse.video.nthReport).to.equal(1) |
106 | expect(abuse.countReportsForReporter).to.equal(1) | 108 | expect(abuse.countReportsForReporter).to.equal(1) |
107 | expect(abuse.countReportsForReportee).to.equal(1) | 109 | expect(abuse.countReportsForReportee).to.equal(1) |
108 | 110 | ||
@@ -128,23 +130,23 @@ describe('Test video abuses', function () { | |||
128 | expect(res1.body.data).to.be.an('array') | 130 | expect(res1.body.data).to.be.an('array') |
129 | expect(res1.body.data.length).to.equal(2) | 131 | expect(res1.body.data.length).to.equal(2) |
130 | 132 | ||
131 | const abuse1: VideoAbuse = res1.body.data[0] | 133 | const abuse1: Abuse = res1.body.data[0] |
132 | expect(abuse1.reason).to.equal('my super bad reason') | 134 | expect(abuse1.reason).to.equal('my super bad reason') |
133 | expect(abuse1.reporterAccount.name).to.equal('root') | 135 | expect(abuse1.reporterAccount.name).to.equal('root') |
134 | expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port) | 136 | expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port) |
135 | expect(abuse1.video.id).to.equal(servers[0].video.id) | 137 | expect(abuse1.video.id).to.equal(servers[0].video.id) |
136 | expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING) | 138 | expect(abuse1.state.id).to.equal(AbuseState.PENDING) |
137 | expect(abuse1.state.label).to.equal('Pending') | 139 | expect(abuse1.state.label).to.equal('Pending') |
138 | expect(abuse1.moderationComment).to.be.null | 140 | expect(abuse1.moderationComment).to.be.null |
139 | expect(abuse1.count).to.equal(1) | 141 | expect(abuse1.video.countReports).to.equal(1) |
140 | expect(abuse1.nth).to.equal(1) | 142 | expect(abuse1.video.nthReport).to.equal(1) |
141 | 143 | ||
142 | const abuse2: VideoAbuse = res1.body.data[1] | 144 | const abuse2: Abuse = res1.body.data[1] |
143 | expect(abuse2.reason).to.equal('my super bad reason 2') | 145 | expect(abuse2.reason).to.equal('my super bad reason 2') |
144 | expect(abuse2.reporterAccount.name).to.equal('root') | 146 | expect(abuse2.reporterAccount.name).to.equal('root') |
145 | expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port) | 147 | expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port) |
146 | expect(abuse2.video.id).to.equal(servers[1].video.id) | 148 | expect(abuse2.video.id).to.equal(servers[1].video.id) |
147 | expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING) | 149 | expect(abuse2.state.id).to.equal(AbuseState.PENDING) |
148 | expect(abuse2.state.label).to.equal('Pending') | 150 | expect(abuse2.state.label).to.equal('Pending') |
149 | expect(abuse2.moderationComment).to.be.null | 151 | expect(abuse2.moderationComment).to.be.null |
150 | 152 | ||
@@ -157,25 +159,25 @@ describe('Test video abuses', function () { | |||
157 | expect(abuseServer2.reason).to.equal('my super bad reason 2') | 159 | expect(abuseServer2.reason).to.equal('my super bad reason 2') |
158 | expect(abuseServer2.reporterAccount.name).to.equal('root') | 160 | expect(abuseServer2.reporterAccount.name).to.equal('root') |
159 | expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port) | 161 | expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port) |
160 | expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING) | 162 | expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) |
161 | expect(abuseServer2.state.label).to.equal('Pending') | 163 | expect(abuseServer2.state.label).to.equal('Pending') |
162 | expect(abuseServer2.moderationComment).to.be.null | 164 | expect(abuseServer2.moderationComment).to.be.null |
163 | }) | 165 | }) |
164 | 166 | ||
165 | it('Should update the state of a video abuse', async function () { | 167 | it('Should update the state of a video abuse', async function () { |
166 | const body = { state: VideoAbuseState.REJECTED } | 168 | const body = { state: AbuseState.REJECTED } |
167 | await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) | 169 | await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) |
168 | 170 | ||
169 | const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) | 171 | const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) |
170 | expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED) | 172 | expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED) |
171 | }) | 173 | }) |
172 | 174 | ||
173 | it('Should add a moderation comment', async function () { | 175 | it('Should add a moderation comment', async function () { |
174 | const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' } | 176 | const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' } |
175 | await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) | 177 | await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) |
176 | 178 | ||
177 | const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) | 179 | const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) |
178 | expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED) | 180 | expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED) |
179 | expect(res.body.data[0].moderationComment).to.equal('It is valid') | 181 | expect(res.body.data[0].moderationComment).to.equal('It is valid') |
180 | }) | 182 | }) |
181 | 183 | ||
@@ -243,7 +245,7 @@ describe('Test video abuses', function () { | |||
243 | expect(res.body.data.length).to.equal(2, "wrong number of videos returned") | 245 | expect(res.body.data.length).to.equal(2, "wrong number of videos returned") |
244 | expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video") | 246 | expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video") |
245 | 247 | ||
246 | const abuse: VideoAbuse = res.body.data[0] | 248 | const abuse: Abuse = res.body.data[0] |
247 | expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id") | 249 | expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id") |
248 | expect(abuse.video.channel).to.exist | 250 | expect(abuse.video.channel).to.exist |
249 | expect(abuse.video.deleted).to.be.true | 251 | expect(abuse.video.deleted).to.be.true |
@@ -277,10 +279,10 @@ describe('Test video abuses', function () { | |||
277 | const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | 279 | const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) |
278 | 280 | ||
279 | { | 281 | { |
280 | for (const abuse of res2.body.data as VideoAbuse[]) { | 282 | for (const abuse of res2.body.data as Abuse[]) { |
281 | if (abuse.video.id === video3.id) { | 283 | if (abuse.video.id === video3.id) { |
282 | expect(abuse.count).to.equal(1, "wrong reports count for video 3") | 284 | expect(abuse.video.countReports).to.equal(1, "wrong reports count for video 3") |
283 | expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3") | 285 | expect(abuse.video.nthReport).to.equal(1, "wrong report position in report list for video 3") |
284 | expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse") | 286 | expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse") |
285 | expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse") | 287 | expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse") |
286 | } | 288 | } |
@@ -295,7 +297,7 @@ describe('Test video abuses', function () { | |||
295 | this.timeout(10000) | 297 | this.timeout(10000) |
296 | 298 | ||
297 | const reason5 = 'my super bad reason 5' | 299 | const reason5 = 'my super bad reason 5' |
298 | const predefinedReasons5: VideoAbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] | 300 | const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] |
299 | const createdAbuse = (await reportVideoAbuse( | 301 | const createdAbuse = (await reportVideoAbuse( |
300 | servers[0].url, | 302 | servers[0].url, |
301 | servers[0].accessToken, | 303 | servers[0].accessToken, |
@@ -304,16 +306,16 @@ describe('Test video abuses', function () { | |||
304 | predefinedReasons5, | 306 | predefinedReasons5, |
305 | 1, | 307 | 1, |
306 | 5 | 308 | 5 |
307 | )).body.videoAbuse as VideoAbuse | 309 | )).body.abuse |
308 | 310 | ||
309 | const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) | 311 | const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) |
310 | 312 | ||
311 | { | 313 | { |
312 | const abuse = (res.body.data as VideoAbuse[]).find(a => a.id === createdAbuse.id) | 314 | const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id) |
313 | expect(abuse.reason).to.equals(reason5) | 315 | expect(abuse.reason).to.equals(reason5) |
314 | expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported") | 316 | expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported") |
315 | expect(abuse.startAt).to.equal(1, "starting timestamp doesn't match the one reported") | 317 | expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported") |
316 | expect(abuse.endAt).to.equal(5, "ending timestamp doesn't match the one reported") | 318 | expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported") |
317 | } | 319 | } |
318 | }) | 320 | }) |
319 | 321 | ||
@@ -348,7 +350,7 @@ describe('Test video abuses', function () { | |||
348 | 350 | ||
349 | const res = await getVideoAbusesList(options) | 351 | const res = await getVideoAbusesList(options) |
350 | 352 | ||
351 | return res.body.data as VideoAbuse[] | 353 | return res.body.data as Abuse[] |
352 | } | 354 | } |
353 | 355 | ||
354 | expect(await list({ id: 56 })).to.have.lengthOf(0) | 356 | expect(await list({ id: 56 })).to.have.lengthOf(0) |
@@ -365,14 +367,14 @@ describe('Test video abuses', function () { | |||
365 | expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) | 367 | expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) |
366 | expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5) | 368 | expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5) |
367 | 369 | ||
368 | expect(await list({ searchReportee: 'root' })).to.have.lengthOf(4) | 370 | expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5) |
369 | expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) | 371 | expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) |
370 | 372 | ||
371 | expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) | 373 | expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) |
372 | expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) | 374 | expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) |
373 | 375 | ||
374 | expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0) | 376 | expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0) |
375 | expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(6) | 377 | expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6) |
376 | 378 | ||
377 | expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) | 379 | expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) |
378 | expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0) | 380 | 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 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './moderation' | ||
2 | export * from './oauth' | 3 | export * from './oauth' |
3 | export * from './server' | 4 | export * from './server' |
4 | export * from './user' | 5 | 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..a0bf4b08f --- /dev/null +++ b/server/types/models/moderation/abuse.ts | |||
@@ -0,0 +1,103 @@ | |||
1 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' | ||
2 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' | ||
3 | import { PickWith } from '@shared/core-utils' | ||
4 | import { AbuseModel } from '../../../models/abuse/abuse' | ||
5 | import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account' | ||
6 | import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video' | ||
7 | import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' | ||
8 | |||
9 | type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M> | ||
10 | type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M> | ||
11 | type UseCommentAbuse<K extends keyof VideoCommentAbuseModel, M> = PickWith<VideoCommentAbuseModel, K, M> | ||
12 | |||
13 | // ############################################################################ | ||
14 | |||
15 | export type MAbuse = Omit<AbuseModel, 'VideoCommentAbuse' | 'VideoAbuse' | 'ReporterAccount' | 'FlaggedAccount' | 'toActivityPubObject'> | ||
16 | |||
17 | export type MVideoAbuse = Omit<VideoAbuseModel, 'Abuse' | 'Video'> | ||
18 | |||
19 | export type MCommentAbuse = Omit<VideoCommentAbuseModel, 'Abuse' | 'VideoComment'> | ||
20 | |||
21 | // ############################################################################ | ||
22 | |||
23 | export type MVideoAbuseVideo = | ||
24 | MVideoAbuse & | ||
25 | UseVideoAbuse<'Video', MVideo> | ||
26 | |||
27 | export type MVideoAbuseVideoUrl = | ||
28 | MVideoAbuse & | ||
29 | UseVideoAbuse<'Video', MVideoUrl> | ||
30 | |||
31 | export type MVideoAbuseVideoFull = | ||
32 | MVideoAbuse & | ||
33 | UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles> | ||
34 | |||
35 | export type MVideoAbuseFormattable = | ||
36 | MVideoAbuse & | ||
37 | UseVideoAbuse<'Video', Pick<MVideoAccountLightBlacklistAllFiles, | ||
38 | 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>> | ||
39 | |||
40 | // ############################################################################ | ||
41 | |||
42 | export type MCommentAbuseAccount = | ||
43 | MCommentAbuse & | ||
44 | UseCommentAbuse<'VideoComment', MCommentOwner> | ||
45 | |||
46 | export type MCommentAbuseAccountVideo = | ||
47 | MCommentAbuse & | ||
48 | UseCommentAbuse<'VideoComment', MCommentOwnerVideo> | ||
49 | |||
50 | export type MCommentAbuseUrl = | ||
51 | MCommentAbuse & | ||
52 | UseCommentAbuse<'VideoComment', MCommentUrl> | ||
53 | |||
54 | export type MCommentAbuseFormattable = | ||
55 | MCommentAbuse & | ||
56 | UseCommentAbuse<'VideoComment', MComment & PickWith<MCommentVideo, 'Video', Pick<MVideo, 'id' | 'uuid' | 'name'>>> | ||
57 | |||
58 | // ############################################################################ | ||
59 | |||
60 | export type MAbuseId = Pick<AbuseModel, 'id'> | ||
61 | |||
62 | export type MAbuseVideo = | ||
63 | MAbuse & | ||
64 | Pick<AbuseModel, 'toActivityPubObject'> & | ||
65 | Use<'VideoAbuse', MVideoAbuseVideo> | ||
66 | |||
67 | export type MAbuseUrl = | ||
68 | MAbuse & | ||
69 | Use<'VideoAbuse', MVideoAbuseVideoUrl> & | ||
70 | Use<'VideoCommentAbuse', MCommentAbuseUrl> | ||
71 | |||
72 | export type MAbuseAccountVideo = | ||
73 | MAbuse & | ||
74 | Pick<AbuseModel, 'toActivityPubObject'> & | ||
75 | Use<'VideoAbuse', MVideoAbuseVideoFull> & | ||
76 | Use<'ReporterAccount', MAccountDefault> | ||
77 | |||
78 | export type MAbuseAP = | ||
79 | MAbuse & | ||
80 | Pick<AbuseModel, 'toActivityPubObject'> & | ||
81 | Use<'ReporterAccount', MAccountUrl> & | ||
82 | Use<'FlaggedAccount', MAccountUrl> & | ||
83 | Use<'VideoAbuse', MVideoAbuseVideo> & | ||
84 | Use<'VideoCommentAbuse', MCommentAbuseAccount> | ||
85 | |||
86 | export type MAbuseFull = | ||
87 | MAbuse & | ||
88 | Pick<AbuseModel, 'toActivityPubObject'> & | ||
89 | Use<'ReporterAccount', MAccountLight> & | ||
90 | Use<'FlaggedAccount', MAccountLight> & | ||
91 | Use<'VideoAbuse', MVideoAbuseVideoFull> & | ||
92 | Use<'VideoCommentAbuse', MCommentAbuseAccountVideo> | ||
93 | |||
94 | // ############################################################################ | ||
95 | |||
96 | // Format for API or AP object | ||
97 | |||
98 | export type MAbuseFormattable = | ||
99 | MAbuse & | ||
100 | Use<'ReporterAccount', MAccountFormattable> & | ||
101 | Use<'FlaggedAccount', MAccountFormattable> & | ||
102 | Use<'VideoAbuse', MVideoAbuseFormattable> & | ||
103 | Use<'VideoCommentAbuse', MCommentAbuseFormattable> | ||
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..f59eb7260 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts | |||
@@ -1,16 +1,18 @@ | |||
1 | import { UserNotificationModel } from '../../../models/account/user-notification' | 1 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' |
2 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' | ||
2 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 3 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
3 | import { VideoModel } from '../../../models/video/video' | 4 | import { AbuseModel } from '../../../models/abuse/abuse' |
5 | import { AccountModel } from '../../../models/account/account' | ||
6 | import { UserNotificationModel } from '../../../models/account/user-notification' | ||
4 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { ServerModel } from '../../../models/server/server' | 8 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
6 | import { AvatarModel } from '../../../models/avatar/avatar' | 9 | import { AvatarModel } from '../../../models/avatar/avatar' |
10 | import { ServerModel } from '../../../models/server/server' | ||
11 | import { VideoModel } from '../../../models/video/video' | ||
12 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | ||
7 | import { VideoChannelModel } from '../../../models/video/video-channel' | 13 | import { VideoChannelModel } from '../../../models/video/video-channel' |
8 | import { AccountModel } from '../../../models/account/account' | ||
9 | import { VideoCommentModel } from '../../../models/video/video-comment' | 14 | import { VideoCommentModel } from '../../../models/video/video-comment' |
10 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
11 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | ||
12 | import { VideoImportModel } from '../../../models/video/video-import' | 15 | import { VideoImportModel } from '../../../models/video/video-import' |
13 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
14 | 16 | ||
15 | type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M> | 17 | type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M> |
16 | 18 | ||
@@ -47,6 +49,18 @@ export module UserNotificationIncludes { | |||
47 | Pick<VideoAbuseModel, 'id'> & | 49 | Pick<VideoAbuseModel, 'id'> & |
48 | PickWith<VideoAbuseModel, 'Video', VideoInclude> | 50 | PickWith<VideoAbuseModel, 'Video', VideoInclude> |
49 | 51 | ||
52 | export type VideoCommentAbuseInclude = | ||
53 | Pick<VideoCommentAbuseModel, 'id'> & | ||
54 | PickWith<VideoCommentAbuseModel, 'VideoComment', | ||
55 | Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> & | ||
56 | PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'id' | 'name' | 'uuid'>>> | ||
57 | |||
58 | export type AbuseInclude = | ||
59 | Pick<AbuseModel, 'id'> & | ||
60 | PickWith<AbuseModel, 'VideoAbuse', VideoAbuseInclude> & | ||
61 | PickWith<AbuseModel, 'VideoCommentAbuse', VideoCommentAbuseInclude> & | ||
62 | PickWith<AbuseModel, 'FlaggedAccount', AccountIncludeActor> | ||
63 | |||
50 | export type VideoBlacklistInclude = | 64 | export type VideoBlacklistInclude = |
51 | Pick<VideoBlacklistModel, 'id'> & | 65 | Pick<VideoBlacklistModel, 'id'> & |
52 | PickWith<VideoAbuseModel, 'Video', VideoInclude> | 66 | PickWith<VideoAbuseModel, 'Video', VideoInclude> |
@@ -76,7 +90,7 @@ export module UserNotificationIncludes { | |||
76 | // ############################################################################ | 90 | // ############################################################################ |
77 | 91 | ||
78 | export type MUserNotification = | 92 | export type MUserNotification = |
79 | Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' | | 93 | Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | |
80 | 'VideoImport' | 'Account' | 'ActorFollow'> | 94 | 'VideoImport' | 'Account' | 'ActorFollow'> |
81 | 95 | ||
82 | // ############################################################################ | 96 | // ############################################################################ |
@@ -85,7 +99,7 @@ export type UserNotificationModelForApi = | |||
85 | MUserNotification & | 99 | MUserNotification & |
86 | Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & | 100 | Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & |
87 | Use<'Comment', UserNotificationIncludes.VideoCommentInclude> & | 101 | Use<'Comment', UserNotificationIncludes.VideoCommentInclude> & |
88 | Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> & | 102 | Use<'Abuse', UserNotificationIncludes.AbuseInclude> & |
89 | Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & | 103 | Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & |
90 | Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & | 104 | Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & |
91 | Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & | 105 | 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' | |||
2 | export * from './tag' | 2 | export * from './tag' |
3 | export * from './thumbnail' | 3 | export * from './thumbnail' |
4 | export * from './video' | 4 | export * from './video' |
5 | export * from './video-abuse' | ||
6 | export * from './video-blacklist' | 5 | export * from './video-blacklist' |
7 | export * from './video-caption' | 6 | export * from './video-caption' |
8 | export * from './video-change-ownership' | 7 | 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 @@ | |||
1 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
2 | import { PickWith } from '@shared/core-utils' | ||
3 | import { MVideoAccountLightBlacklistAllFiles, MVideo } from './video' | ||
4 | import { MAccountDefault, MAccountFormattable } from '../account' | ||
5 | |||
6 | type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M> | ||
7 | |||
8 | // ############################################################################ | ||
9 | |||
10 | export type MVideoAbuse = Omit<VideoAbuseModel, 'Account' | 'Video' | 'toActivityPubObject'> | ||
11 | |||
12 | // ############################################################################ | ||
13 | |||
14 | export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'> | ||
15 | |||
16 | export type MVideoAbuseVideo = | ||
17 | MVideoAbuse & | ||
18 | Pick<VideoAbuseModel, 'toActivityPubObject'> & | ||
19 | Use<'Video', MVideo> | ||
20 | |||
21 | export type MVideoAbuseAccountVideo = | ||
22 | MVideoAbuse & | ||
23 | Pick<VideoAbuseModel, 'toActivityPubObject'> & | ||
24 | Use<'Video', MVideoAccountLightBlacklistAllFiles> & | ||
25 | Use<'Account', MAccountDefault> | ||
26 | |||
27 | // ############################################################################ | ||
28 | |||
29 | // Format for API or AP object | ||
30 | |||
31 | export type MVideoAbuseFormattable = | ||
32 | MVideoAbuse & | ||
33 | Use<'Account', MAccountFormattable> & | ||
34 | Use<'Video', Pick<MVideoAccountLightBlacklistAllFiles, | ||
35 | 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>> | ||
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 @@ | |||
1 | import { RegisterServerAuthExternalOptions } from '@server/types' | 1 | import { RegisterServerAuthExternalOptions } from '@server/types' |
2 | import { | 2 | import { |
3 | MAbuse, | ||
3 | MAccountBlocklist, | 4 | MAccountBlocklist, |
4 | MActorUrl, | 5 | MActorUrl, |
5 | MStreamingPlaylist, | 6 | MStreamingPlaylist, |
@@ -26,7 +27,6 @@ import { | |||
26 | MComment, | 27 | MComment, |
27 | MCommentOwnerVideoReply, | 28 | MCommentOwnerVideoReply, |
28 | MUserDefault, | 29 | MUserDefault, |
29 | MVideoAbuse, | ||
30 | MVideoBlacklist, | 30 | MVideoBlacklist, |
31 | MVideoCaptionVideo, | 31 | MVideoCaptionVideo, |
32 | MVideoFullLight, | 32 | MVideoFullLight, |
@@ -77,7 +77,7 @@ declare module 'express' { | |||
77 | 77 | ||
78 | videoCaption?: MVideoCaptionVideo | 78 | videoCaption?: MVideoCaptionVideo |
79 | 79 | ||
80 | videoAbuse?: MVideoAbuse | 80 | abuse?: MAbuse |
81 | 81 | ||
82 | videoStreamingPlaylist?: MStreamingPlaylist | 82 | videoStreamingPlaylist?: MStreamingPlaylist |
83 | 83 | ||
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' | |||
17 | export * from './videos/video-playlists' | 17 | export * from './videos/video-playlists' |
18 | export * from './users/users' | 18 | export * from './users/users' |
19 | export * from './users/accounts' | 19 | export * from './users/accounts' |
20 | export * from './moderation/abuses' | ||
20 | export * from './videos/video-abuses' | 21 | export * from './videos/video-abuses' |
21 | export * from './videos/video-blacklist' | 22 | export * from './videos/video-blacklist' |
22 | export * from './videos/video-captions' | 23 | 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..62af9556e --- /dev/null +++ b/shared/extra-utils/moderation/abuses.ts | |||
@@ -0,0 +1,156 @@ | |||
1 | |||
2 | import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models' | ||
3 | import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' | ||
4 | |||
5 | function reportAbuse (options: { | ||
6 | url: string | ||
7 | token: string | ||
8 | |||
9 | reason: string | ||
10 | |||
11 | accountId?: number | ||
12 | videoId?: number | ||
13 | commentId?: number | ||
14 | |||
15 | predefinedReasons?: AbusePredefinedReasonsString[] | ||
16 | |||
17 | startAt?: number | ||
18 | endAt?: number | ||
19 | |||
20 | statusCodeExpected?: number | ||
21 | }) { | ||
22 | const path = '/api/v1/abuses' | ||
23 | |||
24 | const video = options.videoId ? { | ||
25 | id: options.videoId, | ||
26 | startAt: options.startAt, | ||
27 | endAt: options.endAt | ||
28 | } : undefined | ||
29 | |||
30 | const comment = options.commentId ? { | ||
31 | id: options.commentId | ||
32 | } : undefined | ||
33 | |||
34 | const account = options.accountId ? { | ||
35 | id: options.accountId | ||
36 | } : undefined | ||
37 | |||
38 | const body = { | ||
39 | account, | ||
40 | video, | ||
41 | comment, | ||
42 | |||
43 | reason: options.reason, | ||
44 | predefinedReasons: options.predefinedReasons | ||
45 | } | ||
46 | |||
47 | return makePostBodyRequest({ | ||
48 | url: options.url, | ||
49 | path, | ||
50 | token: options.token, | ||
51 | |||
52 | fields: body, | ||
53 | statusCodeExpected: options.statusCodeExpected || 200 | ||
54 | }) | ||
55 | } | ||
56 | |||
57 | function getAbusesList (options: { | ||
58 | url: string | ||
59 | token: string | ||
60 | |||
61 | start?: number | ||
62 | count?: number | ||
63 | sort?: string | ||
64 | |||
65 | id?: number | ||
66 | predefinedReason?: AbusePredefinedReasonsString | ||
67 | search?: string | ||
68 | filter?: AbuseFilter | ||
69 | state?: AbuseState | ||
70 | videoIs?: AbuseVideoIs | ||
71 | searchReporter?: string | ||
72 | searchReportee?: string | ||
73 | searchVideo?: string | ||
74 | searchVideoChannel?: string | ||
75 | }) { | ||
76 | const { | ||
77 | url, | ||
78 | token, | ||
79 | start, | ||
80 | count, | ||
81 | sort, | ||
82 | id, | ||
83 | predefinedReason, | ||
84 | search, | ||
85 | filter, | ||
86 | state, | ||
87 | videoIs, | ||
88 | searchReporter, | ||
89 | searchReportee, | ||
90 | searchVideo, | ||
91 | searchVideoChannel | ||
92 | } = options | ||
93 | const path = '/api/v1/abuses' | ||
94 | |||
95 | const query = { | ||
96 | id, | ||
97 | predefinedReason, | ||
98 | search, | ||
99 | state, | ||
100 | filter, | ||
101 | videoIs, | ||
102 | start, | ||
103 | count, | ||
104 | sort: sort || 'createdAt', | ||
105 | searchReporter, | ||
106 | searchReportee, | ||
107 | searchVideo, | ||
108 | searchVideoChannel | ||
109 | } | ||
110 | |||
111 | return makeGetRequest({ | ||
112 | url, | ||
113 | path, | ||
114 | token, | ||
115 | query, | ||
116 | statusCodeExpected: 200 | ||
117 | }) | ||
118 | } | ||
119 | |||
120 | function updateAbuse ( | ||
121 | url: string, | ||
122 | token: string, | ||
123 | abuseId: number, | ||
124 | body: AbuseUpdate, | ||
125 | statusCodeExpected = 204 | ||
126 | ) { | ||
127 | const path = '/api/v1/abuses/' + abuseId | ||
128 | |||
129 | return makePutBodyRequest({ | ||
130 | url, | ||
131 | token, | ||
132 | path, | ||
133 | fields: body, | ||
134 | statusCodeExpected | ||
135 | }) | ||
136 | } | ||
137 | |||
138 | function deleteAbuse (url: string, token: string, abuseId: number, statusCodeExpected = 204) { | ||
139 | const path = '/api/v1/abuses/' + abuseId | ||
140 | |||
141 | return makeDeleteRequest({ | ||
142 | url, | ||
143 | token, | ||
144 | path, | ||
145 | statusCodeExpected | ||
146 | }) | ||
147 | } | ||
148 | |||
149 | // --------------------------------------------------------------------------- | ||
150 | |||
151 | export { | ||
152 | reportAbuse, | ||
153 | getAbusesList, | ||
154 | updateAbuse, | ||
155 | deleteAbuse | ||
156 | } | ||
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index 0f883d839..994aac628 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts | |||
@@ -37,8 +37,8 @@ interface ServerInfo { | |||
37 | video?: { | 37 | video?: { |
38 | id: number | 38 | id: number |
39 | uuid: string | 39 | uuid: string |
40 | name: string | 40 | name?: string |
41 | account: { | 41 | account?: { |
42 | name: string | 42 | name: string |
43 | } | 43 | } |
44 | } | 44 | } |
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts index a17a39de9..2061e3353 100644 --- a/shared/extra-utils/users/user-notifications.ts +++ b/shared/extra-utils/users/user-notifications.ts | |||
@@ -139,13 +139,17 @@ async function checkNotification ( | |||
139 | } | 139 | } |
140 | 140 | ||
141 | function checkVideo (video: any, videoName?: string, videoUUID?: string) { | 141 | function checkVideo (video: any, videoName?: string, videoUUID?: string) { |
142 | expect(video.name).to.be.a('string') | 142 | if (videoName) { |
143 | expect(video.name).to.not.be.empty | 143 | expect(video.name).to.be.a('string') |
144 | if (videoName) expect(video.name).to.equal(videoName) | 144 | expect(video.name).to.not.be.empty |
145 | expect(video.name).to.equal(videoName) | ||
146 | } | ||
145 | 147 | ||
146 | expect(video.uuid).to.be.a('string') | 148 | if (videoUUID) { |
147 | expect(video.uuid).to.not.be.empty | 149 | expect(video.uuid).to.be.a('string') |
148 | if (videoUUID) expect(video.uuid).to.equal(videoUUID) | 150 | expect(video.uuid).to.not.be.empty |
151 | expect(video.uuid).to.equal(videoUUID) | ||
152 | } | ||
149 | 153 | ||
150 | expect(video.id).to.be.a('number') | 154 | expect(video.id).to.be.a('number') |
151 | } | 155 | } |
@@ -436,18 +440,43 @@ async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, | |||
436 | } | 440 | } |
437 | 441 | ||
438 | async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { | 442 | async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { |
439 | const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS | 443 | const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS |
444 | |||
445 | function notificationChecker (notification: UserNotification, type: CheckerType) { | ||
446 | if (type === 'presence') { | ||
447 | expect(notification).to.not.be.undefined | ||
448 | expect(notification.type).to.equal(notificationType) | ||
449 | |||
450 | expect(notification.abuse.id).to.be.a('number') | ||
451 | checkVideo(notification.abuse.video, videoName, videoUUID) | ||
452 | } else { | ||
453 | expect(notification).to.satisfy((n: UserNotification) => { | ||
454 | return n === undefined || n.abuse === undefined || n.abuse.video.uuid !== videoUUID | ||
455 | }) | ||
456 | } | ||
457 | } | ||
458 | |||
459 | function emailNotificationFinder (email: object) { | ||
460 | const text = email['text'] | ||
461 | return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1 | ||
462 | } | ||
463 | |||
464 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) | ||
465 | } | ||
466 | |||
467 | async function checkNewCommentAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { | ||
468 | const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS | ||
440 | 469 | ||
441 | function notificationChecker (notification: UserNotification, type: CheckerType) { | 470 | function notificationChecker (notification: UserNotification, type: CheckerType) { |
442 | if (type === 'presence') { | 471 | if (type === 'presence') { |
443 | expect(notification).to.not.be.undefined | 472 | expect(notification).to.not.be.undefined |
444 | expect(notification.type).to.equal(notificationType) | 473 | expect(notification.type).to.equal(notificationType) |
445 | 474 | ||
446 | expect(notification.videoAbuse.id).to.be.a('number') | 475 | expect(notification.abuse.id).to.be.a('number') |
447 | checkVideo(notification.videoAbuse.video, videoName, videoUUID) | 476 | checkVideo(notification.abuse.comment.video, videoName, videoUUID) |
448 | } else { | 477 | } else { |
449 | expect(notification).to.satisfy((n: UserNotification) => { | 478 | expect(notification).to.satisfy((n: UserNotification) => { |
450 | return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID | 479 | return n === undefined || n.abuse === undefined || n.abuse.comment.video.uuid !== videoUUID |
451 | }) | 480 | }) |
452 | } | 481 | } |
453 | } | 482 | } |
@@ -460,6 +489,31 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU | |||
460 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) | 489 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) |
461 | } | 490 | } |
462 | 491 | ||
492 | async function checkNewAccountAbuseForModerators (base: CheckerBaseParams, displayName: string, type: CheckerType) { | ||
493 | const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS | ||
494 | |||
495 | function notificationChecker (notification: UserNotification, type: CheckerType) { | ||
496 | if (type === 'presence') { | ||
497 | expect(notification).to.not.be.undefined | ||
498 | expect(notification.type).to.equal(notificationType) | ||
499 | |||
500 | expect(notification.abuse.id).to.be.a('number') | ||
501 | expect(notification.abuse.account.displayName).to.equal(displayName) | ||
502 | } else { | ||
503 | expect(notification).to.satisfy((n: UserNotification) => { | ||
504 | return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName | ||
505 | }) | ||
506 | } | ||
507 | } | ||
508 | |||
509 | function emailNotificationFinder (email: object) { | ||
510 | const text = email['text'] | ||
511 | return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1 | ||
512 | } | ||
513 | |||
514 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) | ||
515 | } | ||
516 | |||
463 | async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { | 517 | async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { |
464 | const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS | 518 | const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS |
465 | 519 | ||
@@ -516,7 +570,7 @@ function getAllNotificationsSettings () { | |||
516 | return { | 570 | return { |
517 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 571 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
518 | newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 572 | newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
519 | videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 573 | abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
520 | videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 574 | videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
521 | blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 575 | blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
522 | myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 576 | myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
@@ -541,6 +595,9 @@ async function prepareNotificationsTest (serversCount = 3) { | |||
541 | smtp: { | 595 | smtp: { |
542 | hostname: 'localhost', | 596 | hostname: 'localhost', |
543 | port | 597 | port |
598 | }, | ||
599 | signup: { | ||
600 | limit: 20 | ||
544 | } | 601 | } |
545 | } | 602 | } |
546 | const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) | 603 | const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) |
@@ -623,5 +680,7 @@ export { | |||
623 | markAsReadNotifications, | 680 | markAsReadNotifications, |
624 | getLastNotification, | 681 | getLastNotification, |
625 | checkNewInstanceFollower, | 682 | checkNewInstanceFollower, |
626 | prepareNotificationsTest | 683 | prepareNotificationsTest, |
684 | checkNewCommentAbuseForModerators, | ||
685 | checkNewAccountAbuseForModerators | ||
627 | } | 686 | } |
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 @@ | |||
1 | import * as request from 'supertest' | 1 | import * as request from 'supertest' |
2 | import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model' | 2 | import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models' |
3 | import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests' | 3 | import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests' |
4 | import { VideoAbuseState, VideoAbusePredefinedReasonsString } from '@shared/models' | 4 | |
5 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' | 5 | // FIXME: deprecated in 2.3. Remove this file |
6 | 6 | ||
7 | function reportVideoAbuse ( | 7 | function reportVideoAbuse ( |
8 | url: string, | 8 | url: string, |
9 | token: string, | 9 | token: string, |
10 | videoId: number | string, | 10 | videoId: number | string, |
11 | reason: string, | 11 | reason: string, |
12 | predefinedReasons?: VideoAbusePredefinedReasonsString[], | 12 | predefinedReasons?: AbusePredefinedReasonsString[], |
13 | startAt?: number, | 13 | startAt?: number, |
14 | endAt?: number, | 14 | endAt?: number, |
15 | specialStatus = 200 | 15 | specialStatus = 200 |
@@ -28,10 +28,10 @@ function getVideoAbusesList (options: { | |||
28 | url: string | 28 | url: string |
29 | token: string | 29 | token: string |
30 | id?: number | 30 | id?: number |
31 | predefinedReason?: VideoAbusePredefinedReasonsString | 31 | predefinedReason?: AbusePredefinedReasonsString |
32 | search?: string | 32 | search?: string |
33 | state?: VideoAbuseState | 33 | state?: AbuseState |
34 | videoIs?: VideoAbuseVideoIs | 34 | videoIs?: AbuseVideoIs |
35 | searchReporter?: string | 35 | searchReporter?: string |
36 | searchReportee?: string | 36 | searchReportee?: string |
37 | searchVideo?: string | 37 | searchVideo?: string |
@@ -79,7 +79,7 @@ function updateVideoAbuse ( | |||
79 | token: string, | 79 | token: string, |
80 | videoId: string | number, | 80 | videoId: string | number, |
81 | videoAbuseId: number, | 81 | videoAbuseId: number, |
82 | body: VideoAbuseUpdate, | 82 | body: AbuseUpdate, |
83 | statusCodeExpected = 204 | 83 | statusCodeExpected = 204 |
84 | ) { | 84 | ) { |
85 | const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId | 85 | 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 @@ | |||
1 | import { ActivityPubActor } from './activitypub-actor' | 1 | import { ActivityPubActor } from './activitypub-actor' |
2 | import { ActivityPubSignature } from './activitypub-signature' | 2 | import { ActivityPubSignature } from './activitypub-signature' |
3 | import { CacheFileObject, VideoTorrentObject, ActivityFlagReasonObject } from './objects' | 3 | import { ActivityFlagReasonObject, CacheFileObject, VideoTorrentObject } from './objects' |
4 | import { AbuseObject } from './objects/abuse-object' | ||
4 | import { DislikeObject } from './objects/dislike-object' | 5 | import { DislikeObject } from './objects/dislike-object' |
5 | import { VideoAbuseObject } from './objects/video-abuse-object' | ||
6 | import { VideoCommentObject } from './objects/video-comment-object' | ||
7 | import { ViewObject } from './objects/view-object' | ||
8 | import { APObject } from './objects/object.model' | 6 | import { APObject } from './objects/object.model' |
9 | import { PlaylistObject } from './objects/playlist-object' | 7 | import { PlaylistObject } from './objects/playlist-object' |
8 | import { VideoCommentObject } from './objects/video-comment-object' | ||
9 | import { ViewObject } from './objects/view-object' | ||
10 | 10 | ||
11 | export type Activity = | 11 | export type Activity = |
12 | ActivityCreate | | 12 | ActivityCreate | |
@@ -53,7 +53,7 @@ export interface BaseActivity { | |||
53 | 53 | ||
54 | export interface ActivityCreate extends BaseActivity { | 54 | export interface ActivityCreate extends BaseActivity { |
55 | type: 'Create' | 55 | type: 'Create' |
56 | object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject | 56 | object: VideoTorrentObject | AbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject |
57 | } | 57 | } |
58 | 58 | ||
59 | export interface ActivityUpdate extends BaseActivity { | 59 | export interface ActivityUpdate extends BaseActivity { |
diff --git a/shared/models/activitypub/objects/video-abuse-object.ts b/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 @@ | |||
1 | import { ActivityFlagReasonObject } from './common-objects' | 1 | import { ActivityFlagReasonObject } from './common-objects' |
2 | 2 | ||
3 | export interface VideoAbuseObject { | 3 | export interface AbuseObject { |
4 | type: 'Flag' | 4 | type: 'Flag' |
5 | content: string | 5 | content: string |
6 | object: string | string[] | 6 | object: string | string[] |
7 | |||
7 | tag?: ActivityFlagReasonObject[] | 8 | tag?: ActivityFlagReasonObject[] |
9 | |||
8 | startAt?: number | 10 | startAt?: number |
9 | endAt?: number | 11 | endAt?: number |
10 | } | 12 | } |
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 @@ | |||
1 | import { VideoAbusePredefinedReasonsString } from '@shared/models/videos' | 1 | import { AbusePredefinedReasonsString } from '@shared/models' |
2 | 2 | ||
3 | export interface ActivityIdentifierObject { | 3 | export interface ActivityIdentifierObject { |
4 | identifier: string | 4 | identifier: string |
@@ -85,7 +85,7 @@ export interface ActivityMentionObject { | |||
85 | 85 | ||
86 | export interface ActivityFlagReasonObject { | 86 | export interface ActivityFlagReasonObject { |
87 | type: 'Hashtag' | 87 | type: 'Hashtag' |
88 | name: VideoAbusePredefinedReasonsString | 88 | name: AbusePredefinedReasonsString |
89 | } | 89 | } |
90 | 90 | ||
91 | export type ActivityTagObject = | 91 | 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 @@ | |||
1 | export * from './abuse-object' | ||
1 | export * from './cache-file-object' | 2 | export * from './cache-file-object' |
2 | export * from './common-objects' | 3 | export * from './common-objects' |
3 | export * from './video-abuse-object' | 4 | export * from './dislike-object' |
4 | export * from './video-torrent-object' | 5 | export * from './video-torrent-object' |
5 | export * from './view-object' | 6 | export * from './view-object' |
6 | 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 @@ | |||
1 | export * from './activitypub' | 1 | export * from './activitypub' |
2 | export * from './actors' | 2 | export * from './actors' |
3 | export * from './avatars' | 3 | export * from './avatars' |
4 | export * from './blocklist' | 4 | export * from './moderation' |
5 | export * from './bulk' | 5 | export * from './bulk' |
6 | export * from './redundancy' | 6 | export * from './redundancy' |
7 | export * from './users' | 7 | export * from './users' |
@@ -14,4 +14,3 @@ export * from './search' | |||
14 | export * from './server' | 14 | export * from './server' |
15 | export * from './oauth-client-local.model' | 15 | export * from './oauth-client-local.model' |
16 | export * from './result-list.model' | 16 | export * from './result-list.model' |
17 | 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..b0358dbb9 --- /dev/null +++ b/shared/models/moderation/abuse/abuse-create.model.ts | |||
@@ -0,0 +1,29 @@ | |||
1 | import { AbusePredefinedReasonsString } from './abuse-reason.model' | ||
2 | |||
3 | export interface AbuseCreate { | ||
4 | reason: string | ||
5 | |||
6 | predefinedReasons?: AbusePredefinedReasonsString[] | ||
7 | |||
8 | account?: { | ||
9 | id: number | ||
10 | } | ||
11 | |||
12 | video?: { | ||
13 | id: number | ||
14 | startAt?: number | ||
15 | endAt?: number | ||
16 | } | ||
17 | |||
18 | comment?: { | ||
19 | id: number | ||
20 | } | ||
21 | } | ||
22 | |||
23 | // FIXME: deprecated in 2.3. Remove it | ||
24 | export interface VideoAbuseCreate { | ||
25 | reason: string | ||
26 | predefinedReasons?: AbusePredefinedReasonsString[] | ||
27 | startAt?: number | ||
28 | endAt?: number | ||
29 | } | ||
diff --git a/shared/models/moderation/abuse/abuse-filter.type.ts b/shared/models/moderation/abuse/abuse-filter.type.ts new file mode 100644 index 000000000..7dafc6d77 --- /dev/null +++ b/shared/models/moderation/abuse/abuse-filter.type.ts | |||
@@ -0,0 +1 @@ | |||
export type AbuseFilter = 'video' | 'comment' | 'account' | |||
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 @@ | |||
1 | export enum AbusePredefinedReasons { | ||
2 | VIOLENT_OR_REPULSIVE = 1, | ||
3 | HATEFUL_OR_ABUSIVE, | ||
4 | SPAM_OR_MISLEADING, | ||
5 | PRIVACY, | ||
6 | RIGHTS, | ||
7 | SERVER_RULES, | ||
8 | THUMBNAILS, | ||
9 | CAPTIONS | ||
10 | } | ||
11 | |||
12 | export type AbusePredefinedReasonsString = | ||
13 | 'violentOrRepulsive' | | ||
14 | 'hatefulOrAbusive' | | ||
15 | 'spamOrMisleading' | | ||
16 | 'privacy' | | ||
17 | 'rights' | | ||
18 | 'serverRules' | | ||
19 | 'thumbnails' | | ||
20 | 'captions' | ||
21 | |||
22 | export const abusePredefinedReasonsMap: { | ||
23 | [key in AbusePredefinedReasonsString]: AbusePredefinedReasons | ||
24 | } = { | ||
25 | violentOrRepulsive: AbusePredefinedReasons.VIOLENT_OR_REPULSIVE, | ||
26 | hatefulOrAbusive: AbusePredefinedReasons.HATEFUL_OR_ABUSIVE, | ||
27 | spamOrMisleading: AbusePredefinedReasons.SPAM_OR_MISLEADING, | ||
28 | privacy: AbusePredefinedReasons.PRIVACY, | ||
29 | rights: AbusePredefinedReasons.RIGHTS, | ||
30 | serverRules: AbusePredefinedReasons.SERVER_RULES, | ||
31 | thumbnails: AbusePredefinedReasons.THUMBNAILS, | ||
32 | captions: AbusePredefinedReasons.CAPTIONS | ||
33 | } | ||
diff --git a/shared/models/videos/abuse/video-abuse-state.model.ts b/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 @@ | |||
1 | export enum VideoAbuseState { | 1 | export enum AbuseState { |
2 | PENDING = 1, | 2 | PENDING = 1, |
3 | REJECTED = 2, | 3 | REJECTED = 2, |
4 | ACCEPTED = 3 | 4 | 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 @@ | |||
1 | import { AbuseState } from './abuse-state.model' | ||
2 | |||
3 | export interface AbuseUpdate { | ||
4 | moderationComment?: string | ||
5 | |||
6 | state?: AbuseState | ||
7 | } | ||
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..0a0c6bd35 --- /dev/null +++ b/shared/models/moderation/abuse/abuse.model.ts | |||
@@ -0,0 +1,73 @@ | |||
1 | import { Account } from '../../actors/account.model' | ||
2 | import { AbuseState } from './abuse-state.model' | ||
3 | import { AbusePredefinedReasonsString } from './abuse-reason.model' | ||
4 | import { VideoConstant } from '../../videos/video-constant.model' | ||
5 | import { VideoChannel } from '../../videos/channel/video-channel.model' | ||
6 | |||
7 | export interface VideoAbuse { | ||
8 | id: number | ||
9 | name: string | ||
10 | uuid: string | ||
11 | nsfw: boolean | ||
12 | |||
13 | deleted: boolean | ||
14 | blacklisted: boolean | ||
15 | |||
16 | startAt: number | null | ||
17 | endAt: number | null | ||
18 | |||
19 | thumbnailPath?: string | ||
20 | channel?: VideoChannel | ||
21 | |||
22 | countReports: number | ||
23 | nthReport: number | ||
24 | } | ||
25 | |||
26 | export interface VideoCommentAbuse { | ||
27 | id: number | ||
28 | threadId: number | ||
29 | |||
30 | video: { | ||
31 | id: number | ||
32 | name: string | ||
33 | uuid: string | ||
34 | } | ||
35 | |||
36 | text: string | ||
37 | |||
38 | deleted: boolean | ||
39 | } | ||
40 | |||
41 | export interface Abuse { | ||
42 | id: number | ||
43 | |||
44 | reason: string | ||
45 | predefinedReasons?: AbusePredefinedReasonsString[] | ||
46 | |||
47 | reporterAccount: Account | ||
48 | flaggedAccount: Account | ||
49 | |||
50 | state: VideoConstant<AbuseState> | ||
51 | moderationComment?: string | ||
52 | |||
53 | video?: VideoAbuse | ||
54 | comment?: VideoCommentAbuse | ||
55 | |||
56 | createdAt: Date | ||
57 | updatedAt: Date | ||
58 | |||
59 | countReportsForReporter?: number | ||
60 | countReportsForReportee?: number | ||
61 | |||
62 | // FIXME: deprecated in 2.3, remove the following properties | ||
63 | |||
64 | // @deprecated | ||
65 | startAt?: null | ||
66 | // @deprecated | ||
67 | endAt?: null | ||
68 | |||
69 | // @deprecated | ||
70 | count?: number | ||
71 | // @deprecated | ||
72 | nth?: number | ||
73 | } | ||
diff --git a/shared/models/moderation/abuse/index.ts b/shared/models/moderation/abuse/index.ts new file mode 100644 index 000000000..55046426a --- /dev/null +++ b/shared/models/moderation/abuse/index.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | export * from './abuse-create.model' | ||
2 | export * from './abuse-filter.type' | ||
3 | export * from './abuse-reason.model' | ||
4 | export * from './abuse-state.model' | ||
5 | export * from './abuse-update.model' | ||
6 | export * from './abuse-video-is.type' | ||
7 | export * from './abuse.model' | ||
diff --git a/shared/models/blocklist/account-block.model.ts b/shared/models/moderation/account-block.model.ts index a942ed614..a942ed614 100644 --- a/shared/models/blocklist/account-block.model.ts +++ b/shared/models/moderation/account-block.model.ts | |||
diff --git a/shared/models/blocklist/index.ts b/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 @@ | |||
1 | export * from './abuse' | ||
1 | export * from './account-block.model' | 2 | export * from './account-block.model' |
2 | export * from './server-block.model' | 3 | export * from './server-block.model' |
diff --git a/shared/models/blocklist/server-block.model.ts b/shared/models/moderation/server-block.model.ts index a8b8af0b7..a8b8af0b7 100644 --- a/shared/models/blocklist/server-block.model.ts +++ b/shared/models/moderation/server-block.model.ts | |||
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts index 451f40d58..4e2230a76 100644 --- a/shared/models/users/user-notification-setting.model.ts +++ b/shared/models/users/user-notification-setting.model.ts | |||
@@ -7,7 +7,7 @@ export enum UserNotificationSettingValue { | |||
7 | export interface UserNotificationSetting { | 7 | export interface UserNotificationSetting { |
8 | newVideoFromSubscription: UserNotificationSettingValue | 8 | newVideoFromSubscription: UserNotificationSettingValue |
9 | newCommentOnMyVideo: UserNotificationSettingValue | 9 | newCommentOnMyVideo: UserNotificationSettingValue |
10 | videoAbuseAsModerator: UserNotificationSettingValue | 10 | abuseAsModerator: UserNotificationSettingValue |
11 | videoAutoBlacklistAsModerator: UserNotificationSettingValue | 11 | videoAutoBlacklistAsModerator: UserNotificationSettingValue |
12 | blacklistOnMyVideo: UserNotificationSettingValue | 12 | blacklistOnMyVideo: UserNotificationSettingValue |
13 | myVideoPublished: UserNotificationSettingValue | 13 | myVideoPublished: UserNotificationSettingValue |
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts index e9be1ca7f..5f7c33976 100644 --- a/shared/models/users/user-notification.model.ts +++ b/shared/models/users/user-notification.model.ts | |||
@@ -3,7 +3,7 @@ import { FollowState } from '../actors' | |||
3 | export enum UserNotificationType { | 3 | export enum UserNotificationType { |
4 | NEW_VIDEO_FROM_SUBSCRIPTION = 1, | 4 | NEW_VIDEO_FROM_SUBSCRIPTION = 1, |
5 | NEW_COMMENT_ON_MY_VIDEO = 2, | 5 | NEW_COMMENT_ON_MY_VIDEO = 2, |
6 | NEW_VIDEO_ABUSE_FOR_MODERATORS = 3, | 6 | NEW_ABUSE_FOR_MODERATORS = 3, |
7 | 7 | ||
8 | BLACKLIST_ON_MY_VIDEO = 4, | 8 | BLACKLIST_ON_MY_VIDEO = 4, |
9 | UNBLACKLIST_ON_MY_VIDEO = 5, | 9 | UNBLACKLIST_ON_MY_VIDEO = 5, |
@@ -64,9 +64,22 @@ export interface UserNotification { | |||
64 | video: VideoInfo | 64 | video: VideoInfo |
65 | } | 65 | } |
66 | 66 | ||
67 | videoAbuse?: { | 67 | abuse?: { |
68 | id: number | 68 | id: number |
69 | video: VideoInfo | 69 | |
70 | video?: VideoInfo | ||
71 | |||
72 | comment?: { | ||
73 | threadId: number | ||
74 | |||
75 | video: { | ||
76 | id: number | ||
77 | uuid: string | ||
78 | name: string | ||
79 | } | ||
80 | } | ||
81 | |||
82 | account?: ActorInfo | ||
70 | } | 83 | } |
71 | 84 | ||
72 | videoBlacklist?: { | 85 | 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 { | |||
11 | 11 | ||
12 | MANAGE_SERVER_REDUNDANCY, | 12 | MANAGE_SERVER_REDUNDANCY, |
13 | 13 | ||
14 | MANAGE_VIDEO_ABUSES, | 14 | MANAGE_ABUSES, |
15 | 15 | ||
16 | MANAGE_JOBS, | 16 | MANAGE_JOBS, |
17 | 17 | ||
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[] } = { | |||
20 | 20 | ||
21 | [UserRole.MODERATOR]: [ | 21 | [UserRole.MODERATOR]: [ |
22 | UserRight.MANAGE_VIDEO_BLACKLIST, | 22 | UserRight.MANAGE_VIDEO_BLACKLIST, |
23 | UserRight.MANAGE_VIDEO_ABUSES, | 23 | UserRight.MANAGE_ABUSES, |
24 | UserRight.REMOVE_ANY_VIDEO, | 24 | UserRight.REMOVE_ANY_VIDEO, |
25 | UserRight.REMOVE_ANY_VIDEO_CHANNEL, | 25 | UserRight.REMOVE_ANY_VIDEO_CHANNEL, |
26 | UserRight.REMOVE_ANY_VIDEO_PLAYLIST, | 26 | UserRight.REMOVE_ANY_VIDEO_PLAYLIST, |
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 6c959ceea..859736b2f 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -31,10 +31,13 @@ export interface User { | |||
31 | videoQuotaDaily: number | 31 | videoQuotaDaily: number |
32 | videoQuotaUsed?: number | 32 | videoQuotaUsed?: number |
33 | videoQuotaUsedDaily?: number | 33 | videoQuotaUsedDaily?: number |
34 | |||
34 | videosCount?: number | 35 | videosCount?: number |
35 | videoAbusesCount?: number | 36 | |
36 | videoAbusesAcceptedCount?: number | 37 | abusesCount?: number |
37 | videoAbusesCreatedCount?: number | 38 | abusesAcceptedCount?: number |
39 | abusesCreatedCount?: number | ||
40 | |||
38 | videoCommentsCount? : number | 41 | videoCommentsCount? : number |
39 | 42 | ||
40 | theme: string | 43 | theme: string |
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 @@ | |||
1 | export * from './video-abuse-create.model' | ||
2 | export * from './video-abuse-reason.model' | ||
3 | export * from './video-abuse-state.model' | ||
4 | export * from './video-abuse-update.model' | ||
5 | export * from './video-abuse-video-is.type' | ||
6 | 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 @@ | |||
1 | import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model' | ||
2 | |||
3 | export interface VideoAbuseCreate { | ||
4 | reason: string | ||
5 | predefinedReasons?: VideoAbusePredefinedReasonsString[] | ||
6 | startAt?: number | ||
7 | endAt?: number | ||
8 | } | ||
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 @@ | |||
1 | export enum VideoAbusePredefinedReasons { | ||
2 | VIOLENT_OR_REPULSIVE = 1, | ||
3 | HATEFUL_OR_ABUSIVE, | ||
4 | SPAM_OR_MISLEADING, | ||
5 | PRIVACY, | ||
6 | RIGHTS, | ||
7 | SERVER_RULES, | ||
8 | THUMBNAILS, | ||
9 | CAPTIONS | ||
10 | } | ||
11 | |||
12 | export type VideoAbusePredefinedReasonsString = | ||
13 | 'violentOrRepulsive' | | ||
14 | 'hatefulOrAbusive' | | ||
15 | 'spamOrMisleading' | | ||
16 | 'privacy' | | ||
17 | 'rights' | | ||
18 | 'serverRules' | | ||
19 | 'thumbnails' | | ||
20 | 'captions' | ||
21 | |||
22 | export const videoAbusePredefinedReasonsMap: { | ||
23 | [key in VideoAbusePredefinedReasonsString]: VideoAbusePredefinedReasons | ||
24 | } = { | ||
25 | violentOrRepulsive: VideoAbusePredefinedReasons.VIOLENT_OR_REPULSIVE, | ||
26 | hatefulOrAbusive: VideoAbusePredefinedReasons.HATEFUL_OR_ABUSIVE, | ||
27 | spamOrMisleading: VideoAbusePredefinedReasons.SPAM_OR_MISLEADING, | ||
28 | privacy: VideoAbusePredefinedReasons.PRIVACY, | ||
29 | rights: VideoAbusePredefinedReasons.RIGHTS, | ||
30 | serverRules: VideoAbusePredefinedReasons.SERVER_RULES, | ||
31 | thumbnails: VideoAbusePredefinedReasons.THUMBNAILS, | ||
32 | captions: VideoAbusePredefinedReasons.CAPTIONS | ||
33 | } | ||
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 @@ | |||
1 | import { VideoAbuseState } from './video-abuse-state.model' | ||
2 | |||
3 | export interface VideoAbuseUpdate { | ||
4 | moderationComment?: string | ||
5 | state?: VideoAbuseState | ||
6 | } | ||
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 @@ | |||
1 | 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 @@ | |||
1 | import { Account } from '../../actors/index' | ||
2 | import { VideoConstant } from '../video-constant.model' | ||
3 | import { VideoAbuseState } from './video-abuse-state.model' | ||
4 | import { VideoChannel } from '../channel/video-channel.model' | ||
5 | import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model' | ||
6 | |||
7 | export interface VideoAbuse { | ||
8 | id: number | ||
9 | reason: string | ||
10 | predefinedReasons?: VideoAbusePredefinedReasonsString[] | ||
11 | reporterAccount: Account | ||
12 | |||
13 | state: VideoConstant<VideoAbuseState> | ||
14 | moderationComment?: string | ||
15 | |||
16 | video: { | ||
17 | id: number | ||
18 | name: string | ||
19 | uuid: string | ||
20 | nsfw: boolean | ||
21 | deleted: boolean | ||
22 | blacklisted: boolean | ||
23 | thumbnailPath?: string | ||
24 | channel?: VideoChannel | ||
25 | } | ||
26 | |||
27 | createdAt: Date | ||
28 | updatedAt: Date | ||
29 | |||
30 | startAt: number | ||
31 | endAt: number | ||
32 | |||
33 | count?: number | ||
34 | nth?: number | ||
35 | |||
36 | countReportsForReporter?: number | ||
37 | countReportsForReportee?: number | ||
38 | } | ||
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 @@ | |||
1 | export * from './abuse' | ||
2 | export * from './blacklist' | 1 | export * from './blacklist' |
3 | export * from './caption' | 2 | export * from './caption' |
4 | export * from './channel' | 3 | export * from './channel' |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 79f75063f..a0d086324 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -106,9 +106,9 @@ tags: | |||
106 | Managing plugins installed from a local path or from NPM, or search for new ones. | 106 | Managing plugins installed from a local path or from NPM, or search for new ones. |
107 | externalDocs: | 107 | externalDocs: |
108 | url: https://docs.joinpeertube.org/#/api-plugins | 108 | url: https://docs.joinpeertube.org/#/api-plugins |
109 | - name: Video Abuses | 109 | - name: Abuses |
110 | description: | | 110 | description: | |
111 | Video abuses deal with reports of local or remote videos alike. | 111 | Abuses deal with reports of local or remote videos/comments/accounts alike. |
112 | - name: Video | 112 | - name: Video |
113 | description: | | 113 | description: | |
114 | Operations dealing with listing, uploading, fetching or modifying videos. | 114 | Operations dealing with listing, uploading, fetching or modifying videos. |
@@ -166,7 +166,7 @@ x-tagGroups: | |||
166 | - Search | 166 | - Search |
167 | - name: Moderation | 167 | - name: Moderation |
168 | tags: | 168 | tags: |
169 | - Video Abuses | 169 | - Abuses |
170 | - Video Blocks | 170 | - Video Blocks |
171 | - Account Blocks | 171 | - Account Blocks |
172 | - Server Blocks | 172 | - Server Blocks |
@@ -893,7 +893,7 @@ paths: | |||
893 | $ref: '#/components/schemas/NotificationSettingValue' | 893 | $ref: '#/components/schemas/NotificationSettingValue' |
894 | newCommentOnMyVideo: | 894 | newCommentOnMyVideo: |
895 | $ref: '#/components/schemas/NotificationSettingValue' | 895 | $ref: '#/components/schemas/NotificationSettingValue' |
896 | videoAbuseAsModerator: | 896 | abuseAsModerator: |
897 | $ref: '#/components/schemas/NotificationSettingValue' | 897 | $ref: '#/components/schemas/NotificationSettingValue' |
898 | videoAutoBlacklistAsModerator: | 898 | videoAutoBlacklistAsModerator: |
899 | $ref: '#/components/schemas/NotificationSettingValue' | 899 | $ref: '#/components/schemas/NotificationSettingValue' |
@@ -1471,16 +1471,15 @@ paths: | |||
1471 | description: HTTP or Torrent/magnetURI import not enabled | 1471 | description: HTTP or Torrent/magnetURI import not enabled |
1472 | '400': | 1472 | '400': |
1473 | description: '`magnetUri` or `targetUrl` or a torrent file missing' | 1473 | description: '`magnetUri` or `targetUrl` or a torrent file missing' |
1474 | /videos/abuse: | 1474 | /abuses: |
1475 | get: | 1475 | get: |
1476 | deprecated: true | 1476 | summary: List abuses |
1477 | summary: List video abuses | ||
1478 | security: | 1477 | security: |
1479 | - OAuth2: | 1478 | - OAuth2: |
1480 | - admin | 1479 | - admin |
1481 | - moderator | 1480 | - moderator |
1482 | tags: | 1481 | tags: |
1483 | - Video Abuses | 1482 | - Abuses |
1484 | parameters: | 1483 | parameters: |
1485 | - name: id | 1484 | - name: id |
1486 | in: query | 1485 | in: query |
@@ -1491,16 +1490,7 @@ paths: | |||
1491 | in: query | 1490 | in: query |
1492 | description: predefined reason the listed reports should contain | 1491 | description: predefined reason the listed reports should contain |
1493 | schema: | 1492 | schema: |
1494 | type: string | 1493 | $ref: '#/components/schemas/PredefinedAbuseReasons' |
1495 | enum: | ||
1496 | - violentOrAbusive | ||
1497 | - hatefulOrAbusive | ||
1498 | - spamOrMisleading | ||
1499 | - privacy | ||
1500 | - rights | ||
1501 | - serverRules | ||
1502 | - thumbnails | ||
1503 | - captions | ||
1504 | - name: search | 1494 | - name: search |
1505 | in: query | 1495 | in: query |
1506 | description: plain search that will match with video titles, reporter names and more | 1496 | description: plain search that will match with video titles, reporter names and more |
@@ -1508,7 +1498,7 @@ paths: | |||
1508 | type: string | 1498 | type: string |
1509 | - name: state | 1499 | - name: state |
1510 | in: query | 1500 | in: query |
1511 | description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)' | 1501 | description: 'The abuse state (Pending = `1`, Rejected = `2`, Accepted = `3`)' |
1512 | schema: | 1502 | schema: |
1513 | type: integer | 1503 | type: integer |
1514 | enum: | 1504 | enum: |
@@ -1535,6 +1525,23 @@ paths: | |||
1535 | description: only list reports of a specific video channel | 1525 | description: only list reports of a specific video channel |
1536 | schema: | 1526 | schema: |
1537 | type: string | 1527 | type: string |
1528 | - name: videoIs | ||
1529 | in: query | ||
1530 | description: only list blacklisted or deleted videos | ||
1531 | schema: | ||
1532 | type: string | ||
1533 | enum: | ||
1534 | - 'deleted' | ||
1535 | - 'blacklisted' | ||
1536 | - name: filter | ||
1537 | in: query | ||
1538 | description: only list account, comment or video reports | ||
1539 | schema: | ||
1540 | type: string | ||
1541 | enum: | ||
1542 | - 'video' | ||
1543 | - 'comment' | ||
1544 | - 'account' | ||
1538 | - $ref: '#/components/parameters/start' | 1545 | - $ref: '#/components/parameters/start' |
1539 | - $ref: '#/components/parameters/count' | 1546 | - $ref: '#/components/parameters/count' |
1540 | - $ref: '#/components/parameters/abusesSort' | 1547 | - $ref: '#/components/parameters/abusesSort' |
@@ -1547,17 +1554,13 @@ paths: | |||
1547 | type: array | 1554 | type: array |
1548 | items: | 1555 | items: |
1549 | $ref: '#/components/schemas/VideoAbuse' | 1556 | $ref: '#/components/schemas/VideoAbuse' |
1550 | '/videos/{id}/abuse': | 1557 | |
1551 | post: | 1558 | post: |
1552 | deprecated: true | ||
1553 | summary: Report an abuse | 1559 | summary: Report an abuse |
1554 | security: | 1560 | security: |
1555 | - OAuth2: [] | 1561 | - OAuth2: [] |
1556 | tags: | 1562 | tags: |
1557 | - Video Abuses | 1563 | - Abuses |
1558 | - Videos | ||
1559 | parameters: | ||
1560 | - $ref: '#/components/parameters/idOrUUID' | ||
1561 | requestBody: | 1564 | requestBody: |
1562 | required: true | 1565 | required: true |
1563 | content: | 1566 | content: |
@@ -1570,27 +1573,34 @@ paths: | |||
1570 | type: string | 1573 | type: string |
1571 | minLength: 4 | 1574 | minLength: 4 |
1572 | predefinedReasons: | 1575 | predefinedReasons: |
1573 | description: Reason categories that help triage reports | 1576 | $ref: '#/components/schemas/PredefinedAbuseReasons' |
1574 | type: array | 1577 | |
1575 | items: | 1578 | video: |
1576 | type: string | 1579 | type: object |
1577 | enum: | 1580 | properties: |
1578 | - violentOrAbusive | 1581 | id: |
1579 | - hatefulOrAbusive | 1582 | description: Video id to report |
1580 | - spamOrMisleading | 1583 | type: number |
1581 | - privacy | 1584 | startAt: |
1582 | - rights | 1585 | type: integer |
1583 | - serverRules | 1586 | description: Timestamp in the video that marks the beginning of the report |
1584 | - thumbnails | 1587 | minimum: 0 |
1585 | - captions | 1588 | endAt: |
1586 | startAt: | 1589 | type: integer |
1587 | type: integer | 1590 | description: Timestamp in the video that marks the ending of the report |
1588 | description: Timestamp in the video that marks the beginning of the report | 1591 | minimum: 0 |
1589 | minimum: 0 | 1592 | comment: |
1590 | endAt: | 1593 | type: object |
1591 | type: integer | 1594 | properties: |
1592 | description: Timestamp in the video that marks the ending of the report | 1595 | id: |
1593 | minimum: 0 | 1596 | description: Comment id to report |
1597 | type: number | ||
1598 | account: | ||
1599 | type: object | ||
1600 | properties: | ||
1601 | id: | ||
1602 | description: Account id to report | ||
1603 | type: number | ||
1594 | required: | 1604 | required: |
1595 | - reason | 1605 | - reason |
1596 | responses: | 1606 | responses: |
@@ -1598,18 +1608,16 @@ paths: | |||
1598 | description: successful operation | 1608 | description: successful operation |
1599 | '400': | 1609 | '400': |
1600 | description: incorrect request parameters | 1610 | description: incorrect request parameters |
1601 | '/videos/{id}/abuse/{abuseId}': | 1611 | '/abuses/{abuseId}': |
1602 | put: | 1612 | put: |
1603 | deprecated: true | ||
1604 | summary: Update an abuse | 1613 | summary: Update an abuse |
1605 | security: | 1614 | security: |
1606 | - OAuth2: | 1615 | - OAuth2: |
1607 | - admin | 1616 | - admin |
1608 | - moderator | 1617 | - moderator |
1609 | tags: | 1618 | tags: |
1610 | - Video Abuses | 1619 | - Abuses |
1611 | parameters: | 1620 | parameters: |
1612 | - $ref: '#/components/parameters/idOrUUID' | ||
1613 | - $ref: '#/components/parameters/abuseId' | 1621 | - $ref: '#/components/parameters/abuseId' |
1614 | requestBody: | 1622 | requestBody: |
1615 | content: | 1623 | content: |
@@ -1618,7 +1626,7 @@ paths: | |||
1618 | type: object | 1626 | type: object |
1619 | properties: | 1627 | properties: |
1620 | state: | 1628 | state: |
1621 | $ref: '#/components/schemas/VideoAbuseStateSet' | 1629 | $ref: '#/components/schemas/AbuseStateSet' |
1622 | moderationComment: | 1630 | moderationComment: |
1623 | type: string | 1631 | type: string |
1624 | description: Update the report comment visible only to the moderation team | 1632 | description: Update the report comment visible only to the moderation team |
@@ -1626,18 +1634,16 @@ paths: | |||
1626 | '204': | 1634 | '204': |
1627 | description: successful operation | 1635 | description: successful operation |
1628 | '404': | 1636 | '404': |
1629 | description: video abuse not found | 1637 | description: abuse not found |
1630 | delete: | 1638 | delete: |
1631 | deprecated: true | ||
1632 | tags: | 1639 | tags: |
1633 | - Video Abuses | 1640 | - Abuses |
1634 | summary: Delete an abuse | 1641 | summary: Delete an abuse |
1635 | security: | 1642 | security: |
1636 | - OAuth2: | 1643 | - OAuth2: |
1637 | - admin | 1644 | - admin |
1638 | - moderator | 1645 | - moderator |
1639 | parameters: | 1646 | parameters: |
1640 | - $ref: '#/components/parameters/idOrUUID' | ||
1641 | - $ref: '#/components/parameters/abuseId' | 1647 | - $ref: '#/components/parameters/abuseId' |
1642 | responses: | 1648 | responses: |
1643 | '204': | 1649 | '204': |
@@ -3320,7 +3326,7 @@ components: | |||
3320 | name: abuseId | 3326 | name: abuseId |
3321 | in: path | 3327 | in: path |
3322 | required: true | 3328 | required: true |
3323 | description: Video abuse id | 3329 | description: Abuse id |
3324 | schema: | 3330 | schema: |
3325 | type: integer | 3331 | type: integer |
3326 | captionLanguage: | 3332 | captionLanguage: |
@@ -3584,20 +3590,20 @@ components: | |||
3584 | label: | 3590 | label: |
3585 | type: string | 3591 | type: string |
3586 | 3592 | ||
3587 | VideoAbuseStateSet: | 3593 | AbuseStateSet: |
3588 | type: integer | 3594 | type: integer |
3589 | enum: | 3595 | enum: |
3590 | - 1 | 3596 | - 1 |
3591 | - 2 | 3597 | - 2 |
3592 | - 3 | 3598 | - 3 |
3593 | description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)' | 3599 | description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)' |
3594 | VideoAbuseStateConstant: | 3600 | AbuseStateConstant: |
3595 | properties: | 3601 | properties: |
3596 | id: | 3602 | id: |
3597 | $ref: '#/components/schemas/VideoAbuseStateSet' | 3603 | $ref: '#/components/schemas/AbuseStateSet' |
3598 | label: | 3604 | label: |
3599 | type: string | 3605 | type: string |
3600 | VideoAbusePredefinedReasons: | 3606 | AbusePredefinedReasons: |
3601 | type: array | 3607 | type: array |
3602 | items: | 3608 | items: |
3603 | type: string | 3609 | type: string |
@@ -3960,11 +3966,11 @@ components: | |||
3960 | type: string | 3966 | type: string |
3961 | example: The video is a spam | 3967 | example: The video is a spam |
3962 | predefinedReasons: | 3968 | predefinedReasons: |
3963 | $ref: '#/components/schemas/VideoAbusePredefinedReasons' | 3969 | $ref: '#/components/schemas/AbusePredefinedReasons' |
3964 | reporterAccount: | 3970 | reporterAccount: |
3965 | $ref: '#/components/schemas/Account' | 3971 | $ref: '#/components/schemas/Account' |
3966 | state: | 3972 | state: |
3967 | $ref: '#/components/schemas/VideoAbuseStateConstant' | 3973 | $ref: '#/components/schemas/AbuseStateConstant' |
3968 | moderationComment: | 3974 | moderationComment: |
3969 | type: string | 3975 | type: string |
3970 | example: Decided to ban the server since it spams us regularly | 3976 | example: Decided to ban the server since it spams us regularly |
@@ -4553,6 +4559,22 @@ components: | |||
4553 | updatedAt: | 4559 | updatedAt: |
4554 | type: string | 4560 | type: string |
4555 | format: date-time | 4561 | format: date-time |
4562 | |||
4563 | PredefinedAbuseReasons: | ||
4564 | description: Reason categories that help triage reports | ||
4565 | type: array | ||
4566 | items: | ||
4567 | type: string | ||
4568 | enum: | ||
4569 | - violentOrAbusive | ||
4570 | - hatefulOrAbusive | ||
4571 | - spamOrMisleading | ||
4572 | - privacy | ||
4573 | - rights | ||
4574 | - serverRules | ||
4575 | - thumbnails | ||
4576 | - captions | ||
4577 | |||
4556 | Job: | 4578 | Job: |
4557 | properties: | 4579 | properties: |
4558 | id: | 4580 | id: |
@@ -4690,11 +4712,11 @@ components: | |||
4690 | description: The user daily video quota | 4712 | description: The user daily video quota |
4691 | videosCount: | 4713 | videosCount: |
4692 | type: integer | 4714 | type: integer |
4693 | videoAbusesCount: | 4715 | abusesCount: |
4694 | type: integer | 4716 | type: integer |
4695 | videoAbusesAcceptedCount: | 4717 | abusesAcceptedCount: |
4696 | type: integer | 4718 | type: integer |
4697 | videoAbusesCreatedCount: | 4719 | abusesCreatedCount: |
4698 | type: integer | 4720 | type: integer |
4699 | videoCommentsCount: | 4721 | videoCommentsCount: |
4700 | type: integer | 4722 | type: integer |
@@ -5098,7 +5120,7 @@ components: | |||
5098 | 5120 | ||
5099 | - `2` NEW_COMMENT_ON_MY_VIDEO | 5121 | - `2` NEW_COMMENT_ON_MY_VIDEO |
5100 | 5122 | ||
5101 | - `3` NEW_VIDEO_ABUSE_FOR_MODERATORS | 5123 | - `3` NEW_ABUSE_FOR_MODERATORS |
5102 | 5124 | ||
5103 | - `4` BLACKLIST_ON_MY_VIDEO | 5125 | - `4` BLACKLIST_ON_MY_VIDEO |
5104 | 5126 | ||