aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-07-01 16:05:30 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-07-10 14:02:41 +0200
commitd95d15598847c7f020aa056e7e6e0c02d2bbf732 (patch)
treea8a593f1269688caf9e5f99559996f346290fec5
parent72493e44e9b455a04c4f093ed6c6ffa300b98d8b (diff)
downloadPeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.tar.gz
PeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.tar.zst
PeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.zip
Use 3 tables to represent abuses
-rw-r--r--client/src/app/+admin/admin.component.ts2
-rw-r--r--client/src/app/+admin/admin.module.ts10
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-details.component.html93
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts (renamed from client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts)22
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.html (renamed from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html)64
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss (renamed from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss)0
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts (renamed from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts)127
-rw-r--r--client/src/app/+admin/moderation/abuse-list/index.ts3
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html (renamed from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html)0
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss (renamed from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss)0
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts (renamed from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts)18
-rw-r--r--client/src/app/+admin/moderation/index.ts2
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts15
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/index.ts2
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html93
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts2
-rw-r--r--client/src/app/menu/menu.component.ts4
-rw-r--r--client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts (renamed from client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts)10
-rw-r--r--client/src/app/shared/shared-forms/form-validators/index.ts2
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts4
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts2
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts26
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html8
-rw-r--r--client/src/app/shared/shared-moderation/abuse.service.ts (renamed from client/src/app/shared/shared-moderation/video-abuse.service.ts)32
-rw-r--r--client/src/app/shared/shared-moderation/index.ts2
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts4
-rw-r--r--client/src/app/shared/shared-moderation/video-report.component.ts29
-rw-r--r--server/controllers/api/abuse.ts168
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/videos/abuse.ts109
-rw-r--r--server/helpers/audit-logger.ts27
-rw-r--r--server/helpers/custom-validators/abuses.ts54
-rw-r--r--server/helpers/custom-validators/activitypub/flag.ts4
-rw-r--r--server/helpers/custom-validators/video-abuses.ts56
-rw-r--r--server/helpers/middlewares/abuses.ts (renamed from server/helpers/middlewares/video-abuses.ts)18
-rw-r--r--server/helpers/middlewares/index.ts2
-rw-r--r--server/initializers/constants.ts27
-rw-r--r--server/initializers/database.ts41
-rw-r--r--server/initializers/migrations/0250-video-abuse-state.ts4
-rw-r--r--server/lib/activitypub/process/process-flag.ts117
-rw-r--r--server/lib/activitypub/send/send-flag.ts31
-rw-r--r--server/lib/activitypub/url.ts10
-rw-r--r--server/lib/emailer.ts110
-rw-r--r--server/lib/emails/account-abuse-new/html.pug14
-rw-r--r--server/lib/emails/common/mixins.pug6
-rw-r--r--server/lib/emails/video-abuse-new/html.pug8
-rw-r--r--server/lib/emails/video-comment-abuse-new/html.pug15
-rw-r--r--server/lib/moderation.ts164
-rw-r--r--server/lib/notifier.ts43
-rw-r--r--server/middlewares/validators/abuse.ts253
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/sort.ts6
-rw-r--r--server/middlewares/validators/videos/index.ts1
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts135
-rw-r--r--server/models/abuse/abuse.ts (renamed from server/models/video/video-abuse.ts)327
-rw-r--r--server/models/abuse/video-abuse.ts63
-rw-r--r--server/models/abuse/video-comment-abuse.ts53
-rw-r--r--server/models/account/account-blocklist.ts10
-rw-r--r--server/models/account/account.ts4
-rw-r--r--server/models/account/user-notification.ts100
-rw-r--r--server/models/account/user.ts4
-rw-r--r--server/models/server/server-blocklist.ts10
-rw-r--r--server/models/video/video.ts84
-rw-r--r--server/tests/api/check-params/video-abuses.ts9
-rw-r--r--server/tests/api/users/users.ts4
-rw-r--r--server/tests/api/videos/video-abuse.ts54
-rw-r--r--server/types/models/index.ts1
-rw-r--r--server/types/models/moderation/abuse.ts97
-rw-r--r--server/types/models/moderation/index.ts1
-rw-r--r--server/types/models/user/user-notification.ts32
-rw-r--r--server/types/models/video/index.ts1
-rw-r--r--server/types/models/video/video-abuse.ts35
-rw-r--r--server/typings/express/index.d.ts4
-rw-r--r--shared/extra-utils/index.ts1
-rw-r--r--shared/extra-utils/moderation/abuses.ts112
-rw-r--r--shared/extra-utils/users/user-notifications.ts6
-rw-r--r--shared/extra-utils/videos/video-abuses.ts18
-rw-r--r--shared/models/activitypub/activity.ts10
-rw-r--r--shared/models/activitypub/objects/abuse-object.ts (renamed from shared/models/activitypub/objects/video-abuse-object.ts)4
-rw-r--r--shared/models/activitypub/objects/common-objects.ts4
-rw-r--r--shared/models/activitypub/objects/index.ts4
-rw-r--r--shared/models/index.ts3
-rw-r--r--shared/models/moderation/abuse/abuse-create.model.ts26
-rw-r--r--shared/models/moderation/abuse/abuse-filter.ts1
-rw-r--r--shared/models/moderation/abuse/abuse-reason.model.ts33
-rw-r--r--shared/models/moderation/abuse/abuse-state.model.ts (renamed from shared/models/videos/abuse/video-abuse-state.model.ts)2
-rw-r--r--shared/models/moderation/abuse/abuse-update.model.ts7
-rw-r--r--shared/models/moderation/abuse/abuse-video-is.type.ts1
-rw-r--r--shared/models/moderation/abuse/abuse.model.ts53
-rw-r--r--shared/models/moderation/abuse/index.ts6
-rw-r--r--shared/models/moderation/account-block.model.ts (renamed from shared/models/blocklist/account-block.model.ts)0
-rw-r--r--shared/models/moderation/index.ts (renamed from shared/models/blocklist/index.ts)1
-rw-r--r--shared/models/moderation/server-block.model.ts (renamed from shared/models/blocklist/server-block.model.ts)0
-rw-r--r--shared/models/users/user-notification.model.ts15
-rw-r--r--shared/models/users/user-right.enum.ts2
-rw-r--r--shared/models/users/user-role.ts2
-rw-r--r--shared/models/videos/abuse/index.ts6
-rw-r--r--shared/models/videos/abuse/video-abuse-create.model.ts8
-rw-r--r--shared/models/videos/abuse/video-abuse-reason.model.ts33
-rw-r--r--shared/models/videos/abuse/video-abuse-update.model.ts6
-rw-r--r--shared/models/videos/abuse/video-abuse-video-is.type.ts1
-rw-r--r--shared/models/videos/abuse/video-abuse.model.ts38
-rw-r--r--shared/models/videos/index.ts1
103 files changed, 2137 insertions, 1162 deletions
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index 6f340884f..1e137e63e 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -91,7 +91,7 @@ export class AdminComponent implements OnInit {
91 } 91 }
92 92
93 hasVideoAbusesRight () { 93 hasVideoAbusesRight () {
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
14import { FollowingListComponent } from './follows/following-list/following-list.component' 14import { FollowingListComponent } from './follows/following-list/following-list.component'
15import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' 15import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
16import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' 16import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
17import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlockListComponent } from './moderation' 17import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation'
18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' 18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
19import { ModerationComponent } from './moderation/moderation.component' 19import { ModerationComponent } from './moderation/moderation.component'
20import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component' 20import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component'
21import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' 21import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
22import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' 22import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
23import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' 23import { 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..d031ea8ed
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
@@ -0,0 +1,93 @@
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/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" class="chip">
10 <img
11 class="avatar"
12 [src]="abuse.reporterAccount.avatar?.path"
13 (error)="switchToDefaultAvatar($event)"
14 alt="Avatar"
15 >
16 <div>
17 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
18 </div>
19 </a>
20 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
21 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.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/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.video.channel.ownerAccount.displayName + '&quot;' }" class="chip">
30 <img
31 class="avatar"
32 [src]="abuse.video.channel.ownerAccount?.avatar?.path"
33 (error)="switchToDefaultAvatar($event)"
34 alt="Avatar"
35 >
36 <div>
37 <span class="text-muted">{{ abuse.video.channel.ownerAccount ? abuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
38 </div>
39 </a>
40 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.video.channel.ownerAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
41 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
42 </a>
43 </span>
44 </div>
45
46 <div class="d-flex" *ngIf="abuse.updatedAt">
47 <span class="col-3 moderation-expanded-label" i18n>Updated</span>
48 <time class="col-9 moderation-expanded-text video-details-date-updated">{{ abuse.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/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
56 </span>
57 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.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/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="abuse.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="abuse.endAt"> - {{ endAt }}</ng-container>
73 </span>
74 </div>
75
76 <div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
77 <span class="col-3 moderation-expanded-label" i18n>Note</span>
78 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.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="abuse.video.deleted || abuse.video.blacklisted">
87 <span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
88 <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
89 </div>
90 <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
91 </div>
92 </div>
93</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..8f87630b8 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
@@ -1,19 +1,19 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { Actor } from '@app/shared/shared-main' 2import { Actor } from '@app/shared/shared-main'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model' 4import { AbusePredefinedReasonsString } from '@shared/models'
5import { ProcessedVideoAbuse } from './video-abuse-list.component' 5import { ProcessedAbuse } from './abuse-list.component'
6import { durationToString } from '@app/helpers' 6import { 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})
13export class VideoAbuseDetailsComponent { 13export 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,16 @@ export class VideoAbuseDetailsComponent {
31 } 31 }
32 32
33 get startAt () { 33 get startAt () {
34 return durationToString(this.videoAbuse.startAt) 34 return durationToString(this.abuse.startAt)
35 } 35 }
36 36
37 get endAt () { 37 get endAt () {
38 return durationToString(this.videoAbuse.endAt) 38 return durationToString(this.abuse.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 return this.abuse.predefinedReasons.map(r => ({
44 id: r, 44 id: r,
45 label: this.predefinedReasonsTranslations[r] 45 label: this.predefinedReasonsTranslations[r]
46 })) 46 }))
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
index 64641b28a..167f32fe6 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
@@ -1,5 +1,5 @@
1<p-table 1<p-table
2 [value]="videoAbuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" 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" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports" 5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
@@ -16,11 +16,11 @@
16 16
17 <div role="menu" ngbDropdownMenu> 17 <div role="menu" ngbDropdownMenu>
18 <h6 class="dropdown-header" i18n>Advanced report filters</h6> 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> 19 <a [routerLink]="[ '/admin/moderation/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> 20 <a [routerLink]="[ '/admin/moderation/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> 21 <a [routerLink]="[ '/admin/moderation/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> 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/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a> 23 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
24 </div> 24 </div>
25 </div> 25 </div>
26 <input 26 <input
@@ -45,91 +45,91 @@
45 </tr> 45 </tr>
46 </ng-template> 46 </ng-template>
47 47
48 <ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse> 48 <ng-template pTemplate="body" let-expanded="expanded" let-abuse>
49 <tr> 49 <tr>
50 <td class="c-hand" [pRowToggler]="videoAbuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> 50 <td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
51 <span class="expander"> 51 <span class="expander">
52 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> 52 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
53 </span> 53 </span>
54 </td> 54 </td>
55 55
56 <td> 56 <td>
57 <a [href]="videoAbuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 57 <a [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
58 <div class="chip two-lines"> 58 <div class="chip two-lines">
59 <img 59 <img
60 class="avatar" 60 class="avatar"
61 [src]="videoAbuse.reporterAccount.avatar?.path" 61 [src]="abuse.reporterAccount.avatar?.path"
62 (error)="switchToDefaultAvatar($event)" 62 (error)="switchToDefaultAvatar($event)"
63 alt="Avatar" 63 alt="Avatar"
64 > 64 >
65 <div> 65 <div>
66 {{ videoAbuse.reporterAccount.displayName }} 66 {{ abuse.reporterAccount.displayName }}
67 <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span> 67 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
68 </div> 68 </div>
69 </div> 69 </div>
70 </a> 70 </a>
71 </td> 71 </td>
72 72
73 <td *ngIf="!videoAbuse.video.deleted"> 73 <td *ngIf="!abuse.video.deleted">
74 <a [href]="getVideoUrl(videoAbuse)" class="video-table-video-link" [title]="videoAbuse.video.name" target="_blank" rel="noopener noreferrer"> 74 <a [href]="getVideoUrl(abuse)" class="video-table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
75 <div class="video-table-video"> 75 <div class="video-table-video">
76 <div class="video-table-video-image"> 76 <div class="video-table-video-image">
77 <img [src]="videoAbuse.video.thumbnailPath"> 77 <img [src]="abuse.video.thumbnailPath">
78 <span 78 <span
79 class="video-table-video-image-label" *ngIf="videoAbuse.count > 1" 79 class="video-table-video-image-label" *ngIf="abuse.count > 1"
80 i18n-title title="This video has been reported multiple times." 80 i18n-title title="This video has been reported multiple times."
81 > 81 >
82 {{ videoAbuse.nth }}/{{ videoAbuse.count }} 82 {{ abuse.nth }}/{{ abuse.count }}
83 </span> 83 </span>
84 </div> 84 </div>
85 <div class="video-table-video-text"> 85 <div class="video-table-video-text">
86 <div> 86 <div>
87 <span *ngIf="!videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span> 87 <span *ngIf="!abuse.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> 88 <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
89 {{ videoAbuse.video.name }} 89 {{ abuse.video.name }}
90 </div> 90 </div>
91 <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div> 91 <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
92 </div> 92 </div>
93 </div> 93 </div>
94 </a> 94 </a>
95 </td> 95 </td>
96 96
97 <td *ngIf="videoAbuse.video.deleted" class="c-hand" [pRowToggler]="videoAbuse"> 97 <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
98 <div class="video-table-video" i18n-title title="Video was deleted"> 98 <div class="video-table-video" i18n-title title="Video was deleted">
99 <div class="video-table-video-image"> 99 <div class="video-table-video-image">
100 <span i18n>Deleted</span> 100 <span i18n>Deleted</span>
101 </div> 101 </div>
102 <div class="video-table-video-text"> 102 <div class="video-table-video-text">
103 <div> 103 <div>
104 {{ videoAbuse.video.name }} 104 {{ abuse.video.name }}
105 <span class="glyphicon glyphicon-trash"></span> 105 <span class="glyphicon glyphicon-trash"></span>
106 </div> 106 </div>
107 <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div> 107 <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
108 </div> 108 </div>
109 </div> 109 </div>
110 </td> 110 </td>
111 111
112 <td class="c-hand" [pRowToggler]="videoAbuse">{{ videoAbuse.createdAt | date: 'short' }}</td> 112 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
113 113
114 <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse"> 114 <td class="c-hand video-abuse-states" [pRowToggler]="abuse">
115 <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> 115 <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
116 <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span> 116 <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.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> 117 <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
118 </td> 118 </td>
119 119
120 <td class="action-cell"> 120 <td class="action-cell">
121 <my-action-dropdown 121 <my-action-dropdown
122 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" 122 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
123 i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse" 123 i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
124 ></my-action-dropdown> 124 ></my-action-dropdown>
125 </td> 125 </td>
126 </tr> 126 </tr>
127 </ng-template> 127 </ng-template>
128 128
129 <ng-template pTemplate="rowexpansion" let-videoAbuse> 129 <ng-template pTemplate="rowexpansion" let-abuse>
130 <tr> 130 <tr>
131 <td class="expand-cell" colspan="6"> 131 <td class="expand-cell" colspan="6">
132 <my-video-abuse-details [videoAbuse]="videoAbuse"></my-video-abuse-details> 132 <my-abuse-details [abuse]="abuse"></my-abuse-details>
133 </td> 133 </td>
134 </tr> 134 </tr>
135 </ng-template> 135 </ng-template>
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..8eee15b64 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
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
index 409dd42c7..427ec4d5d 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
@@ -1,5 +1,4 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { filter } from 'rxjs/operators'
3import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' 2import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
4import { environment } from 'src/environments/environment' 3import { environment } from 'src/environments/environment'
5import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' 4import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
@@ -7,43 +6,45 @@ import { DomSanitizer } from '@angular/platform-browser'
7import { ActivatedRoute, Params, Router } from '@angular/router' 6import { ActivatedRoute, Params, Router } from '@angular/router'
8import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' 7import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
9import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' 8import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
10import { BlocklistService, VideoAbuseService, VideoBlockService } from '@app/shared/shared-moderation' 9import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
11import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
12import { VideoAbuse, VideoAbuseState } from '@shared/models' 11import { Abuse, AbuseState } from '@shared/models'
13import { ModerationCommentModalComponent } from './moderation-comment-modal.component' 12import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
14 13
15export type ProcessedVideoAbuse = VideoAbuse & { 14export type ProcessedAbuse = Abuse & {
16 moderationCommentHtml?: string, 15 moderationCommentHtml?: string,
17 reasonHtml?: string 16 reasonHtml?: string
18 embedHtml?: string 17 embedHtml?: string
19 updatedAt?: Date 18 updatedAt?: Date
19
20 // override bare server-side definitions with rich client-side definitions 20 // override bare server-side definitions with rich client-side definitions
21 reporterAccount: Account 21 reporterAccount: Account
22 video: VideoAbuse['video'] & { 22
23 channel: VideoAbuse['video']['channel'] & { 23 video: Abuse['video'] & {
24 channel: Abuse['video']['channel'] & {
24 ownerAccount: Account 25 ownerAccount: Account
25 } 26 }
26 } 27 }
27} 28}
28 29
29@Component({ 30@Component({
30 selector: 'my-video-abuse-list', 31 selector: 'my-abuse-list',
31 templateUrl: './video-abuse-list.component.html', 32 templateUrl: './abuse-list.component.html',
32 styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ] 33 styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ]
33}) 34})
34export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit { 35export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
35 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent 36 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
36 37
37 videoAbuses: ProcessedVideoAbuse[] = [] 38 abuses: ProcessedAbuse[] = []
38 totalRecords = 0 39 totalRecords = 0
39 sort: SortMeta = { field: 'createdAt', order: 1 } 40 sort: SortMeta = { field: 'createdAt', order: 1 }
40 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 41 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
41 42
42 videoAbuseActions: DropdownAction<VideoAbuse>[][] = [] 43 abuseActions: DropdownAction<Abuse>[][] = []
43 44
44 constructor ( 45 constructor (
45 private notifier: Notifier, 46 private notifier: Notifier,
46 private videoAbuseService: VideoAbuseService, 47 private abuseService: AbuseService,
47 private blocklistService: BlocklistService, 48 private blocklistService: BlocklistService,
48 private videoService: VideoService, 49 private videoService: VideoService,
49 private videoBlocklistService: VideoBlockService, 50 private videoBlocklistService: VideoBlockService,
@@ -56,7 +57,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
56 ) { 57 ) {
57 super() 58 super()
58 59
59 this.videoAbuseActions = [ 60 this.abuseActions = [
60 [ 61 [
61 { 62 {
62 label: this.i18n('Internal actions'), 63 label: this.i18n('Internal actions'),
@@ -64,45 +65,45 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
64 }, 65 },
65 { 66 {
66 label: this.i18n('Delete report'), 67 label: this.i18n('Delete report'),
67 handler: videoAbuse => this.removeVideoAbuse(videoAbuse) 68 handler: abuse => this.removeAbuse(abuse)
68 }, 69 },
69 { 70 {
70 label: this.i18n('Add note'), 71 label: this.i18n('Add note'),
71 handler: videoAbuse => this.openModerationCommentModal(videoAbuse), 72 handler: abuse => this.openModerationCommentModal(abuse),
72 isDisplayed: videoAbuse => !videoAbuse.moderationComment 73 isDisplayed: abuse => !abuse.moderationComment
73 }, 74 },
74 { 75 {
75 label: this.i18n('Update note'), 76 label: this.i18n('Update note'),
76 handler: videoAbuse => this.openModerationCommentModal(videoAbuse), 77 handler: abuse => this.openModerationCommentModal(abuse),
77 isDisplayed: videoAbuse => !!videoAbuse.moderationComment 78 isDisplayed: abuse => !!abuse.moderationComment
78 }, 79 },
79 { 80 {
80 label: this.i18n('Mark as accepted'), 81 label: this.i18n('Mark as accepted'),
81 handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED), 82 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
82 isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse) 83 isDisplayed: abuse => !this.isAbuseAccepted(abuse)
83 }, 84 },
84 { 85 {
85 label: this.i18n('Mark as rejected'), 86 label: this.i18n('Mark as rejected'),
86 handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED), 87 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
87 isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse) 88 isDisplayed: abuse => !this.isAbuseRejected(abuse)
88 } 89 }
89 ], 90 ],
90 [ 91 [
91 { 92 {
92 label: this.i18n('Actions for the video'), 93 label: this.i18n('Actions for the video'),
93 isHeader: true, 94 isHeader: true,
94 isDisplayed: videoAbuse => !videoAbuse.video.deleted 95 isDisplayed: abuse => !abuse.video.deleted
95 }, 96 },
96 { 97 {
97 label: this.i18n('Block video'), 98 label: this.i18n('Block video'),
98 isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted, 99 isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted,
99 handler: videoAbuse => { 100 handler: abuse => {
100 this.videoBlocklistService.blockVideo(videoAbuse.video.id, undefined, true) 101 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
101 .subscribe( 102 .subscribe(
102 () => { 103 () => {
103 this.notifier.success(this.i18n('Video blocked.')) 104 this.notifier.success(this.i18n('Video blocked.'))
104 105
105 this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) 106 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
106 }, 107 },
107 108
108 err => this.notifier.error(err.message) 109 err => this.notifier.error(err.message)
@@ -111,14 +112,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
111 }, 112 },
112 { 113 {
113 label: this.i18n('Unblock video'), 114 label: this.i18n('Unblock video'),
114 isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted, 115 isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted,
115 handler: videoAbuse => { 116 handler: abuse => {
116 this.videoBlocklistService.unblockVideo(videoAbuse.video.id) 117 this.videoBlocklistService.unblockVideo(abuse.video.id)
117 .subscribe( 118 .subscribe(
118 () => { 119 () => {
119 this.notifier.success(this.i18n('Video unblocked.')) 120 this.notifier.success(this.i18n('Video unblocked.'))
120 121
121 this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) 122 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
122 }, 123 },
123 124
124 err => this.notifier.error(err.message) 125 err => this.notifier.error(err.message)
@@ -127,20 +128,20 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
127 }, 128 },
128 { 129 {
129 label: this.i18n('Delete video'), 130 label: this.i18n('Delete video'),
130 isDisplayed: videoAbuse => !videoAbuse.video.deleted, 131 isDisplayed: abuse => !abuse.video.deleted,
131 handler: async videoAbuse => { 132 handler: async abuse => {
132 const res = await this.confirmService.confirm( 133 const res = await this.confirmService.confirm(
133 this.i18n('Do you really want to delete this video?'), 134 this.i18n('Do you really want to delete this video?'),
134 this.i18n('Delete') 135 this.i18n('Delete')
135 ) 136 )
136 if (res === false) return 137 if (res === false) return
137 138
138 this.videoService.removeVideo(videoAbuse.video.id) 139 this.videoService.removeVideo(abuse.video.id)
139 .subscribe( 140 .subscribe(
140 () => { 141 () => {
141 this.notifier.success(this.i18n('Video deleted.')) 142 this.notifier.success(this.i18n('Video deleted.'))
142 143
143 this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) 144 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
144 }, 145 },
145 146
146 err => this.notifier.error(err.message) 147 err => this.notifier.error(err.message)
@@ -155,8 +156,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
155 }, 156 },
156 { 157 {
157 label: this.i18n('Mute reporter'), 158 label: this.i18n('Mute reporter'),
158 handler: async videoAbuse => { 159 handler: async abuse => {
159 const account = videoAbuse.reporterAccount as Account 160 const account = abuse.reporterAccount as Account
160 161
161 this.blocklistService.blockAccountByInstance(account) 162 this.blocklistService.blockAccountByInstance(account)
162 .subscribe( 163 .subscribe(
@@ -174,13 +175,13 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
174 }, 175 },
175 { 176 {
176 label: this.i18n('Mute server'), 177 label: this.i18n('Mute server'),
177 isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId, 178 isDisplayed: abuse => !abuse.reporterAccount.userId,
178 handler: async videoAbuse => { 179 handler: async abuse => {
179 this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host) 180 this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
180 .subscribe( 181 .subscribe(
181 () => { 182 () => {
182 this.notifier.success( 183 this.notifier.success(
183 this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host }) 184 this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host })
184 ) 185 )
185 }, 186 },
186 187
@@ -209,11 +210,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
209 } 210 }
210 211
211 getIdentifier () { 212 getIdentifier () {
212 return 'VideoAbuseListComponent' 213 return 'AbuseListComponent'
213 } 214 }
214 215
215 openModerationCommentModal (videoAbuse: VideoAbuse) { 216 openModerationCommentModal (abuse: Abuse) {
216 this.moderationCommentModal.openModal(videoAbuse) 217 this.moderationCommentModal.openModal(abuse)
217 } 218 }
218 219
219 onModerationCommentUpdated () { 220 onModerationCommentUpdated () {
@@ -240,26 +241,26 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
240 } 241 }
241 /* END Table filter functions */ 242 /* END Table filter functions */
242 243
243 isVideoAbuseAccepted (videoAbuse: VideoAbuse) { 244 isAbuseAccepted (abuse: Abuse) {
244 return videoAbuse.state.id === VideoAbuseState.ACCEPTED 245 return abuse.state.id === AbuseState.ACCEPTED
245 } 246 }
246 247
247 isVideoAbuseRejected (videoAbuse: VideoAbuse) { 248 isAbuseRejected (abuse: Abuse) {
248 return videoAbuse.state.id === VideoAbuseState.REJECTED 249 return abuse.state.id === AbuseState.REJECTED
249 } 250 }
250 251
251 getVideoUrl (videoAbuse: VideoAbuse) { 252 getVideoUrl (abuse: Abuse) {
252 return Video.buildClientUrl(videoAbuse.video.uuid) 253 return Video.buildClientUrl(abuse.video.uuid)
253 } 254 }
254 255
255 getVideoEmbed (videoAbuse: VideoAbuse) { 256 getVideoEmbed (abuse: Abuse) {
256 return buildVideoEmbed( 257 return buildVideoEmbed(
257 buildVideoLink({ 258 buildVideoLink({
258 baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`, 259 baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
259 title: false, 260 title: false,
260 warningTitle: false, 261 warningTitle: false,
261 startTime: videoAbuse.startAt, 262 startTime: abuse.startAt,
262 stopTime: videoAbuse.endAt 263 stopTime: abuse.endAt
263 }) 264 })
264 ) 265 )
265 } 266 }
@@ -268,11 +269,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
268 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() 269 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
269 } 270 }
270 271
271 async removeVideoAbuse (videoAbuse: VideoAbuse) { 272 async removeAbuse (abuse: Abuse) {
272 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) 273 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 if (res === false) return
274 275
275 this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( 276 this.abuseService.removeAbuse(abuse).subscribe(
276 () => { 277 () => {
277 this.notifier.success(this.i18n('Abuse deleted.')) 278 this.notifier.success(this.i18n('Abuse deleted.'))
278 this.loadData() 279 this.loadData()
@@ -282,8 +283,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
282 ) 283 )
283 } 284 }
284 285
285 updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) { 286 updateAbuseState (abuse: Abuse, state: AbuseState) {
286 this.videoAbuseService.updateVideoAbuse(videoAbuse, { state }) 287 this.abuseService.updateAbuse(abuse, { state })
287 .subscribe( 288 .subscribe(
288 () => this.loadData(), 289 () => this.loadData(),
289 290
@@ -292,14 +293,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
292 } 293 }
293 294
294 protected loadData () { 295 protected loadData () {
295 return this.videoAbuseService.getVideoAbuses({ 296 return this.abuseService.getAbuses({
296 pagination: this.pagination, 297 pagination: this.pagination,
297 sort: this.sort, 298 sort: this.sort,
298 search: this.search 299 search: this.search
299 }).subscribe( 300 }).subscribe(
300 async resultList => { 301 async resultList => {
301 this.totalRecords = resultList.total 302 this.totalRecords = resultList.total
302 const videoAbuses = [] 303 const abuses = []
303 304
304 for (const abuse of resultList.data) { 305 for (const abuse of resultList.data) {
305 Object.assign(abuse, { 306 Object.assign(abuse, {
@@ -312,10 +313,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
312 if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) 313 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 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
314 315
315 videoAbuses.push(abuse as ProcessedVideoAbuse) 316 abuses.push(abuse as ProcessedAbuse)
316 } 317 }
317 318
318 this.videoAbuses = videoAbuses 319 this.abuses = abuses
319 }, 320 },
320 321
321 err => this.notifier.error(err.message) 322 err => this.notifier.error(err.message)
diff --git a/client/src/app/+admin/moderation/abuse-list/index.ts b/client/src/app/+admin/moderation/abuse-list/index.ts
new file mode 100644
index 000000000..c6037dab4
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/index.ts
@@ -0,0 +1,3 @@
1export * from './abuse-details.component'
2export * from './abuse-list.component'
3export * 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 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms' 3import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
4import { VideoAbuseService } from '@app/shared/shared-moderation' 4import { AbuseService } from '@app/shared/shared-moderation'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { VideoAbuse } from '@shared/models' 8import { 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 @@
1export * from './abuse-list'
1export * from './instance-blocklist' 2export * from './instance-blocklist'
2export * from './video-abuse-list'
3export * from './video-block-list' 3export * from './video-block-list'
4export * from './moderation.component' 4export * from './moderation.component'
5export * from './moderation.routes' 5export * from './moderation.routes'
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index cd837bcb9..1e207e5e8 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -1,7 +1,7 @@
1import { Routes } from '@angular/router' 1import { Routes } from '@angular/router'
2import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' 2import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
3import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 3import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' 4import { AbuseListComponent } from '@app/+admin/moderation/abuse-list'
5import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' 5import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
6import { UserRightGuard } from '@app/core' 6import { UserRightGuard } from '@app/core'
7import { UserRight } from '@shared/models' 7import { UserRight } from '@shared/models'
@@ -13,20 +13,25 @@ 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: 'Video reports'
32 } 37 }
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 @@
1export * from './video-abuse-list.component'
2export * 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:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" 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:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" 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:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" 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:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" 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/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index cfa514b26..adc18b587 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -47,7 +47,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
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 videoAbuseAsModerator: 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/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'
4import { BuildFormValidator } from './form-validator.service' 4import { BuildFormValidator } from './form-validator.service'
5 5
6@Injectable() 6@Injectable()
7export class VideoAbuseValidatorsService { 7export 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 @@
1export * from './abuse-validators.service'
1export * from './batch-domains-validators.service' 2export * from './batch-domains-validators.service'
2export * from './custom-config-validators.service' 3export * from './custom-config-validators.service'
3export * from './form-validator.service' 4export * from './form-validator.service'
@@ -6,7 +7,6 @@ export * from './instance-validators.service'
6export * from './login-validators.service' 7export * from './login-validators.service'
7export * from './reset-password-validators.service' 8export * from './reset-password-validators.service'
8export * from './user-validators.service' 9export * from './user-validators.service'
9export * from './video-abuse-validators.service'
10export * from './video-accept-ownership-validators.service' 10export * from './video-accept-ownership-validators.service'
11export * from './video-block-validators.service' 11export * from './video-block-validators.service'
12export * from './video-captions-validators.service' 12export * 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..0fa161ce6 100644
--- a/client/src/app/shared/shared-main/account/actor.model.ts
+++ b/client/src/app/shared/shared-main/account/actor.model.ts
@@ -14,7 +14,7 @@ export abstract class Actor implements ActorServer {
14 14
15 avatarUrl: string 15 avatarUrl: string
16 16
17 static GET_ACTOR_AVATAR_URL (actor: { avatar?: Avatar }) { 17 static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
18 if (actor?.avatar?.url) return actor.avatar.url 18 if (actor?.avatar?.url) return actor.avatar.url
19 19
20 if (actor && actor.avatar) { 20 if (actor && actor.avatar) {
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index de25d3ab9..389a242fd 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -25,9 +25,20 @@ export class UserNotification implements UserNotificationServer {
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 uuid: string
38 }
39 }
40
41 account?: ActorInfo
31 } 42 }
32 43
33 videoBlacklist?: { 44 videoBlacklist?: {
@@ -55,7 +66,7 @@ export class UserNotification implements UserNotificationServer {
55 // Additional fields 66 // Additional fields
56 videoUrl?: string 67 videoUrl?: string
57 commentUrl?: any[] 68 commentUrl?: any[]
58 videoAbuseUrl?: string 69 abuseUrl?: string
59 videoAutoBlacklistUrl?: string 70 videoAutoBlacklistUrl?: string
60 accountUrl?: string 71 accountUrl?: string
61 videoImportIdentifier?: string 72 videoImportIdentifier?: string
@@ -78,7 +89,7 @@ export class UserNotification implements UserNotificationServer {
78 this.comment = hash.comment 89 this.comment = hash.comment
79 if (this.comment) this.setAvatarUrl(this.comment.account) 90 if (this.comment) this.setAvatarUrl(this.comment.account)
80 91
81 this.videoAbuse = hash.videoAbuse 92 this.abuse = hash.abuse
82 93
83 this.videoBlacklist = hash.videoBlacklist 94 this.videoBlacklist = hash.videoBlacklist
84 95
@@ -108,8 +119,9 @@ export class UserNotification implements UserNotificationServer {
108 break 119 break
109 120
110 case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS: 121 case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS:
111 this.videoAbuseUrl = '/admin/moderation/video-abuses/list' 122 this.abuseUrl = '/admin/moderation/abuses/list'
112 this.videoUrl = this.buildVideoUrl(this.videoAbuse.video) 123
124 if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
113 break 125 break
114 126
115 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: 127 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
@@ -178,7 +190,7 @@ export class UserNotification implements UserNotificationServer {
178 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName 190 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
179 } 191 }
180 192
181 private setAvatarUrl (actor: { avatarUrl?: string, avatar?: Avatar }) { 193 private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
182 actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) 194 actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
183 } 195 }
184} 196}
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index d5be1470e..8d31eab0d 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -19,7 +19,7 @@
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>
@@ -46,7 +46,7 @@
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" 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> 50 </div>
51 </ng-container> 51 </ng-container>
52 52
@@ -65,7 +65,7 @@
65 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 65 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
66 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 66 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
67 </a> 67 </a>
68 68
69 <div class="message" i18n> 69 <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> 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>
71 </div> 71 </div>
@@ -73,7 +73,7 @@
73 73
74 <ng-template #noComment> 74 <ng-template #noComment>
75 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> 75 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
76 76
77 <div class="message" i18n> 77 <div class="message" i18n>
78 The notification concerns a comment now unavailable 78 The notification concerns a comment now unavailable
79 </div> 79 </div>
diff --git a/client/src/app/shared/shared-moderation/video-abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts
index 44dea44a5..f45018d5c 100644
--- a/client/src/app/shared/shared-moderation/video-abuse.service.ts
+++ b/client/src/app/shared/shared-moderation/abuse.service.ts
@@ -5,12 +5,12 @@ import { catchError, map } from 'rxjs/operators'
5import { HttpClient, HttpParams } from '@angular/common/http' 5import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable } from '@angular/core' 6import { Injectable } from '@angular/core'
7import { RestExtractor, RestPagination, RestService } from '@app/core' 7import { RestExtractor, RestPagination, RestService } from '@app/core'
8import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '@shared/models' 8import { AbuseUpdate, ResultList, Abuse, AbuseCreate, AbuseState } from '@shared/models'
9import { environment } from '../../../environments/environment' 9import { environment } from '../../../environments/environment'
10 10
11@Injectable() 11@Injectable()
12export class VideoAbuseService { 12export class AbuseService {
13 private static BASE_VIDEO_ABUSE_URL = environment.apiUrl + '/api/v1/videos/' 13 private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses'
14 14
15 constructor ( 15 constructor (
16 private authHttp: HttpClient, 16 private authHttp: HttpClient,
@@ -18,13 +18,13 @@ export class VideoAbuseService {
18 private restExtractor: RestExtractor 18 private restExtractor: RestExtractor
19 ) {} 19 ) {}
20 20
21 getVideoAbuses (options: { 21 getAbuses (options: {
22 pagination: RestPagination, 22 pagination: RestPagination,
23 sort: SortMeta, 23 sort: SortMeta,
24 search?: string 24 search?: string
25 }): Observable<ResultList<VideoAbuse>> { 25 }): Observable<ResultList<Abuse>> {
26 const { pagination, sort, search } = options 26 const { pagination, sort, search } = options
27 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse' 27 const url = AbuseService.BASE_ABUSE_URL + 'abuse'
28 28
29 let params = new HttpParams() 29 let params = new HttpParams()
30 params = this.restService.addRestGetParams(params, pagination, sort) 30 params = this.restService.addRestGetParams(params, pagination, sort)
@@ -35,9 +35,9 @@ export class VideoAbuseService {
35 state: { 35 state: {
36 prefix: 'state:', 36 prefix: 'state:',
37 handler: v => { 37 handler: v => {
38 if (v === 'accepted') return VideoAbuseState.ACCEPTED 38 if (v === 'accepted') return AbuseState.ACCEPTED
39 if (v === 'pending') return VideoAbuseState.PENDING 39 if (v === 'pending') return AbuseState.PENDING
40 if (v === 'rejected') return VideoAbuseState.REJECTED 40 if (v === 'rejected') return AbuseState.REJECTED
41 41
42 return undefined 42 return undefined
43 } 43 }
@@ -59,14 +59,14 @@ export class VideoAbuseService {
59 params = this.restService.addObjectParams(params, filters) 59 params = this.restService.addObjectParams(params, filters)
60 } 60 }
61 61
62 return this.authHttp.get<ResultList<VideoAbuse>>(url, { params }) 62 return this.authHttp.get<ResultList<Abuse>>(url, { params })
63 .pipe( 63 .pipe(
64 catchError(res => this.restExtractor.handleError(res)) 64 catchError(res => this.restExtractor.handleError(res))
65 ) 65 )
66 } 66 }
67 67
68 reportVideo (parameters: { id: number } & VideoAbuseCreate) { 68 reportVideo (parameters: AbuseCreate) {
69 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse' 69 const url = AbuseService.BASE_ABUSE_URL
70 70
71 const body = omit(parameters, [ 'id' ]) 71 const body = omit(parameters, [ 'id' ])
72 72
@@ -77,8 +77,8 @@ export class VideoAbuseService {
77 ) 77 )
78 } 78 }
79 79
80 updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) { 80 updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
81 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id 81 const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
82 82
83 return this.authHttp.put(url, abuseUpdate) 83 return this.authHttp.put(url, abuseUpdate)
84 .pipe( 84 .pipe(
@@ -87,8 +87,8 @@ export class VideoAbuseService {
87 ) 87 )
88 } 88 }
89 89
90 removeVideoAbuse (videoAbuse: VideoAbuse) { 90 removeAbuse (abuse: Abuse) {
91 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id 91 const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
92 92
93 return this.authHttp.delete(url) 93 return this.authHttp.delete(url)
94 .pipe( 94 .pipe(
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts
index 8e74254f6..d6c4a10be 100644
--- a/client/src/app/shared/shared-moderation/index.ts
+++ b/client/src/app/shared/shared-moderation/index.ts
@@ -1,3 +1,4 @@
1export * from './abuse.service'
1export * from './account-block.model' 2export * from './account-block.model'
2export * from './account-blocklist.component' 3export * from './account-blocklist.component'
3export * from './batch-domains-modal.component' 4export * from './batch-domains-modal.component'
@@ -6,7 +7,6 @@ export * from './bulk.service'
6export * from './server-blocklist.component' 7export * from './server-blocklist.component'
7export * from './user-ban-modal.component' 8export * from './user-ban-modal.component'
8export * from './user-moderation-dropdown.component' 9export * from './user-moderation-dropdown.component'
9export * from './video-abuse.service'
10export * from './video-block.component' 10export * from './video-block.component'
11export * from './video-block.service' 11export * from './video-block.service'
12export * from './video-report.component' 12export * from './video-report.component'
diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
index f7e64dfa3..742193e58 100644
--- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts
+++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
@@ -8,7 +8,7 @@ import { BlocklistService } from './blocklist.service'
8import { BulkService } from './bulk.service' 8import { BulkService } from './bulk.service'
9import { UserBanModalComponent } from './user-ban-modal.component' 9import { UserBanModalComponent } from './user-ban-modal.component'
10import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' 10import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
11import { VideoAbuseService } from './video-abuse.service' 11import { AbuseService } from './abuse.service'
12import { VideoBlockComponent } from './video-block.component' 12import { VideoBlockComponent } from './video-block.component'
13import { VideoBlockService } from './video-block.service' 13import { VideoBlockService } from './video-block.service'
14import { VideoReportComponent } from './video-report.component' 14import { VideoReportComponent } from './video-report.component'
@@ -39,7 +39,7 @@ import { VideoReportComponent } from './video-report.component'
39 providers: [ 39 providers: [
40 BlocklistService, 40 BlocklistService,
41 BulkService, 41 BulkService,
42 VideoAbuseService, 42 AbuseService,
43 VideoBlockService 43 VideoBlockService
44 ] 44 ]
45}) 45})
diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/video-report.component.ts
index 11c805636..b8d9f8d27 100644
--- a/client/src/app/shared/shared-moderation/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/video-report.component.ts
@@ -3,13 +3,13 @@ import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
3import { Component, Input, OnInit, ViewChild } from '@angular/core' 3import { Component, Input, OnInit, ViewChild } from '@angular/core'
4import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 4import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms' 6import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { videoAbusePredefinedReasonsMap, VideoAbusePredefinedReasonsString } from '@shared/models/videos/abuse/video-abuse-reason.model' 10import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
11import { Video } from '../shared-main' 11import { Video } from '../shared-main'
12import { VideoAbuseService } from './video-abuse.service' 12import { AbuseService } from './abuse.service'
13 13
14@Component({ 14@Component({
15 selector: 'my-video-report', 15 selector: 'my-video-report',
@@ -22,7 +22,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
22 @ViewChild('modal', { static: true }) modal: NgbModal 22 @ViewChild('modal', { static: true }) modal: NgbModal
23 23
24 error: string = null 24 error: string = null
25 predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] 25 predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
26 embedHtml: SafeHtml 26 embedHtml: SafeHtml
27 27
28 private openedModal: NgbModalRef 28 private openedModal: NgbModalRef
@@ -30,8 +30,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
30 constructor ( 30 constructor (
31 protected formValidatorService: FormValidatorService, 31 protected formValidatorService: FormValidatorService,
32 private modalService: NgbModal, 32 private modalService: NgbModal,
33 private videoAbuseValidatorsService: VideoAbuseValidatorsService, 33 private abuseValidatorsService: AbuseValidatorsService,
34 private videoAbuseService: VideoAbuseService, 34 private abuseService: AbuseService,
35 private notifier: Notifier, 35 private notifier: Notifier,
36 private sanitizer: DomSanitizer, 36 private sanitizer: DomSanitizer,
37 private i18n: I18n 37 private i18n: I18n
@@ -69,8 +69,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
69 69
70 ngOnInit () { 70 ngOnInit () {
71 this.buildForm({ 71 this.buildForm({
72 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON, 72 reason: this.abuseValidatorsService.ABUSE_REASON,
73 predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null), 73 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null),
74 timestamp: { 74 timestamp: {
75 hasStart: null, 75 hasStart: null,
76 startAt: null, 76 startAt: null,
@@ -136,15 +136,18 @@ export class VideoReportComponent extends FormReactive implements OnInit {
136 136
137 report () { 137 report () {
138 const reason = this.form.get('reason').value 138 const reason = this.form.get('reason').value
139 const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[] 139 const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[]
140 const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value 140 const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
141 141
142 this.videoAbuseService.reportVideo({ 142 this.abuseService.reportVideo({
143 id: this.video.id, 143 accountId: this.video.account.id,
144 reason, 144 reason,
145 predefinedReasons, 145 predefinedReasons,
146 startAt: hasStart && startAt ? startAt : undefined, 146 video: {
147 endAt: hasEnd && endAt ? endAt : undefined 147 id: this.video.id,
148 startAt: hasStart && startAt ? startAt : undefined,
149 endAt: hasEnd && endAt ? endAt : undefined
150 }
148 }).subscribe( 151 }).subscribe(
149 () => { 152 () => {
150 this.notifier.success(this.i18n('Video reported.')) 153 this.notifier.success(this.i18n('Video reported.'))
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
new file mode 100644
index 000000000..ee046cb3a
--- /dev/null
+++ b/server/controllers/api/abuse.ts
@@ -0,0 +1,168 @@
1import * as express from 'express'
2import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
3import { AbuseModel } from '@server/models/abuse/abuse'
4import { getServerActor } from '@server/models/application/application'
5import { AbuseCreate, abusePredefinedReasonsMap, AbuseState, UserRight } from '../../../shared'
6import { getFormattedObjects } from '../../helpers/utils'
7import { sequelizeTypescript } from '../../initializers/database'
8import {
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'
22import { AccountModel } from '../../models/account/account'
23
24const abuseRouter = express.Router()
25
26abuseRouter.get('/abuse',
27 authenticate,
28 ensureUserHasRight(UserRight.MANAGE_ABUSES),
29 paginationValidator,
30 abusesSortValidator,
31 setDefaultSort,
32 setDefaultPagination,
33 abuseListValidator,
34 asyncMiddleware(listAbuses)
35)
36abuseRouter.put('/:videoId/abuse/:id',
37 authenticate,
38 ensureUserHasRight(UserRight.MANAGE_ABUSES),
39 asyncMiddleware(abuseUpdateValidator),
40 asyncRetryTransactionMiddleware(updateAbuse)
41)
42abuseRouter.post('/:videoId/abuse',
43 authenticate,
44 asyncMiddleware(abuseReportValidator),
45 asyncRetryTransactionMiddleware(reportAbuse)
46)
47abuseRouter.delete('/:videoId/abuse/:id',
48 authenticate,
49 ensureUserHasRight(UserRight.MANAGE_ABUSES),
50 asyncMiddleware(abuseGetValidator),
51 asyncRetryTransactionMiddleware(deleteAbuse)
52)
53
54// ---------------------------------------------------------------------------
55
56export {
57 abuseRouter,
58
59 // FIXME: deprecated in 2.3. Remove these exports
60 listAbuses,
61 updateAbuse,
62 deleteAbuse,
63 reportAbuse
64}
65
66// ---------------------------------------------------------------------------
67
68async 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: 'video',
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
93async 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 video abuse
104
105 return res.type('json').status(204).end()
106}
107
108async 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 video abuse
116
117 return res.type('json').status(204).end()
118}
119
120async 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'
3import * as RateLimit from 'express-rate-limit' 3import * as RateLimit from 'express-rate-limit'
4import { badRequest } from '../../helpers/express-utils' 4import { badRequest } from '../../helpers/express-utils'
5import { CONFIG } from '../../initializers/config' 5import { CONFIG } from '../../initializers/config'
6import { abuseRouter } from './abuse'
6import { accountsRouter } from './accounts' 7import { accountsRouter } from './accounts'
7import { bulkRouter } from './bulk' 8import { bulkRouter } from './bulk'
8import { configRouter } from './config' 9import { configRouter } from './config'
@@ -32,6 +33,7 @@ const apiRateLimiter = RateLimit({
32apiRouter.use(apiRateLimiter) 33apiRouter.use(apiRateLimiter)
33 34
34apiRouter.use('/server', serverRouter) 35apiRouter.use('/server', serverRouter)
36apiRouter.use('/abuses', abuseRouter)
35apiRouter.use('/bulk', bulkRouter) 37apiRouter.use('/bulk', bulkRouter)
36apiRouter.use('/oauth-clients', oauthClientsRouter) 38apiRouter.use('/oauth-clients', oauthClientsRouter)
37apiRouter.use('/config', configRouter) 39apiRouter.use('/config', configRouter)
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index ab2074459..b92a66360 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -1,9 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared' 2import { AbuseModel } from '@server/models/abuse/abuse'
3import { logger } from '../../../helpers/logger' 3import { getServerActor } from '@server/models/application/application'
4import { AbuseCreate, UserRight, VideoAbuseCreate } from '../../../../shared'
4import { getFormattedObjects } from '../../../helpers/utils' 5import { getFormattedObjects } from '../../../helpers/utils'
5import { sequelizeTypescript } from '../../../initializers/database'
6import { 6import {
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'
20import { AccountModel } from '../../../models/account/account' 20import { deleteAbuse, reportAbuse, updateAbuse } from '../abuse'
21import { VideoAbuseModel } from '../../../models/video/video-abuse' 21
22import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' 22// FIXME: deprecated in 2.3. Remove this controller
23import { Notifier } from '../../../lib/notifier'
24import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
25import { MVideoAbuseAccountVideo } from '../../../types/models/video'
26import { getServerActor } from '@server/models/application/application'
27import { MAccountDefault } from '@server/types/models'
28 23
29const auditLogger = auditLoggerFactory('abuse')
30const abuseVideoRouter = express.Router() 24const abuseVideoRouter = express.Router()
31 25
32abuseVideoRouter.get('/abuse', 26abuseVideoRouter.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)
42abuseVideoRouter.put('/:videoId/abuse/:id', 36abuseVideoRouter.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)
53abuseVideoRouter.delete('/:videoId/abuse/:id', 47abuseVideoRouter.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
92async function updateVideoAbuse (req: express.Request, res: express.Response) { 87async 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
107async function deleteVideoAbuse (req: express.Request, res: express.Response) { 91async 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
119async function reportVideoAbuse (req: express.Request, res: express.Response) { 95async 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 @@
1import * as path from 'path'
2import * as express from 'express'
3import { diff } from 'deep-object-diff' 1import { diff } from 'deep-object-diff'
4import { chain } from 'lodash' 2import * as express from 'express'
5import * as flatten from 'flat' 3import * as flatten from 'flat'
4import { chain } from 'lodash'
5import * as path from 'path'
6import * as winston from 'winston' 6import * as winston from 'winston'
7import { jsonLoggerFormat, labelFormatter } from './logger' 7import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
8import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared' 8import { Abuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
9import { VideoComment } from '../../shared/models/videos/video-comment.model'
10import { CustomConfig } from '../../shared/models/server/custom-config.model' 9import { CustomConfig } from '../../shared/models/server/custom-config.model'
10import { VideoComment } from '../../shared/models/videos/video-comment.model'
11import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
12import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' 12import { jsonLoggerFormat, labelFormatter } from './logger'
13 13
14function getAuditIdFromRes (res: express.Response) { 14function 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
215const videoAbuseKeysToKeep = [ 215const 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]
224class VideoAbuseAuditView extends EntityAuditView { 221class 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..a6a895c65
--- /dev/null
+++ b/server/helpers/custom-validators/abuses.ts
@@ -0,0 +1,54 @@
1import validator from 'validator'
2import { abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs } from '@shared/models'
3import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
4import { exists, isArray } from './misc'
5
6const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
7
8function isAbuseReasonValid (value: string) {
9 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
10}
11
12function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) {
13 return exists(value) && value in abusePredefinedReasonsMap
14}
15
16function isAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) {
17 return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap)
18}
19
20function isAbuseTimestampValid (value: number) {
21 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
22}
23
24function isAbuseTimestampCoherent (endAt: number, { req }) {
25 return exists(req.body.startAt) && endAt > req.body.startAt
26}
27
28function isAbuseModerationCommentValid (value: string) {
29 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
30}
31
32function isAbuseStateValid (value: string) {
33 return exists(value) && ABUSE_STATES[value] !== undefined
34}
35
36function isAbuseVideoIsValid (value: AbuseVideoIs) {
37 return exists(value) && (
38 value === 'deleted' ||
39 value === 'blacklisted'
40 )
41}
42
43// ---------------------------------------------------------------------------
44
45export {
46 isAbuseReasonValid,
47 isAbusePredefinedReasonValid,
48 isAbusePredefinedReasonsValid,
49 isAbuseTimestampValid,
50 isAbuseTimestampCoherent,
51 isAbuseModerationCommentValid,
52 isAbuseStateValid,
53 isAbuseVideoIsValid
54}
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 @@
1import { isActivityPubUrlValid } from './misc' 1import { isActivityPubUrlValid } from './misc'
2import { isVideoAbuseReasonValid } from '../video-abuses' 2import { isAbuseReasonValid } from '../abuses'
3 3
4function isFlagActivityValid (activity: any) { 4function 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 @@
1import validator from 'validator'
2
3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
4import { exists, isArray } from './misc'
5import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
6import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model'
7
8const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
9
10function isVideoAbuseReasonValid (value: string) {
11 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
12}
13
14function isVideoAbusePredefinedReasonValid (value: VideoAbusePredefinedReasonsString) {
15 return exists(value) && value in videoAbusePredefinedReasonsMap
16}
17
18function isVideoAbusePredefinedReasonsValid (value: VideoAbusePredefinedReasonsString[]) {
19 return exists(value) && isArray(value) && value.every(v => v in videoAbusePredefinedReasonsMap)
20}
21
22function isVideoAbuseTimestampValid (value: number) {
23 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
24}
25
26function isVideoAbuseTimestampCoherent (endAt: number, { req }) {
27 return exists(req.body.startAt) && endAt > req.body.startAt
28}
29
30function isVideoAbuseModerationCommentValid (value: string) {
31 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
32}
33
34function isVideoAbuseStateValid (value: string) {
35 return exists(value) && VIDEO_ABUSE_STATES[value] !== undefined
36}
37
38function isAbuseVideoIsValid (value: VideoAbuseVideoIs) {
39 return exists(value) && (
40 value === 'deleted' ||
41 value === 'blacklisted'
42 )
43}
44
45// ---------------------------------------------------------------------------
46
47export {
48 isVideoAbuseReasonValid,
49 isVideoAbusePredefinedReasonValid,
50 isVideoAbusePredefinedReasonsValid,
51 isVideoAbuseTimestampValid,
52 isVideoAbuseTimestampCoherent,
53 isVideoAbuseModerationCommentValid,
54 isVideoAbuseStateValid,
55 isAbuseVideoIsValid
56}
diff --git a/server/helpers/middlewares/video-abuses.ts b/server/helpers/middlewares/abuses.ts
index 97a5724b6..3906f6760 100644
--- a/server/helpers/middlewares/video-abuses.ts
+++ b/server/helpers/middlewares/abuses.ts
@@ -1,19 +1,20 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import { VideoAbuseModel } from '../../models/video/video-abuse' 2import { AbuseModel } from '../../models/abuse/abuse'
3import { fetchVideo } from '../video' 3import { fetchVideo } from '../video'
4 4
5// FIXME: deprecated in 2.3. Remove this function
5async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) { 6async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
6 const abuseId = parseInt(abuseIdArg + '', 10) 7 const abuseId = parseInt(abuseIdArg + '', 10)
7 let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID) 8 let abuse = await AbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
8 9
9 if (!videoAbuse) { 10 if (!abuse) {
10 const userId = res.locals.oauth?.token.User.id 11 const userId = res.locals.oauth?.token.User.id
11 const video = await fetchVideo(videoUUID, 'all', userId) 12 const video = await fetchVideo(videoUUID, 'all', userId)
12 13
13 if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id) 14 if (video) abuse = await AbuseModel.loadByIdAndVideoId(abuseId, video.id)
14 } 15 }
15 16
16 if (videoAbuse === null) { 17 if (abuse === null) {
17 res.status(404) 18 res.status(404)
18 .json({ error: 'Video abuse not found' }) 19 .json({ error: 'Video abuse not found' })
19 .end() 20 .end()
@@ -21,12 +22,17 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
21 return false 22 return false
22 } 23 }
23 24
24 res.locals.videoAbuse = videoAbuse 25 res.locals.abuse = abuse
25 return true 26 return true
26} 27}
27 28
29async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
30
31}
32
28// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
29 34
30export { 35export {
36 doesAbuseExist,
31 doesVideoAbuseExist 37 doesVideoAbuseExist
32} 38}
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 @@
1export * from './abuses'
1export * from './accounts' 2export * from './accounts'
2export * from './video-abuses'
3export * from './video-blacklists' 3export * from './video-blacklists'
4export * from './video-captions' 4export * from './video-captions'
5export * from './video-channels' 5export * from './video-channels'
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index e730e3c84..8f86bbbef 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -1,9 +1,17 @@
1import { join } from 'path' 1import { join } from 'path'
2import { randomBytes } from 'crypto' 2import { randomBytes } from 'crypto'
3import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 3import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 4import { FollowState } from '../../shared/models/actors'
6import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' 5import {
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
8import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils' 16import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 17import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
@@ -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
381const VIDEO_ABUSE_STATES = { 390const 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
387const VIDEO_PLAYLIST_PRIVACIES = { 396const 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 @@
1import { QueryTypes, Transaction } from 'sequelize'
1import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' 2import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
3import { AbuseModel } from '@server/models/abuse/abuse'
4import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
5import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
2import { isTestInstance } from '../helpers/core-utils' 6import { isTestInstance } from '../helpers/core-utils'
3import { logger } from '../helpers/logger' 7import { logger } from '../helpers/logger'
4
5import { AccountModel } from '../models/account/account' 8import { AccountModel } from '../models/account/account'
9import { AccountBlocklistModel } from '../models/account/account-blocklist'
6import { AccountVideoRateModel } from '../models/account/account-video-rate' 10import { AccountVideoRateModel } from '../models/account/account-video-rate'
7import { UserModel } from '../models/account/user' 11import { UserModel } from '../models/account/user'
12import { UserNotificationModel } from '../models/account/user-notification'
13import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
14import { UserVideoHistoryModel } from '../models/account/user-video-history'
8import { ActorModel } from '../models/activitypub/actor' 15import { ActorModel } from '../models/activitypub/actor'
9import { ActorFollowModel } from '../models/activitypub/actor-follow' 16import { ActorFollowModel } from '../models/activitypub/actor-follow'
10import { ApplicationModel } from '../models/application/application' 17import { ApplicationModel } from '../models/application/application'
11import { AvatarModel } from '../models/avatar/avatar' 18import { AvatarModel } from '../models/avatar/avatar'
12import { OAuthClientModel } from '../models/oauth/oauth-client' 19import { OAuthClientModel } from '../models/oauth/oauth-client'
13import { OAuthTokenModel } from '../models/oauth/oauth-token' 20import { OAuthTokenModel } from '../models/oauth/oauth-token'
21import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
22import { PluginModel } from '../models/server/plugin'
14import { ServerModel } from '../models/server/server' 23import { ServerModel } from '../models/server/server'
24import { ServerBlocklistModel } from '../models/server/server-blocklist'
25import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
15import { TagModel } from '../models/video/tag' 26import { TagModel } from '../models/video/tag'
27import { ThumbnailModel } from '../models/video/thumbnail'
16import { VideoModel } from '../models/video/video' 28import { VideoModel } from '../models/video/video'
17import { VideoAbuseModel } from '../models/video/video-abuse'
18import { VideoBlacklistModel } from '../models/video/video-blacklist' 29import { VideoBlacklistModel } from '../models/video/video-blacklist'
30import { VideoCaptionModel } from '../models/video/video-caption'
31import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
19import { VideoChannelModel } from '../models/video/video-channel' 32import { VideoChannelModel } from '../models/video/video-channel'
20import { VideoCommentModel } from '../models/video/video-comment' 33import { VideoCommentModel } from '../models/video/video-comment'
21import { VideoFileModel } from '../models/video/video-file' 34import { VideoFileModel } from '../models/video/video-file'
22import { VideoShareModel } from '../models/video/video-share'
23import { VideoTagModel } from '../models/video/video-tag'
24import { CONFIG } from './config'
25import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
26import { VideoCaptionModel } from '../models/video/video-caption'
27import { VideoImportModel } from '../models/video/video-import' 35import { VideoImportModel } from '../models/video/video-import'
28import { VideoViewModel } from '../models/video/video-view'
29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
31import { UserVideoHistoryModel } from '../models/account/user-video-history'
32import { AccountBlocklistModel } from '../models/account/account-blocklist'
33import { ServerBlocklistModel } from '../models/server/server-blocklist'
34import { UserNotificationModel } from '../models/account/user-notification'
35import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
36import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
37import { VideoPlaylistModel } from '../models/video/video-playlist' 36import { VideoPlaylistModel } from '../models/video/video-playlist'
38import { VideoPlaylistElementModel } from '../models/video/video-playlist-element' 37import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
39import { ThumbnailModel } from '../models/video/thumbnail' 38import { VideoShareModel } from '../models/video/video-share'
40import { PluginModel } from '../models/server/plugin' 39import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
41import { QueryTypes, Transaction } from 'sequelize' 40import { VideoTagModel } from '../models/video/video-tag'
41import { VideoViewModel } from '../models/video/video-view'
42import { CONFIG } from './config'
42 43
43require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 44require('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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { VideoAbuseState } from '../../../shared/models/videos' 2import { AbuseState } from '../../../shared/models'
3 3
4async function up (utils: { 4async 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/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 @@
1import { 1import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
2 ActivityCreate, 2import { AccountModel } from '@server/models/account/account'
3 ActivityFlag, 3import { VideoModel } from '@server/models/video/video'
4 VideoAbuseState, 4import { VideoCommentModel } from '@server/models/video/video-comment'
5 videoAbusePredefinedReasonsMap 5import { AbuseObject, abusePredefinedReasonsMap, AbuseState, ActivityCreate, ActivityFlag } from '../../../../shared'
6} from '../../../../shared' 6import { getAPId } from '../../../helpers/activitypub'
7import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
8import { retryTransactionWrapper } from '../../../helpers/database-utils' 7import { retryTransactionWrapper } from '../../../helpers/database-utils'
9import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
10import { sequelizeTypescript } from '../../../initializers/database' 9import { sequelizeTypescript } from '../../../initializers/database'
11import { VideoAbuseModel } from '../../../models/video/video-abuse'
12import { getOrCreateVideoAndAccountAndChannel } from '../videos'
13import { Notifier } from '../../notifier'
14import { getAPId } from '../../../helpers/activitypub'
15import { APProcessorOptions } from '../../../types/activitypub-processor.model' 10import { APProcessorOptions } from '../../../types/activitypub-processor.model'
16import { MActorSignature, MVideoAbuseAccountVideo } from '../../../types/models' 11import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models'
17import { AccountModel } from '@server/models/account/account'
18 12
19async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { 13async 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
32async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { 27async 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 @@
1import { getVideoAbuseActivityPubUrl } from '../url' 1import { Transaction } from 'sequelize'
2import { unicastTo } from './utils'
3import { logger } from '../../../helpers/logger'
4import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
3import { logger } from '../../../helpers/logger'
4import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
5import { audiencify, getAudience } from '../audience' 5import { audiencify, getAudience } from '../audience'
6import { Transaction } from 'sequelize' 6import { getAbuseActivityPubUrl } from '../url'
7import { MActor, MVideoFullLight } from '../../../types/models' 7import { unicastTo } from './utils'
8import { MVideoAbuseVideo } from '../../../types/models/video'
9 8
10function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { 9function 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
24function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag { 23function 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
37export { 36export {
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'
13import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist' 13import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist'
14import { MVideoFileVideoUUID } from '../../types/models/video/video-file' 14import { 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
51function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) { 51function getAbuseActivityPubUrl (abuse: MAbuseId) {
52 return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id 52 return WEBSERVER.URL + '/admin/abuses/' + abuse.id
53} 53}
54 54
55function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) { 55function 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..e821aea5f 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,26 +1,20 @@
1import { readFileSync } from 'fs-extra'
2import { merge } from 'lodash'
1import { createTransport, Transporter } from 'nodemailer' 3import { createTransport, Transporter } from 'nodemailer'
4import { join } from 'path'
5import { VideoChannelModel } from '@server/models/video/video-channel'
6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { Abuse, EmailPayload } from '@shared/models'
9import { SendEmailOptions } from '../../shared/models/server/emailer.model'
2import { isTestInstance, root } from '../helpers/core-utils' 10import { isTestInstance, root } from '../helpers/core-utils'
3import { bunyanLogger, logger } from '../helpers/logger' 11import { bunyanLogger, logger } from '../helpers/logger'
4import { CONFIG, isEmailEnabled } from '../initializers/config' 12import { CONFIG, isEmailEnabled } from '../initializers/config'
5import { JobQueue } from './job-queue'
6import { readFileSync } from 'fs-extra'
7import { WEBSERVER } from '../initializers/constants' 13import { WEBSERVER } from '../initializers/constants'
8import { 14import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
9 MCommentOwnerVideo, 15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
10 MVideo, 16import { JobQueue } from './job-queue'
11 MVideoAbuseVideo, 17
12 MVideoAccountLight,
13 MVideoBlacklistLightVideo,
14 MVideoBlacklistVideo
15} from '../types/models/video'
16import { MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
17import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
18import { EmailPayload } from '@shared/models'
19import { join } from 'path'
20import { VideoAbuse } from '../../shared/models/videos'
21import { SendEmailOptions } from '../../shared/models/server/emailer.model'
22import { merge } from 'lodash'
23import { VideoChannelModel } from '@server/models/video/video-channel'
24const Email = require('email-templates') 18const Email = require('email-templates')
25 19
26class Emailer { 20class Emailer {
@@ -288,28 +282,70 @@ 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: video.VideoChannel,
315 action
316 }
317 }
318 } else if (abuseInstance.VideoCommentAbuse) {
319 const comment = abuseInstance.VideoCommentAbuse.VideoComment
320 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
321
322 emailPayload = {
323 template: 'comment-abuse-new',
324 to,
325 subject: `New comment abuse report from ${reporter}`,
326 locals: {
327 commentUrl,
328 isLocal: comment.isOwned(),
329 commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
330 reason: abuse.reason,
331 flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
332 action
333 }
334 }
335 } else {
336 const account = abuseInstance.FlaggedAccount
337 const accountUrl = account.getClientUrl()
338
339 emailPayload = {
340 template: 'account-abuse-new',
341 to,
342 subject: `New account abuse report from ${reporter}`,
343 locals: {
344 accountUrl,
345 accountDisplayName: account.getDisplayName(),
346 isLocal: account.isOwned(),
347 reason: abuse.reason,
348 action
313 } 349 }
314 } 350 }
315 } 351 }
diff --git a/server/lib/emails/account-abuse-new/html.pug b/server/lib/emails/account-abuse-new/html.pug
new file mode 100644
index 000000000..06be8025b
--- /dev/null
+++ b/server/lib/emails/account-abuse-new/html.pug
@@ -0,0 +1,14 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | An account is pending moderation
6
7block 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 @@
1mixin channel(channel) 1mixin 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
5mixin 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
7block content 7block 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..170b79576
--- /dev/null
+++ b/server/lib/emails/video-comment-abuse-new/html.pug
@@ -0,0 +1,15 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | A comment is pending moderation
6
7block content
8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}comment "
10 a(href=commentUrl) of #{flaggedAccount}
11 | created on #{commentCreatedAt}
12
13 p The reporter, #{reporter}, cited the following reason(s):
14 blockquote #{reason}
15 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 @@
1import { VideoModel } from '../models/video/video' 1import { PathLike } from 'fs-extra'
2import { VideoCommentModel } from '../models/video/video-comment' 2import { Transaction } from 'sequelize/types'
3import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' 3import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger'
4import { logger } from '@server/helpers/logger'
5import { AbuseModel } from '@server/models/abuse/abuse'
6import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
7import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
8import { VideoFileModel } from '@server/models/video/video-file'
9import { FilteredModelAttributes } from '@server/types'
10import {
11 MAbuseFull,
12 MAccountDefault,
13 MAccountLight,
14 MCommentAbuseAccountVideo,
15 MCommentOwnerVideo,
16 MUser,
17 MVideoAbuseVideoFull,
18 MVideoAccountLightBlacklistAllFiles
19} from '@server/types/models'
20import { ActivityCreate } from '../../shared/models/activitypub'
21import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
22import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
4import { VideoCreate, VideoImportCreate } from '../../shared/models/videos' 23import { VideoCreate, VideoImportCreate } from '../../shared/models/videos'
24import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
5import { UserModel } from '../models/account/user' 25import { UserModel } from '../models/account/user'
6import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
7import { ActivityCreate } from '../../shared/models/activitypub'
8import { ActorModel } from '../models/activitypub/actor' 26import { ActorModel } from '../models/activitypub/actor'
9import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' 27import { VideoModel } from '../models/video/video'
10import { VideoFileModel } from '@server/models/video/video-file' 28import { VideoCommentModel } from '../models/video/video-comment'
11import { PathLike } from 'fs-extra' 29import { sendAbuse } from './activitypub/send/send-flag'
12import { MUser } from '@server/types/models' 30import { Notifier } from './notifier'
13 31
14export type AcceptResult = { 32export 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
94async 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
127function 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
156function 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
76export { 177export {
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
194async 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..40cff66d2 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'
11import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
11import { MVideoImportVideo } from '@server/types/models/video/video-import' 12import { MVideoImportVideo } from '@server/types/models/video/video-import'
13import { Abuse } from '@shared/models'
12import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' 14import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
13import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos' 15import { VideoPrivacy, VideoState } from '../../shared/models/videos'
14import { logger } from '../helpers/logger' 16import { logger } from '../helpers/logger'
15import { CONFIG } from '../initializers/config' 17import { CONFIG } from '../initializers/config'
16import { AccountBlocklistModel } from '../models/account/account-blocklist' 18import { AccountBlocklistModel } from '../models/account/account-blocklist'
17import { UserModel } from '../models/account/user' 19import { UserModel } from '../models/account/user'
18import { UserNotificationModel } from '../models/account/user-notification' 20import { UserNotificationModel } from '../models/account/user-notification'
19import { MAccountServer, MActorFollowFull } from '../types/models' 21import { MAbuseFull, MAbuseVideo, MAccountServer, MActorFollowFull } from '../types/models'
20import { 22import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
21 MCommentOwnerVideo,
22 MVideoAbuseVideo,
23 MVideoAccountLight,
24 MVideoBlacklistLightVideo,
25 MVideoBlacklistVideo,
26 MVideoFullLight
27} from '../types/models/video'
28import { isBlockedByServerOrAccount } from './blocklist' 23import { isBlockedByServerOrAccount } from './blocklist'
29import { Emailer } from './emailer' 24import { Emailer } from './emailer'
30import { PeerTubeSocket } from './peertube-socket' 25import { 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,37 @@ 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 || abuseInstance.VideoCommentAbuse?.VideoComment?.url
363
364 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
366 365
367 function settingGetter (user: MUserWithNotificationSetting) { 366 function settingGetter (user: MUserWithNotificationSetting) {
368 return user.NotificationSetting.videoAbuseAsModerator 367 return user.NotificationSetting.videoAbuseAsModerator
369 } 368 }
370 369
371 async function notificationCreator (user: MUserWithNotificationSetting) { 370 async function notificationCreator (user: MUserWithNotificationSetting) {
372 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ 371 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
373 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 372 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
374 userId: user.id, 373 userId: user.id,
375 videoAbuseId: parameters.videoAbuse.id 374 abuseId: abuse.id
376 }) 375 })
377 notification.VideoAbuse = parameters.videoAbuseInstance 376 notification.Abuse = abuseInstance
378 377
379 return notification 378 return notification
380 } 379 }
381 380
382 function emailSender (emails: string[]) { 381 function emailSender (emails: string[]) {
383 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters) 382 return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters)
384 } 383 }
385 384
386 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 385 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts
new file mode 100644
index 000000000..f098e2ff9
--- /dev/null
+++ b/server/middlewares/validators/abuse.ts
@@ -0,0 +1,253 @@
1import * as express from 'express'
2import { body, param, query } from 'express-validator'
3import {
4 isAbuseModerationCommentValid,
5 isAbusePredefinedReasonsValid,
6 isAbusePredefinedReasonValid,
7 isAbuseReasonValid,
8 isAbuseStateValid,
9 isAbuseTimestampCoherent,
10 isAbuseTimestampValid,
11 isAbuseVideoIsValid
12} from '@server/helpers/custom-validators/abuses'
13import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc'
14import { logger } from '@server/helpers/logger'
15import { doesAbuseExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares'
16import { areValidationErrors } from './utils'
17
18const abuseReportValidator = [
19 param('videoId')
20 .custom(isIdOrUUIDValid)
21 .not()
22 .isEmpty()
23 .withMessage('Should have a valid videoId'),
24 body('reason')
25 .custom(isAbuseReasonValid)
26 .withMessage('Should have a valid reason'),
27 body('predefinedReasons')
28 .optional()
29 .custom(isAbusePredefinedReasonsValid)
30 .withMessage('Should have a valid list of predefined reasons'),
31 body('startAt')
32 .optional()
33 .customSanitizer(toIntOrNull)
34 .custom(isAbuseTimestampValid)
35 .withMessage('Should have valid starting time value'),
36 body('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 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
46 logger.debug('Checking abuseReport parameters', { parameters: req.body })
47
48 if (areValidationErrors(req, res)) return
49 if (!await doesVideoExist(req.params.videoId, res)) return
50
51 // TODO: check comment or video (exlusive)
52
53 return next()
54 }
55]
56
57const abuseGetValidator = [
58 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
59 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
60
61 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
62 logger.debug('Checking abuseGetValidator parameters', { parameters: req.body })
63
64 if (areValidationErrors(req, res)) return
65 // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return
66
67 return next()
68 }
69]
70
71const abuseUpdateValidator = [
72 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
73 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
74 body('state')
75 .optional()
76 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
77 body('moderationComment')
78 .optional()
79 .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
80
81 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
82 logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body })
83
84 if (areValidationErrors(req, res)) return
85 // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return
86
87 return next()
88 }
89]
90
91const abuseListValidator = [
92 query('id')
93 .optional()
94 .custom(isIdValid).withMessage('Should have a valid id'),
95 query('predefinedReason')
96 .optional()
97 .custom(isAbusePredefinedReasonValid)
98 .withMessage('Should have a valid predefinedReason'),
99 query('search')
100 .optional()
101 .custom(exists).withMessage('Should have a valid search'),
102 query('state')
103 .optional()
104 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
105 query('videoIs')
106 .optional()
107 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
108 query('searchReporter')
109 .optional()
110 .custom(exists).withMessage('Should have a valid reporter search'),
111 query('searchReportee')
112 .optional()
113 .custom(exists).withMessage('Should have a valid reportee search'),
114 query('searchVideo')
115 .optional()
116 .custom(exists).withMessage('Should have a valid video search'),
117 query('searchVideoChannel')
118 .optional()
119 .custom(exists).withMessage('Should have a valid video channel search'),
120
121 (req: express.Request, res: express.Response, next: express.NextFunction) => {
122 logger.debug('Checking abuseListValidator parameters', { parameters: req.body })
123
124 if (areValidationErrors(req, res)) return
125
126 return next()
127 }
128]
129
130// FIXME: deprecated in 2.3. Remove these validators
131
132const videoAbuseReportValidator = [
133 param('videoId')
134 .custom(isIdOrUUIDValid)
135 .not()
136 .isEmpty()
137 .withMessage('Should have a valid videoId'),
138 body('reason')
139 .custom(isAbuseReasonValid)
140 .withMessage('Should have a valid reason'),
141 body('predefinedReasons')
142 .optional()
143 .custom(isAbusePredefinedReasonsValid)
144 .withMessage('Should have a valid list of predefined reasons'),
145 body('startAt')
146 .optional()
147 .customSanitizer(toIntOrNull)
148 .custom(isAbuseTimestampValid)
149 .withMessage('Should have valid starting time value'),
150 body('endAt')
151 .optional()
152 .customSanitizer(toIntOrNull)
153 .custom(isAbuseTimestampValid)
154 .withMessage('Should have valid ending time value')
155 .bail()
156 .custom(isAbuseTimestampCoherent)
157 .withMessage('Should have a startAt timestamp beginning before endAt'),
158
159 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
160 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
161
162 if (areValidationErrors(req, res)) return
163 if (!await doesVideoExist(req.params.videoId, res)) return
164
165 return next()
166 }
167]
168
169const videoAbuseGetValidator = [
170 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
171 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
172
173 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
174 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
175
176 if (areValidationErrors(req, res)) return
177 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
178
179 return next()
180 }
181]
182
183const videoAbuseUpdateValidator = [
184 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
185 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
186 body('state')
187 .optional()
188 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
189 body('moderationComment')
190 .optional()
191 .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
192
193 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
194 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
195
196 if (areValidationErrors(req, res)) return
197 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
198
199 return next()
200 }
201]
202
203const videoAbuseListValidator = [
204 query('id')
205 .optional()
206 .custom(isIdValid).withMessage('Should have a valid id'),
207 query('predefinedReason')
208 .optional()
209 .custom(isAbusePredefinedReasonValid)
210 .withMessage('Should have a valid predefinedReason'),
211 query('search')
212 .optional()
213 .custom(exists).withMessage('Should have a valid search'),
214 query('state')
215 .optional()
216 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
217 query('videoIs')
218 .optional()
219 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
220 query('searchReporter')
221 .optional()
222 .custom(exists).withMessage('Should have a valid reporter search'),
223 query('searchReportee')
224 .optional()
225 .custom(exists).withMessage('Should have a valid reportee search'),
226 query('searchVideo')
227 .optional()
228 .custom(exists).withMessage('Should have a valid video search'),
229 query('searchVideoChannel')
230 .optional()
231 .custom(exists).withMessage('Should have a valid video channel search'),
232
233 (req: express.Request, res: express.Response, next: express.NextFunction) => {
234 logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body })
235
236 if (areValidationErrors(req, res)) return
237
238 return next()
239 }
240]
241
242// ---------------------------------------------------------------------------
243
244export {
245 abuseListValidator,
246 abuseReportValidator,
247 abuseGetValidator,
248 abuseUpdateValidator,
249 videoAbuseReportValidator,
250 videoAbuseGetValidator,
251 videoAbuseUpdateValidator,
252 videoAbuseListValidator
253}
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 @@
1export * from './abuse'
1export * from './account' 2export * from './account'
2export * from './blocklist' 3export * from './blocklist'
3export * from './oembed' 4export * 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'
5const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) 5const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
6const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS) 6const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS)
7const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) 7const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
8const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) 8const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES)
9const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) 9const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
10const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) 10const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) 11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
@@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM
28const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 28const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
29const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 29const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
30const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) 30const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
31const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) 31const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS)
32const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 32const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
33const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) 33const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
34const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) 34const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
@@ -52,7 +52,7 @@ const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COL
52 52
53export { 53export {
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/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 @@
1export * from './video-abuses'
2export * from './video-blacklist' 1export * from './video-blacklist'
3export * from './video-captions' 2export * from './video-captions'
4export * from './video-channels' 3export * 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 @@
1import * as express from 'express'
2import { body, param, query } from 'express-validator'
3import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
4import {
5 isAbuseVideoIsValid,
6 isVideoAbuseModerationCommentValid,
7 isVideoAbuseReasonValid,
8 isVideoAbuseStateValid,
9 isVideoAbusePredefinedReasonsValid,
10 isVideoAbusePredefinedReasonValid,
11 isVideoAbuseTimestampValid,
12 isVideoAbuseTimestampCoherent
13} from '../../../helpers/custom-validators/video-abuses'
14import { logger } from '../../../helpers/logger'
15import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares'
16import { areValidationErrors } from '../utils'
17
18const 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
55const 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
69const 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
89const 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
130export {
131 videoAbuseListValidator,
132 videoAbuseReportValidator,
133 videoAbuseGetValidator,
134 videoAbuseUpdateValidator
135}
diff --git a/server/models/video/video-abuse.ts b/server/models/abuse/abuse.ts
index 1319332f0..4f99f9c9b 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/abuse/abuse.ts
@@ -1,5 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { literal, Op } from 'sequelize' 2import { invert } from 'lodash'
3import { literal, Op, WhereOptions } from 'sequelize'
3import { 4import {
4 AllowNull, 5 AllowNull,
5 BelongsTo, 6 BelongsTo,
@@ -8,36 +9,35 @@ import {
8 DataType, 9 DataType,
9 Default, 10 Default,
10 ForeignKey, 11 ForeignKey,
12 HasOne,
11 Is, 13 Is,
12 Model, 14 Model,
13 Scopes, 15 Scopes,
14 Table, 16 Table,
15 UpdatedAt 17 UpdatedAt
16} from 'sequelize-typescript' 18} from 'sequelize-typescript'
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' 19import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
18import {
19 VideoAbuseState,
20 VideoDetails,
21 VideoAbusePredefinedReasons,
22 VideoAbusePredefinedReasonsString,
23 videoAbusePredefinedReasonsMap
24} from '../../../shared'
25import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
26import { VideoAbuse } from '../../../shared/models/videos'
27import { 20import {
28 isVideoAbuseModerationCommentValid, 21 Abuse,
29 isVideoAbuseReasonValid, 22 AbuseObject,
30 isVideoAbuseStateValid 23 AbusePredefinedReasons,
31} from '../../helpers/custom-validators/video-abuses' 24 abusePredefinedReasonsMap,
32import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 25 AbusePredefinedReasonsString,
33import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models' 26 AbuseState,
34import { AccountModel } from '../account/account' 27 AbuseVideoIs,
28 VideoAbuse
29} from '@shared/models'
30import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter'
31import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
32import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
33import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
35import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' 34import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
36import { ThumbnailModel } from './thumbnail' 35import { ThumbnailModel } from '../video/thumbnail'
37import { VideoModel } from './video' 36import { VideoModel } from '../video/video'
38import { VideoBlacklistModel } from './video-blacklist' 37import { VideoBlacklistModel } from '../video/video-blacklist'
39import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' 38import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
40import { invert } from 'lodash' 39import { VideoAbuseModel } from './video-abuse'
40import { VideoCommentAbuseModel } from './video-comment-abuse'
41 41
42export enum ScopeNames { 42export enum ScopeNames {
43 FOR_API = 'FOR_API' 43 FOR_API = 'FOR_API'
@@ -49,20 +49,26 @@ export enum ScopeNames {
49 search?: string 49 search?: string
50 searchReporter?: string 50 searchReporter?: string
51 searchReportee?: string 51 searchReportee?: string
52
53 // video releated
52 searchVideo?: string 54 searchVideo?: string
53 searchVideoChannel?: string 55 searchVideoChannel?: string
56 videoIs?: AbuseVideoIs
54 57
55 // filters 58 // filters
56 id?: number 59 id?: number
57 predefinedReasonId?: number 60 predefinedReasonId?: number
61 filter?: AbuseFilter
58 62
59 state?: VideoAbuseState 63 state?: AbuseState
60 videoIs?: VideoAbuseVideoIs
61 64
62 // accountIds 65 // accountIds
63 serverAccountId: number 66 serverAccountId: number
64 userAccountId: number 67 userAccountId: number
65 }) => { 68 }) => {
69 const onlyBlacklisted = options.videoIs === 'blacklisted'
70 const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel)
71
66 const where = { 72 const where = {
67 reporterAccountId: { 73 reporterAccountId: {
68 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') 74 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
@@ -70,33 +76,36 @@ export enum ScopeNames {
70 } 76 }
71 77
72 if (options.search) { 78 if (options.search) {
79 const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%')
80
73 Object.assign(where, { 81 Object.assign(where, {
74 [Op.or]: [ 82 [Op.or]: [
75 { 83 {
76 [Op.and]: [ 84 [Op.and]: [
77 { videoId: { [Op.not]: null } }, 85 { '$VideoAbuse.videoId$': { [Op.not]: null } },
78 searchAttribute(options.search, '$Video.name$') 86 searchAttribute(options.search, '$VideoAbuse.Video.name$')
79 ] 87 ]
80 }, 88 },
81 { 89 {
82 [Op.and]: [ 90 [Op.and]: [
83 { videoId: { [Op.not]: null } }, 91 { '$VideoAbuse.videoId$': { [Op.not]: null } },
84 searchAttribute(options.search, '$Video.VideoChannel.name$') 92 searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$')
85 ] 93 ]
86 }, 94 },
87 { 95 {
88 [Op.and]: [ 96 [Op.and]: [
89 { deletedVideo: { [Op.not]: null } }, 97 { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
90 { deletedVideo: searchAttribute(options.search, 'name') } 98 literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`)
91 ] 99 ]
92 }, 100 },
93 { 101 {
94 [Op.and]: [ 102 [Op.and]: [
95 { deletedVideo: { [Op.not]: null } }, 103 { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
96 { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } } 104 literal(`"VideoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE ${escapedSearch}`)
97 ] 105 ]
98 }, 106 },
99 searchAttribute(options.search, '$Account.name$') 107 searchAttribute(options.search, '$ReporterAccount.name$'),
108 searchAttribute(options.search, '$FlaggedAccount.name$')
100 ] 109 ]
101 }) 110 })
102 } 111 }
@@ -106,7 +115,7 @@ export enum ScopeNames {
106 115
107 if (options.videoIs === 'deleted') { 116 if (options.videoIs === 'deleted') {
108 Object.assign(where, { 117 Object.assign(where, {
109 deletedVideo: { 118 '$VideoAbuse.deletedVideo$': {
110 [Op.not]: null 119 [Op.not]: null
111 } 120 }
112 }) 121 })
@@ -120,8 +129,6 @@ export enum ScopeNames {
120 }) 129 })
121 } 130 }
122 131
123 const onlyBlacklisted = options.videoIs === 'blacklisted'
124
125 return { 132 return {
126 attributes: { 133 attributes: {
127 include: [ 134 include: [
@@ -131,7 +138,7 @@ export enum ScopeNames {
131 '(' + 138 '(' +
132 'SELECT count(*) ' + 139 'SELECT count(*) ' +
133 'FROM "videoAbuse" ' + 140 'FROM "videoAbuse" ' +
134 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' + 141 'WHERE "videoId" = "VideoAbuse"."videoId" ' +
135 ')' 142 ')'
136 ), 143 ),
137 'countReportsForVideo' 144 'countReportsForVideo'
@@ -146,7 +153,7 @@ export enum ScopeNames {
146 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + 153 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
147 'FROM "videoAbuse" ' + 154 'FROM "videoAbuse" ' +
148 ') t ' + 155 ') t ' +
149 'WHERE t.id = "VideoAbuseModel".id ' + 156 'WHERE t.id = "VideoAbuse".id' +
150 ')' 157 ')'
151 ), 158 ),
152 'nthReportForVideo' 159 'nthReportForVideo'
@@ -159,7 +166,7 @@ export enum ScopeNames {
159 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + 166 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
160 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 167 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
161 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + 168 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
162 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' + 169 'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
163 ')' 170 ')'
164 ), 171 ),
165 'countReportsForReporter__video' 172 'countReportsForReporter__video'
@@ -169,7 +176,7 @@ export enum ScopeNames {
169 '(' + 176 '(' +
170 'SELECT count(DISTINCT "videoAbuse"."id") ' + 177 'SELECT count(DISTINCT "videoAbuse"."id") ' +
171 'FROM "videoAbuse" ' + 178 'FROM "videoAbuse" ' +
172 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` + 179 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` +
173 ')' 180 ')'
174 ), 181 ),
175 'countReportsForReporter__deletedVideo' 182 'countReportsForReporter__deletedVideo'
@@ -182,8 +189,8 @@ export enum ScopeNames {
182 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + 189 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
183 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 190 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
184 'INNER JOIN "account" ON ' + 191 'INNER JOIN "account" ON ' +
185 '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' + 192 '"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' +
186 `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + 193 `OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
187 ')' 194 ')'
188 ), 195 ),
189 'countReportsForReportee__video' 196 'countReportsForReportee__video'
@@ -193,9 +200,9 @@ export enum ScopeNames {
193 '(' + 200 '(' +
194 'SELECT count(DISTINCT "videoAbuse"."id") ' + 201 'SELECT count(DISTINCT "videoAbuse"."id") ' +
195 'FROM "videoAbuse" ' + 202 'FROM "videoAbuse" ' +
196 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` + 203 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuse->Video->VideoChannel"."accountId" ` +
197 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` + 204 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
198 `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + 205 `CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
199 ')' 206 ')'
200 ), 207 ),
201 'countReportsForReportee__deletedVideo' 208 'countReportsForReportee__deletedVideo'
@@ -204,32 +211,47 @@ export enum ScopeNames {
204 }, 211 },
205 include: [ 212 include: [
206 { 213 {
207 model: AccountModel, 214 model: AccountModel.scope(AccountScopeNames.SUMMARY),
215 as: 'ReporterAccount',
208 required: true, 216 required: true,
209 where: searchAttribute(options.searchReporter, 'name') 217 where: searchAttribute(options.searchReporter, 'name')
210 }, 218 },
211 { 219 {
212 model: VideoModel, 220 model: AccountModel.scope(AccountScopeNames.SUMMARY),
213 required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel), 221 as: 'FlaggedAccount',
214 where: searchAttribute(options.searchVideo, 'name'), 222 required: true,
223 where: searchAttribute(options.searchReportee, 'name')
224 },
225 {
226 model: VideoAbuseModel,
227 required: options.filter === 'video' || !!options.videoIs || videoRequired,
215 include: [ 228 include: [
216 { 229 {
217 model: ThumbnailModel 230 model: VideoModel,
218 }, 231 required: videoRequired,
219 { 232 where: searchAttribute(options.searchVideo, 'name'),
220 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
221 where: searchAttribute(options.searchVideoChannel, 'name'),
222 include: [ 233 include: [
223 { 234 {
224 model: AccountModel, 235 model: ThumbnailModel
225 where: searchAttribute(options.searchReportee, 'name') 236 },
237 {
238 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: false } as SummaryOptions ] }),
239 where: searchAttribute(options.searchVideoChannel, 'name'),
240 required: true,
241 include: [
242 {
243 model: AccountModel.scope(AccountScopeNames.SUMMARY),
244 required: true,
245 where: searchAttribute(options.searchReportee, 'name')
246 }
247 ]
248 },
249 {
250 attributes: [ 'id', 'reason', 'unfederated' ],
251 model: VideoBlacklistModel,
252 required: onlyBlacklisted
226 } 253 }
227 ] 254 ]
228 },
229 {
230 attributes: [ 'id', 'reason', 'unfederated' ],
231 model: VideoBlacklistModel,
232 required: onlyBlacklisted
233 } 255 }
234 ] 256 ]
235 } 257 }
@@ -239,55 +261,40 @@ export enum ScopeNames {
239 } 261 }
240})) 262}))
241@Table({ 263@Table({
242 tableName: 'videoAbuse', 264 tableName: 'abuse',
243 indexes: [ 265 indexes: [
244 { 266 {
245 fields: [ 'videoId' ] 267 fields: [ 'reporterAccountId' ]
246 }, 268 },
247 { 269 {
248 fields: [ 'reporterAccountId' ] 270 fields: [ 'flaggedAccountId' ]
249 } 271 }
250 ] 272 ]
251}) 273})
252export class VideoAbuseModel extends Model<VideoAbuseModel> { 274export class AbuseModel extends Model<AbuseModel> {
253 275
254 @AllowNull(false) 276 @AllowNull(false)
255 @Default(null) 277 @Default(null)
256 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) 278 @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
257 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) 279 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
258 reason: string 280 reason: string
259 281
260 @AllowNull(false) 282 @AllowNull(false)
261 @Default(null) 283 @Default(null)
262 @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state')) 284 @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
263 @Column 285 @Column
264 state: VideoAbuseState 286 state: AbuseState
265 287
266 @AllowNull(true) 288 @AllowNull(true)
267 @Default(null) 289 @Default(null)
268 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true)) 290 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
269 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) 291 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
270 moderationComment: string 292 moderationComment: string
271 293
272 @AllowNull(true) 294 @AllowNull(true)
273 @Default(null) 295 @Default(null)
274 @Column(DataType.JSONB)
275 deletedVideo: VideoDetails
276
277 @AllowNull(true)
278 @Default(null)
279 @Column(DataType.ARRAY(DataType.INTEGER)) 296 @Column(DataType.ARRAY(DataType.INTEGER))
280 predefinedReasons: VideoAbusePredefinedReasons[] 297 predefinedReasons: AbusePredefinedReasons[]
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 298
292 @CreatedAt 299 @CreatedAt
293 createdAt: Date 300 createdAt: Date
@@ -301,36 +308,65 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
301 308
302 @BelongsTo(() => AccountModel, { 309 @BelongsTo(() => AccountModel, {
303 foreignKey: { 310 foreignKey: {
311 name: 'reporterAccountId',
304 allowNull: true 312 allowNull: true
305 }, 313 },
314 as: 'ReporterAccount',
306 onDelete: 'set null' 315 onDelete: 'set null'
307 }) 316 })
308 Account: AccountModel 317 ReporterAccount: AccountModel
309 318
310 @ForeignKey(() => VideoModel) 319 @ForeignKey(() => AccountModel)
311 @Column 320 @Column
312 videoId: number 321 flaggedAccountId: number
313 322
314 @BelongsTo(() => VideoModel, { 323 @BelongsTo(() => AccountModel, {
315 foreignKey: { 324 foreignKey: {
325 name: 'flaggedAccountId',
316 allowNull: true 326 allowNull: true
317 }, 327 },
328 as: 'FlaggedAccount',
318 onDelete: 'set null' 329 onDelete: 'set null'
319 }) 330 })
320 Video: VideoModel 331 FlaggedAccount: AccountModel
332
333 @HasOne(() => VideoCommentAbuseModel, {
334 foreignKey: {
335 name: 'abuseId',
336 allowNull: false
337 },
338 onDelete: 'cascade'
339 })
340 VideoCommentAbuse: VideoCommentAbuseModel
321 341
322 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> { 342 @HasOne(() => VideoAbuseModel, {
323 const videoAttributes = {} 343 foreignKey: {
324 if (videoId) videoAttributes['videoId'] = videoId 344 name: 'abuseId',
325 if (uuid) videoAttributes['deletedVideo'] = { uuid } 345 allowNull: false
346 },
347 onDelete: 'cascade'
348 })
349 VideoAbuse: VideoAbuseModel
350
351 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
352 const videoWhere: WhereOptions = {}
353
354 if (videoId) videoWhere.videoId = videoId
355 if (uuid) videoWhere.deletedVideo = { uuid }
326 356
327 const query = { 357 const query = {
358 include: [
359 {
360 model: VideoAbuseModel,
361 required: true,
362 where: videoWhere
363 }
364 ],
328 where: { 365 where: {
329 id, 366 id
330 ...videoAttributes
331 } 367 }
332 } 368 }
333 return VideoAbuseModel.findOne(query) 369 return AbuseModel.findOne(query)
334 } 370 }
335 371
336 static listForApi (parameters: { 372 static listForApi (parameters: {
@@ -338,13 +374,15 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
338 count: number 374 count: number
339 sort: string 375 sort: string
340 376
377 filter?: AbuseFilter
378
341 serverAccountId: number 379 serverAccountId: number
342 user?: MUserAccountId 380 user?: MUserAccountId
343 381
344 id?: number 382 id?: number
345 predefinedReason?: VideoAbusePredefinedReasonsString 383 predefinedReason?: AbusePredefinedReasonsString
346 state?: VideoAbuseState 384 state?: AbuseState
347 videoIs?: VideoAbuseVideoIs 385 videoIs?: AbuseVideoIs
348 386
349 search?: string 387 search?: string
350 searchReporter?: string 388 searchReporter?: string
@@ -364,24 +402,26 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
364 predefinedReason, 402 predefinedReason,
365 searchReportee, 403 searchReportee,
366 searchVideo, 404 searchVideo,
405 filter,
367 searchVideoChannel, 406 searchVideoChannel,
368 searchReporter, 407 searchReporter,
369 id 408 id
370 } = parameters 409 } = parameters
371 410
372 const userAccountId = user ? user.Account.id : undefined 411 const userAccountId = user ? user.Account.id : undefined
373 const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined 412 const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
374 413
375 const query = { 414 const query = {
376 offset: start, 415 offset: start,
377 limit: count, 416 limit: count,
378 order: getSort(sort), 417 order: getSort(sort),
379 col: 'VideoAbuseModel.id', 418 col: 'AbuseModel.id',
380 distinct: true 419 distinct: true
381 } 420 }
382 421
383 const filters = { 422 const filters = {
384 id, 423 id,
424 filter,
385 predefinedReasonId, 425 predefinedReasonId,
386 search, 426 search,
387 state, 427 state,
@@ -394,7 +434,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
394 userAccountId 434 userAccountId
395 } 435 }
396 436
397 return VideoAbuseModel 437 return AbuseModel
398 .scope([ 438 .scope([
399 { method: [ ScopeNames.FOR_API, filters ] } 439 { method: [ ScopeNames.FOR_API, filters ] }
400 ]) 440 ])
@@ -404,8 +444,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
404 }) 444 })
405 } 445 }
406 446
407 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { 447 toFormattedJSON (this: MAbuseFormattable): Abuse {
408 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) 448 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
409 const countReportsForVideo = this.get('countReportsForVideo') as number 449 const countReportsForVideo = this.get('countReportsForVideo') as number
410 const nthReportForVideo = this.get('nthReportForVideo') as number 450 const nthReportForVideo = this.get('nthReportForVideo') as number
411 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number 451 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
@@ -413,51 +453,70 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
413 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number 453 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
414 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number 454 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
415 455
416 const video = this.Video 456 let video: VideoAbuse
417 ? this.Video 457
418 : this.deletedVideo 458 if (this.VideoAbuse) {
459 const abuseModel = this.VideoAbuse
460 const entity = abuseModel.Video || abuseModel.deletedVideo
461
462 video = {
463 id: entity.id,
464 uuid: entity.uuid,
465 name: entity.name,
466 nsfw: entity.nsfw,
467
468 startAt: abuseModel.startAt,
469 endAt: abuseModel.endAt,
470
471 deleted: !abuseModel.Video,
472 blacklisted: abuseModel.Video?.isBlacklisted() || false,
473 thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
474 channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel
475 }
476 }
419 477
420 return { 478 return {
421 id: this.id, 479 id: this.id,
422 reason: this.reason, 480 reason: this.reason,
423 predefinedReasons, 481 predefinedReasons,
424 reporterAccount: this.Account.toFormattedJSON(), 482
483 reporterAccount: this.ReporterAccount.toFormattedJSON(),
484
425 state: { 485 state: {
426 id: this.state, 486 id: this.state,
427 label: VideoAbuseModel.getStateLabel(this.state) 487 label: AbuseModel.getStateLabel(this.state)
428 }, 488 },
489
429 moderationComment: this.moderationComment, 490 moderationComment: this.moderationComment,
430 video: { 491
431 id: video.id, 492 video,
432 uuid: video.uuid, 493 comment: null,
433 name: video.name, 494
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, 495 createdAt: this.createdAt,
441 updatedAt: this.updatedAt, 496 updatedAt: this.updatedAt,
442 startAt: this.startAt,
443 endAt: this.endAt,
444 count: countReportsForVideo || 0, 497 count: countReportsForVideo || 0,
445 nth: nthReportForVideo || 0, 498 nth: nthReportForVideo || 0,
446 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), 499 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
447 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0) 500 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0),
501
502 // FIXME: deprecated in 2.3, remove this
503 startAt: null,
504 endAt: null
448 } 505 }
449 } 506 }
450 507
451 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject { 508 toActivityPubObject (this: MAbuseAP): AbuseObject {
452 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) 509 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
510
511 const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
453 512
454 const startAt = this.startAt 513 const startAt = this.VideoAbuse?.startAt
455 const endAt = this.endAt 514 const endAt = this.VideoAbuse?.endAt
456 515
457 return { 516 return {
458 type: 'Flag' as 'Flag', 517 type: 'Flag' as 'Flag',
459 content: this.reason, 518 content: this.reason,
460 object: this.Video.url, 519 object,
461 tag: predefinedReasons.map(r => ({ 520 tag: predefinedReasons.map(r => ({
462 type: 'Hashtag' as 'Hashtag', 521 type: 'Hashtag' as 'Hashtag',
463 name: r 522 name: r
@@ -468,12 +527,12 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
468 } 527 }
469 528
470 private static getStateLabel (id: number) { 529 private static getStateLabel (id: number) {
471 return VIDEO_ABUSE_STATES[id] || 'Unknown' 530 return ABUSE_STATES[id] || 'Unknown'
472 } 531 }
473 532
474 private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] { 533 private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] {
475 return (predefinedReasons || []) 534 return (predefinedReasons || [])
476 .filter(r => r in VideoAbusePredefinedReasons) 535 .filter(r => r in AbusePredefinedReasons)
477 .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString) 536 .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString)
478 } 537 }
479} 538}
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 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoDetails } from '@shared/models'
3import { VideoModel } from '../video/video'
4import { AbuseModel } from './abuse'
5
6@Table({
7 tableName: 'videoAbuse',
8 indexes: [
9 {
10 fields: [ 'abuseId' ]
11 },
12 {
13 fields: [ 'videoId' ]
14 }
15 ]
16})
17export 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..b4cc2762e
--- /dev/null
+++ b/server/models/abuse/video-comment-abuse.ts
@@ -0,0 +1,53 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoComment } from '@shared/models'
3import { VideoCommentModel } from '../video/video-comment'
4import { AbuseModel } from './abuse'
5
6@Table({
7 tableName: 'commentAbuse',
8 indexes: [
9 {
10 fields: [ 'abuseId' ]
11 },
12 {
13 fields: [ 'videoCommentId' ]
14 }
15 ]
16})
17export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> {
18
19 @CreatedAt
20 createdAt: Date
21
22 @UpdatedAt
23 updatedAt: Date
24
25 @AllowNull(true)
26 @Default(null)
27 @Column(DataType.JSONB)
28 deletedComment: VideoComment
29
30 @ForeignKey(() => AbuseModel)
31 @Column
32 abuseId: number
33
34 @BelongsTo(() => AbuseModel, {
35 foreignKey: {
36 allowNull: false
37 },
38 onDelete: 'cascade'
39 })
40 Abuse: AbuseModel
41
42 @ForeignKey(() => VideoCommentModel)
43 @Column
44 videoCommentId: number
45
46 @BelongsTo(() => VideoCommentModel, {
47 foreignKey: {
48 allowNull: true
49 },
50 onDelete: 'set null'
51 })
52 VideoComment: VideoCommentModel
53}
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 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from './account'
3import { getSort, searchAttribute } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist'
5import { Op } from 'sequelize'
6import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { Op } from 'sequelize'
3import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
7import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' 4import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
5import { AccountBlock } from '../../../shared/models'
8import { ActorModel } from '../activitypub/actor' 6import { ActorModel } from '../activitypub/actor'
9import { ServerModel } from '../server/server' 7import { ServerModel } from '../server/server'
8import { getSort, searchAttribute } from '../utils'
9import { AccountModel } from './account'
10 10
11enum ScopeNames { 11enum 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..466d6258e 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -388,6 +388,10 @@ export class AccountModel extends Model<AccountModel> {
388 .findAll(query) 388 .findAll(query)
389 } 389 }
390 390
391 getClientUrl () {
392 return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
393 }
394
391 toFormattedJSON (this: MAccountFormattable): Account { 395 toFormattedJSON (this: MAccountFormattable): Account {
392 const actor = this.Actor.toFormattedJSON() 396 const actor = this.Actor.toFormattedJSON()
393 const account = { 397 const account = {
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 30985bb0f..07db5a2db 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -1,22 +1,24 @@
1import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
1import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
2import { UserNotification, UserNotificationType } from '../../../shared' 4import { UserNotification, UserNotificationType } from '../../../shared'
3import { getSort, throwIfNotValid } from '../utils'
4import { isBooleanValid } from '../../helpers/custom-validators/misc' 5import { isBooleanValid } from '../../helpers/custom-validators/misc'
5import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' 6import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
6import { UserModel } from './user' 7import { AbuseModel } from '../abuse/abuse'
7import { VideoModel } from '../video/video' 8import { VideoAbuseModel } from '../abuse/video-abuse'
8import { VideoCommentModel } from '../video/video-comment' 9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
9import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
10import { VideoChannelModel } from '../video/video-channel'
11import { AccountModel } from './account'
12import { VideoAbuseModel } from '../video/video-abuse'
13import { VideoBlacklistModel } from '../video/video-blacklist'
14import { VideoImportModel } from '../video/video-import'
15import { ActorModel } from '../activitypub/actor' 10import { ActorModel } from '../activitypub/actor'
16import { ActorFollowModel } from '../activitypub/actor-follow' 11import { ActorFollowModel } from '../activitypub/actor-follow'
17import { AvatarModel } from '../avatar/avatar' 12import { AvatarModel } from '../avatar/avatar'
18import { ServerModel } from '../server/server' 13import { ServerModel } from '../server/server'
19import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' 14import { getSort, throwIfNotValid } from '../utils'
15import { VideoModel } from '../video/video'
16import { VideoBlacklistModel } from '../video/video-blacklist'
17import { VideoChannelModel } from '../video/video-channel'
18import { VideoCommentModel } from '../video/video-comment'
19import { VideoImportModel } from '../video/video-import'
20import { AccountModel } from './account'
21import { UserModel } from './user'
20 22
21enum ScopeNames { 23enum 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: [ '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,27 @@ 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 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
496 }
497 } : undefined
498
499 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
500
501 const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined
502
503 return {
504 id: abuse.id,
505 video: videoAbuse,
506 comment: commentAbuse,
507 account: accountAbuse
508 }
509 }
510
459 formatActor ( 511 formatActor (
460 this: UserNotificationModelForApi, 512 this: UserNotificationModelForApi,
461 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor 513 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index de193131a..f21eff04b 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -19,7 +19,7 @@ import {
19 Table, 19 Table,
20 UpdatedAt 20 UpdatedAt
21} from 'sequelize-typescript' 21} from 'sequelize-typescript'
22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' 22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isNoInstanceConfigWarningModal, 25 isNoInstanceConfigWarningModal,
@@ -169,7 +169,7 @@ enum ScopeNames {
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("videoAbuse"."id") AS "abuses", ' +
172 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` + 172 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
173 'FROM "videoAbuse" ' + 173 'FROM "videoAbuse" ' +
174 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + 174 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index 30f0525e5..68cd72ee7 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -1,11 +1,11 @@
1import * as Bluebird from 'bluebird'
2import { Op } from 'sequelize'
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 3import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
4import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
5import { ServerBlock } from '@shared/models'
2import { AccountModel } from '../account/account' 6import { AccountModel } from '../account/account'
3import { ServerModel } from './server'
4import { ServerBlock } from '../../../shared/models/blocklist'
5import { getSort, searchAttribute } from '../utils' 7import { getSort, searchAttribute } from '../utils'
6import * as Bluebird from 'bluebird' 8import { ServerModel } from './server'
7import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
8import { Op } from 'sequelize'
9 9
10enum ScopeNames { 10enum ScopeNames {
11 WITH_ACCOUNT = 'WITH_ACCOUNT', 11 WITH_ACCOUNT = 'WITH_ACCOUNT',
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index e2718300e..272bba0e1 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,4 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { remove } from 'fs-extra'
2import { maxBy, minBy, pick } from 'lodash' 3import { maxBy, minBy, pick } from 'lodash'
3import { join } from 'path' 4import { join } from 'path'
4import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 5import { 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'
26import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
29import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
30import { getServerActor } from '@server/models/application/application'
31import { ModelCache } from '@server/models/model-cache'
32import { VideoFile } from '@shared/models/videos/video-file.model'
33import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
27import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 34import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
28import { Video, VideoDetails } from '../../../shared/models/videos' 35import { Video, VideoDetails } from '../../../shared/models/videos'
36import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
29import { VideoFilter } from '../../../shared/models/videos/video-query.type' 37import { VideoFilter } from '../../../shared/models/videos/video-query.type'
38import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
30import { peertubeTruncate } from '../../helpers/core-utils' 39import { peertubeTruncate } from '../../helpers/core-utils'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 40import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { isBooleanValid } from '../../helpers/custom-validators/misc' 41import { isBooleanValid } from '../../helpers/custom-validators/misc'
@@ -43,6 +52,7 @@ import {
43} from '../../helpers/custom-validators/videos' 52} from '../../helpers/custom-validators/videos'
44import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' 53import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
45import { logger } from '../../helpers/logger' 54import { logger } from '../../helpers/logger'
55import { CONFIG } from '../../initializers/config'
46import { 56import {
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'
61import { sendDeleteVideo } from '../../lib/activitypub/send' 71import { sendDeleteVideo } from '../../lib/activitypub/send'
62import { AccountModel } from '../account/account'
63import { AccountVideoRateModel } from '../account/account-video-rate'
64import { ActorModel } from '../activitypub/actor'
65import { AvatarModel } from '../avatar/avatar'
66import { ServerModel } from '../server/server'
67import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
68import { TagModel } from './tag'
69import { VideoAbuseModel } from './video-abuse'
70import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
71import { VideoCommentModel } from './video-comment'
72import { VideoFileModel } from './video-file'
73import { VideoShareModel } from './video-share'
74import { VideoTagModel } from './video-tag'
75import { ScheduleVideoUpdateModel } from './schedule-video-update'
76import { VideoCaptionModel } from './video-caption'
77import { VideoBlacklistModel } from './video-blacklist'
78import { remove } from 'fs-extra'
79import { VideoViewModel } from './video-view'
80import { VideoRedundancyModel } from '../redundancy/video-redundancy'
81import {
82 videoFilesModelToFormattedJSON,
83 VideoFormattingJSONOptions,
84 videoModelToActivityPubObject,
85 videoModelToFormattedDetailsJSON,
86 videoModelToFormattedJSON
87} from './video-format-utils'
88import { UserVideoHistoryModel } from '../account/user-video-history'
89import { VideoImportModel } from './video-import'
90import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
91import { VideoPlaylistElementModel } from './video-playlist-element'
92import { CONFIG } from '../../initializers/config'
93import { ThumbnailModel } from './thumbnail'
94import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
95import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
96import { 72import {
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'
121import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
122import { MThumbnail } from '../../types/models/video/thumbnail' 97import { MThumbnail } from '../../types/models/video/thumbnail'
123import { VideoFile } from '@shared/models/videos/video-file.model' 98import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
124import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 99import { VideoAbuseModel } from '../abuse/video-abuse'
125import { ModelCache } from '@server/models/model-cache' 100import { AccountModel } from '../account/account'
101import { AccountVideoRateModel } from '../account/account-video-rate'
102import { UserVideoHistoryModel } from '../account/user-video-history'
103import { ActorModel } from '../activitypub/actor'
104import { AvatarModel } from '../avatar/avatar'
105import { VideoRedundancyModel } from '../redundancy/video-redundancy'
106import { ServerModel } from '../server/server'
107import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
108import { ScheduleVideoUpdateModel } from './schedule-video-update'
109import { TagModel } from './tag'
110import { ThumbnailModel } from './thumbnail'
111import { VideoBlacklistModel } from './video-blacklist'
112import { VideoCaptionModel } from './video-caption'
113import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
114import { VideoCommentModel } from './video-comment'
115import { VideoFileModel } from './video-file'
116import {
117 videoFilesModelToFormattedJSON,
118 VideoFormattingJSONOptions,
119 videoModelToActivityPubObject,
120 videoModelToFormattedDetailsJSON,
121 videoModelToFormattedJSON
122} from './video-format-utils'
123import { VideoImportModel } from './video-import'
124import { VideoPlaylistElementModel } from './video-playlist-element'
126import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' 125import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
127import { buildNSFWFilter } from '@server/helpers/express-utils' 126import { VideoShareModel } from './video-share'
128import { getServerActor } from '@server/models/application/application' 127import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
129import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video" 128import { VideoTagModel } from './video-tag'
129import { VideoViewModel } from './video-view'
130 130
131export enum ScopeNames { 131export enum ScopeNames {
132 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 132 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index 557bf20eb..f122baef4 100644
--- a/server/tests/api/check-params/video-abuses.ts
+++ b/server/tests/api/check-params/video-abuses.ts
@@ -1,7 +1,7 @@
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
3import 'mocha' 3import 'mocha'
4 4import { AbuseState, VideoAbuseCreate } from '@shared/models'
5import { 5import {
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'
23import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos' 23
24// FIXME: deprecated in 2.3. Remove this controller
24 25
25describe('Test video abuses API validators', function () { 26describe('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 () {
@@ -190,7 +191,7 @@ describe('Test video abuses API validators', function () {
190 }) 191 })
191 192
192 it('Should succeed with the correct params', async function () { 193 it('Should succeed with the correct params', async function () {
193 const body = { state: VideoAbuseState.ACCEPTED } 194 const body = { state: AbuseState.ACCEPTED }
194 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body) 195 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body)
195 }) 196 })
196 }) 197 })
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 0a66bd1ce..88b68d977 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -2,7 +2,7 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index' 5import { MyUser, User, UserRole, Video, AbuseState, AbuseUpdate, VideoPlaylistType } from '@shared/models'
6import { 6import {
7 addVideoCommentThread, 7 addVideoCommentThread,
8 blockUser, 8 blockUser,
@@ -937,7 +937,7 @@ describe('Test users', function () {
937 expect(user2.videoAbusesCount).to.equal(1) // number of incriminations 937 expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
938 expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created 938 expect(user2.videoAbusesCreatedCount).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 updateVideoAbuse(server.url, server.accessToken, videoId, 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)
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index 7383bd991..20975aa4a 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -1,21 +1,21 @@
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
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
5import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos' 4import * as chai from 'chai'
5import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models'
6import { 6import {
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'
21import { doubleFollow } from '../../../../shared/extra-utils/server/follows' 21import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
@@ -29,9 +29,11 @@ import {
29 29
30const expect = chai.expect 30const expect = chai.expect
31 31
32// FIXME: deprecated in 2.3. Remove this controller
33
32describe('Test video abuses', function () { 34describe('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,7 +97,7 @@ 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)
@@ -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.count).to.equal(1)
140 expect(abuse1.nth).to.equal(1) 142 expect(abuse1.nth).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,7 +279,7 @@ 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.count).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.nth).to.equal(1, "wrong report position in report list for video 3")
@@ -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 @@
1export * from './account' 1export * from './account'
2export * from './moderation'
2export * from './oauth' 3export * from './oauth'
3export * from './server' 4export * from './server'
4export * from './user' 5export * from './user'
diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts
new file mode 100644
index 000000000..abbc93d6f
--- /dev/null
+++ b/server/types/models/moderation/abuse.ts
@@ -0,0 +1,97 @@
1import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
3import { PickWith } from '@shared/core-utils'
4import { AbuseModel } from '../../../models/abuse/abuse'
5import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account'
6import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo } from '../video'
7import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
8
9type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M>
10type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
11type UseCommentAbuse<K extends keyof VideoCommentAbuseModel, M> = PickWith<VideoCommentAbuseModel, K, M>
12
13// ############################################################################
14
15export type MAbuse = Omit<AbuseModel, 'VideoCommentAbuse' | 'VideoAbuse' | 'ReporterAccount' | 'FlaggedAccount' | 'toActivityPubObject'>
16
17export type MVideoAbuse = Omit<VideoAbuseModel, 'Abuse' | 'Video'>
18
19export type MCommentAbuse = Omit<VideoCommentAbuseModel, 'Abuse' | 'VideoComment'>
20
21// ############################################################################
22
23export type MVideoAbuseVideo =
24 MVideoAbuse &
25 UseVideoAbuse<'Video', MVideo>
26
27export type MVideoAbuseVideoUrl =
28 MVideoAbuse &
29 UseVideoAbuse<'Video', MVideoUrl>
30
31export type MVideoAbuseVideoFull =
32 MVideoAbuse &
33 UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles>
34
35export type MVideoAbuseFormattable =
36 MVideoAbuse &
37 UseVideoAbuse<'Video', Pick<MVideoAccountLightBlacklistAllFiles,
38 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>>
39
40// ############################################################################
41
42export type MCommentAbuseAccount =
43 MCommentAbuse &
44 UseCommentAbuse<'VideoComment', MCommentOwner>
45
46export type MCommentAbuseAccountVideo =
47 MCommentAbuse &
48 UseCommentAbuse<'VideoComment', MCommentOwnerVideo>
49
50export type MCommentAbuseUrl =
51 MCommentAbuse &
52 UseCommentAbuse<'VideoComment', MCommentUrl>
53
54// ############################################################################
55
56export type MAbuseId = Pick<AbuseModel, 'id'>
57
58export type MAbuseVideo =
59 MAbuse &
60 Pick<AbuseModel, 'toActivityPubObject'> &
61 Use<'VideoAbuse', MVideoAbuseVideo>
62
63export type MAbuseUrl =
64 MAbuse &
65 Use<'VideoAbuse', MVideoAbuseVideoUrl> &
66 Use<'VideoCommentAbuse', MCommentAbuseUrl>
67
68export type MAbuseAccountVideo =
69 MAbuse &
70 Pick<AbuseModel, 'toActivityPubObject'> &
71 Use<'VideoAbuse', MVideoAbuseVideoFull> &
72 Use<'ReporterAccount', MAccountDefault>
73
74export type MAbuseAP =
75 MAbuse &
76 Pick<AbuseModel, 'toActivityPubObject'> &
77 Use<'ReporterAccount', MAccountUrl> &
78 Use<'FlaggedAccount', MAccountUrl> &
79 Use<'VideoAbuse', MVideoAbuseVideo> &
80 Use<'VideoCommentAbuse', MCommentAbuseAccount>
81
82export type MAbuseFull =
83 MAbuse &
84 Pick<AbuseModel, 'toActivityPubObject'> &
85 Use<'ReporterAccount', MAccountLight> &
86 Use<'FlaggedAccount', MAccountLight> &
87 Use<'VideoAbuse', MVideoAbuseVideoFull> &
88 Use<'VideoCommentAbuse', MCommentAbuseAccountVideo>
89
90// ############################################################################
91
92// Format for API or AP object
93
94export type MAbuseFormattable =
95 MAbuse &
96 Use<'ReporterAccount', MAccountFormattable> &
97 Use<'VideoAbuse', MVideoAbuseFormattable>
diff --git a/server/types/models/moderation/index.ts b/server/types/models/moderation/index.ts
new file mode 100644
index 000000000..8bea1708f
--- /dev/null
+++ b/server/types/models/moderation/index.ts
@@ -0,0 +1 @@
export * from './abuse'
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts
index dd3de423b..92ea16768 100644
--- a/server/types/models/user/user-notification.ts
+++ b/server/types/models/user/user-notification.ts
@@ -1,16 +1,18 @@
1import { UserNotificationModel } from '../../../models/account/user-notification' 1import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
2import { PickWith, PickWithOpt } from '@shared/core-utils' 3import { PickWith, PickWithOpt } from '@shared/core-utils'
3import { VideoModel } from '../../../models/video/video' 4import { AbuseModel } from '../../../models/abuse/abuse'
5import { AccountModel } from '../../../models/account/account'
6import { UserNotificationModel } from '../../../models/account/user-notification'
4import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
5import { ServerModel } from '../../../models/server/server' 8import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
6import { AvatarModel } from '../../../models/avatar/avatar' 9import { AvatarModel } from '../../../models/avatar/avatar'
10import { ServerModel } from '../../../models/server/server'
11import { VideoModel } from '../../../models/video/video'
12import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
7import { VideoChannelModel } from '../../../models/video/video-channel' 13import { VideoChannelModel } from '../../../models/video/video-channel'
8import { AccountModel } from '../../../models/account/account'
9import { VideoCommentModel } from '../../../models/video/video-comment' 14import { VideoCommentModel } from '../../../models/video/video-comment'
10import { VideoAbuseModel } from '../../../models/video/video-abuse'
11import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
12import { VideoImportModel } from '../../../models/video/video-import' 15import { VideoImportModel } from '../../../models/video/video-import'
13import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
14 16
15type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M> 17type 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, '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
78export type MUserNotification = 92export 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'
2export * from './tag' 2export * from './tag'
3export * from './thumbnail' 3export * from './thumbnail'
4export * from './video' 4export * from './video'
5export * from './video-abuse'
6export * from './video-blacklist' 5export * from './video-blacklist'
7export * from './video-caption' 6export * from './video-caption'
8export * from './video-change-ownership' 7export * 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 @@
1import { VideoAbuseModel } from '../../../models/video/video-abuse'
2import { PickWith } from '@shared/core-utils'
3import { MVideoAccountLightBlacklistAllFiles, MVideo } from './video'
4import { MAccountDefault, MAccountFormattable } from '../account'
5
6type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
7
8// ############################################################################
9
10export type MVideoAbuse = Omit<VideoAbuseModel, 'Account' | 'Video' | 'toActivityPubObject'>
11
12// ############################################################################
13
14export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'>
15
16export type MVideoAbuseVideo =
17 MVideoAbuse &
18 Pick<VideoAbuseModel, 'toActivityPubObject'> &
19 Use<'Video', MVideo>
20
21export 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
31export 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 @@
1import { RegisterServerAuthExternalOptions } from '@server/types' 1import { RegisterServerAuthExternalOptions } from '@server/types'
2import { 2import {
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'
17export * from './videos/video-playlists' 17export * from './videos/video-playlists'
18export * from './users/users' 18export * from './users/users'
19export * from './users/accounts' 19export * from './users/accounts'
20export * from './moderation/abuses'
20export * from './videos/video-abuses' 21export * from './videos/video-abuses'
21export * from './videos/video-blacklist' 22export * from './videos/video-blacklist'
22export * from './videos/video-captions' 23export * from './videos/video-captions'
diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts
new file mode 100644
index 000000000..48a51e2b8
--- /dev/null
+++ b/shared/extra-utils/moderation/abuses.ts
@@ -0,0 +1,112 @@
1import * as request from 'supertest'
2import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
3import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
4
5function reportAbuse (
6 url: string,
7 token: string,
8 videoId: number | string,
9 reason: string,
10 predefinedReasons?: AbusePredefinedReasonsString[],
11 startAt?: number,
12 endAt?: number,
13 specialStatus = 200
14) {
15 const path = '/api/v1/videos/' + videoId + '/abuse'
16
17 return request(url)
18 .post(path)
19 .set('Accept', 'application/json')
20 .set('Authorization', 'Bearer ' + token)
21 .send({ reason, predefinedReasons, startAt, endAt })
22 .expect(specialStatus)
23}
24
25function getAbusesList (options: {
26 url: string
27 token: string
28 id?: number
29 predefinedReason?: AbusePredefinedReasonsString
30 search?: string
31 state?: AbuseState
32 videoIs?: AbuseVideoIs
33 searchReporter?: string
34 searchReportee?: string
35 searchVideo?: string
36 searchVideoChannel?: string
37}) {
38 const {
39 url,
40 token,
41 id,
42 predefinedReason,
43 search,
44 state,
45 videoIs,
46 searchReporter,
47 searchReportee,
48 searchVideo,
49 searchVideoChannel
50 } = options
51 const path = '/api/v1/videos/abuse'
52
53 const query = {
54 sort: 'createdAt',
55 id,
56 predefinedReason,
57 search,
58 state,
59 videoIs,
60 searchReporter,
61 searchReportee,
62 searchVideo,
63 searchVideoChannel
64 }
65
66 return makeGetRequest({
67 url,
68 path,
69 token,
70 query,
71 statusCodeExpected: 200
72 })
73}
74
75function updateAbuse (
76 url: string,
77 token: string,
78 videoId: string | number,
79 videoAbuseId: number,
80 body: AbuseUpdate,
81 statusCodeExpected = 204
82) {
83 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
84
85 return makePutBodyRequest({
86 url,
87 token,
88 path,
89 fields: body,
90 statusCodeExpected
91 })
92}
93
94function deleteAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) {
95 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
96
97 return makeDeleteRequest({
98 url,
99 token,
100 path,
101 statusCodeExpected
102 })
103}
104
105// ---------------------------------------------------------------------------
106
107export {
108 reportAbuse,
109 getAbusesList,
110 updateAbuse,
111 deleteAbuse
112}
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts
index a17a39de9..62f3418c5 100644
--- a/shared/extra-utils/users/user-notifications.ts
+++ b/shared/extra-utils/users/user-notifications.ts
@@ -443,11 +443,11 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
443 expect(notification).to.not.be.undefined 443 expect(notification).to.not.be.undefined
444 expect(notification.type).to.equal(notificationType) 444 expect(notification.type).to.equal(notificationType)
445 445
446 expect(notification.videoAbuse.id).to.be.a('number') 446 expect(notification.abuse.id).to.be.a('number')
447 checkVideo(notification.videoAbuse.video, videoName, videoUUID) 447 checkVideo(notification.abuse.video, videoName, videoUUID)
448 } else { 448 } else {
449 expect(notification).to.satisfy((n: UserNotification) => { 449 expect(notification).to.satisfy((n: UserNotification) => {
450 return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID 450 return n === undefined || n.abuse === undefined || n.abuse.video.uuid !== videoUUID
451 }) 451 })
452 } 452 }
453 } 453 }
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 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model' 2import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
3import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests' 3import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
4import { VideoAbuseState, VideoAbusePredefinedReasonsString } from '@shared/models' 4
5import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' 5// FIXME: deprecated in 2.3. Remove this file
6 6
7function reportVideoAbuse ( 7function 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 @@
1import { ActivityPubActor } from './activitypub-actor' 1import { ActivityPubActor } from './activitypub-actor'
2import { ActivityPubSignature } from './activitypub-signature' 2import { ActivityPubSignature } from './activitypub-signature'
3import { CacheFileObject, VideoTorrentObject, ActivityFlagReasonObject } from './objects' 3import { ActivityFlagReasonObject, CacheFileObject, VideoTorrentObject } from './objects'
4import { AbuseObject } from './objects/abuse-object'
4import { DislikeObject } from './objects/dislike-object' 5import { DislikeObject } from './objects/dislike-object'
5import { VideoAbuseObject } from './objects/video-abuse-object'
6import { VideoCommentObject } from './objects/video-comment-object'
7import { ViewObject } from './objects/view-object'
8import { APObject } from './objects/object.model' 6import { APObject } from './objects/object.model'
9import { PlaylistObject } from './objects/playlist-object' 7import { PlaylistObject } from './objects/playlist-object'
8import { VideoCommentObject } from './objects/video-comment-object'
9import { ViewObject } from './objects/view-object'
10 10
11export type Activity = 11export type Activity =
12 ActivityCreate | 12 ActivityCreate |
@@ -53,7 +53,7 @@ export interface BaseActivity {
53 53
54export interface ActivityCreate extends BaseActivity { 54export 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
59export interface ActivityUpdate extends BaseActivity { 59export 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 @@
1import { ActivityFlagReasonObject } from './common-objects' 1import { ActivityFlagReasonObject } from './common-objects'
2 2
3export interface VideoAbuseObject { 3export 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 @@
1import { VideoAbusePredefinedReasonsString } from '@shared/models/videos' 1import { AbusePredefinedReasonsString } from '@shared/models'
2 2
3export interface ActivityIdentifierObject { 3export interface ActivityIdentifierObject {
4 identifier: string 4 identifier: string
@@ -85,7 +85,7 @@ export interface ActivityMentionObject {
85 85
86export interface ActivityFlagReasonObject { 86export interface ActivityFlagReasonObject {
87 type: 'Hashtag' 87 type: 'Hashtag'
88 name: VideoAbusePredefinedReasonsString 88 name: AbusePredefinedReasonsString
89} 89}
90 90
91export type ActivityTagObject = 91export 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 @@
1export * from './abuse-object'
1export * from './cache-file-object' 2export * from './cache-file-object'
2export * from './common-objects' 3export * from './common-objects'
3export * from './video-abuse-object' 4export * from './dislike-object'
4export * from './video-torrent-object' 5export * from './video-torrent-object'
5export * from './view-object' 6export * from './view-object'
6export * 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 @@
1export * from './activitypub' 1export * from './activitypub'
2export * from './actors' 2export * from './actors'
3export * from './avatars' 3export * from './avatars'
4export * from './blocklist' 4export * from './moderation'
5export * from './bulk' 5export * from './bulk'
6export * from './redundancy' 6export * from './redundancy'
7export * from './users' 7export * from './users'
@@ -14,4 +14,3 @@ export * from './search'
14export * from './server' 14export * from './server'
15export * from './oauth-client-local.model' 15export * from './oauth-client-local.model'
16export * from './result-list.model' 16export * from './result-list.model'
17export * from './server/server-config.model'
diff --git a/shared/models/moderation/abuse/abuse-create.model.ts b/shared/models/moderation/abuse/abuse-create.model.ts
new file mode 100644
index 000000000..c0d04e46d
--- /dev/null
+++ b/shared/models/moderation/abuse/abuse-create.model.ts
@@ -0,0 +1,26 @@
1import { AbusePredefinedReasonsString } from './abuse-reason.model'
2
3export interface AbuseCreate {
4 accountId: number
5
6 reason: string
7 predefinedReasons?: AbusePredefinedReasonsString[]
8
9 video?: {
10 id: number
11 startAt?: number
12 endAt?: number
13 }
14
15 comment?: {
16 id: number
17 }
18}
19
20// FIXME: deprecated in 2.3. Remove it
21export interface VideoAbuseCreate {
22 reason: string
23 predefinedReasons?: AbusePredefinedReasonsString[]
24 startAt?: number
25 endAt?: number
26}
diff --git a/shared/models/moderation/abuse/abuse-filter.ts b/shared/models/moderation/abuse/abuse-filter.ts
new file mode 100644
index 000000000..03303bbab
--- /dev/null
+++ b/shared/models/moderation/abuse/abuse-filter.ts
@@ -0,0 +1 @@
export type AbuseFilter = 'video' | 'comment'
diff --git a/shared/models/moderation/abuse/abuse-reason.model.ts b/shared/models/moderation/abuse/abuse-reason.model.ts
new file mode 100644
index 000000000..36875969d
--- /dev/null
+++ b/shared/models/moderation/abuse/abuse-reason.model.ts
@@ -0,0 +1,33 @@
1export 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
12export type AbusePredefinedReasonsString =
13 'violentOrRepulsive' |
14 'hatefulOrAbusive' |
15 'spamOrMisleading' |
16 'privacy' |
17 'rights' |
18 'serverRules' |
19 'thumbnails' |
20 'captions'
21
22export 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 @@
1export enum VideoAbuseState { 1export 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 @@
1import { AbuseState } from './abuse-state.model'
2
3export 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..9ff150c4a
--- /dev/null
+++ b/shared/models/moderation/abuse/abuse.model.ts
@@ -0,0 +1,53 @@
1import { Account } from '../../actors/account.model'
2import { AbuseState } from './abuse-state.model'
3import { AbusePredefinedReasonsString } from './abuse-reason.model'
4import { VideoConstant } from '../../videos/video-constant.model'
5import { VideoChannel } from '../../videos/channel/video-channel.model'
6
7export interface VideoAbuse {
8 id: number
9 name: string
10 uuid: string
11 nsfw: boolean
12 deleted: boolean
13 blacklisted: boolean
14
15 startAt: number | null
16 endAt: number | null
17
18 thumbnailPath?: string
19 channel?: VideoChannel
20}
21
22export interface VideoCommentAbuse {
23 id: number
24 account?: Account
25 text: string
26 deleted: boolean
27}
28
29export interface Abuse {
30 id: number
31 reason: string
32 predefinedReasons?: AbusePredefinedReasonsString[]
33 reporterAccount: Account
34
35 state: VideoConstant<AbuseState>
36 moderationComment?: string
37
38 video?: VideoAbuse
39 comment?: VideoCommentAbuse
40
41 createdAt: Date
42 updatedAt: Date
43
44 // FIXME: deprecated in 2.3, remove this
45 startAt: null
46 endAt: null
47
48 count?: number
49 nth?: number
50
51 countReportsForReporter?: number
52 countReportsForReportee?: number
53}
diff --git a/shared/models/moderation/abuse/index.ts b/shared/models/moderation/abuse/index.ts
new file mode 100644
index 000000000..32a6b4e6c
--- /dev/null
+++ b/shared/models/moderation/abuse/index.ts
@@ -0,0 +1,6 @@
1export * from './abuse-create.model'
2export * from './abuse-reason.model'
3export * from './abuse-state.model'
4export * from './abuse-update.model'
5export * from './abuse-video-is.type'
6export * 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 @@
1export * from './abuse'
1export * from './account-block.model' 2export * from './account-block.model'
2export * from './server-block.model' 3export * 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.model.ts b/shared/models/users/user-notification.model.ts
index e9be1ca7f..39090f5a1 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -64,9 +64,20 @@ export interface UserNotification {
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 uuid: string
77 }
78 }
79
80 account?: ActorInfo
70 } 81 }
71 82
72 videoBlacklist?: { 83 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/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 @@
1export * from './video-abuse-create.model'
2export * from './video-abuse-reason.model'
3export * from './video-abuse-state.model'
4export * from './video-abuse-update.model'
5export * from './video-abuse-video-is.type'
6export * 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 @@
1import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
2
3export 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 @@
1export 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
12export type VideoAbusePredefinedReasonsString =
13 'violentOrRepulsive' |
14 'hatefulOrAbusive' |
15 'spamOrMisleading' |
16 'privacy' |
17 'rights' |
18 'serverRules' |
19 'thumbnails' |
20 'captions'
21
22export 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 @@
1import { VideoAbuseState } from './video-abuse-state.model'
2
3export 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 @@
1export 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 @@
1import { Account } from '../../actors/index'
2import { VideoConstant } from '../video-constant.model'
3import { VideoAbuseState } from './video-abuse-state.model'
4import { VideoChannel } from '../channel/video-channel.model'
5import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
6
7export 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 @@
1export * from './abuse'
2export * from './blacklist' 1export * from './blacklist'
3export * from './caption' 2export * from './caption'
4export * from './channel' 3export * from './channel'