aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/admin.component.ts6
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html8
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts4
-rw-r--r--client/src/app/core/users/user.model.ts14
-rw-r--r--server/controllers/api/users/my-notifications.ts2
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0470-cleanup-indexes.ts (renamed from server/initializers/migrations/0470-cleaup-indexes.ts)0
-rw-r--r--server/initializers/migrations/0520-abuses-split.ts92
-rw-r--r--server/lib/emailer.ts2
-rw-r--r--server/lib/notifier.ts8
-rw-r--r--server/lib/user.ts2
-rw-r--r--server/middlewares/validators/user-notifications.ts4
-rw-r--r--server/models/abuse/abuse.ts130
-rw-r--r--server/models/account/account.ts5
-rw-r--r--server/models/account/user-notification-setting.ts8
-rw-r--r--server/models/account/user.ts36
-rw-r--r--server/models/video/video-channel.ts3
-rw-r--r--server/tests/api/check-params/user-notifications.ts2
-rw-r--r--server/tests/api/ci-4.sh1
-rw-r--r--server/tests/api/index.ts1
-rw-r--r--server/tests/api/moderation/abuses.ts384
-rw-r--r--server/tests/api/moderation/blocklist.ts (renamed from server/tests/api/users/blocklist.ts)0
-rw-r--r--server/tests/api/moderation/index.ts2
-rw-r--r--server/tests/api/notifications/moderation-notifications.ts14
-rw-r--r--server/tests/api/server/email.ts12
-rw-r--r--server/tests/api/users/index.ts3
-rw-r--r--server/tests/api/users/users.ts40
-rw-r--r--server/tests/api/videos/video-abuse.ts12
-rw-r--r--server/types/models/moderation/abuse.ts1
-rw-r--r--shared/extra-utils/users/user-notifications.ts2
-rw-r--r--shared/models/moderation/abuse/abuse.model.ts25
-rw-r--r--shared/models/users/user-notification-setting.model.ts2
-rw-r--r--shared/models/users/user.model.ts9
-rw-r--r--support/doc/api/openapi.yaml22
34 files changed, 675 insertions, 183 deletions
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index 1e137e63e..87ed33a45 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -45,7 +45,7 @@ export class AdminComponent implements OnInit {
45 children: [] 45 children: []
46 } 46 }
47 47
48 if (this.hasVideoAbusesRight()) { 48 if (this.hasAbusesRight()) {
49 moderationItems.children.push({ 49 moderationItems.children.push({
50 label: this.i18n('Video reports'), 50 label: this.i18n('Video reports'),
51 routerLink: '/admin/moderation/video-abuses/list', 51 routerLink: '/admin/moderation/video-abuses/list',
@@ -76,7 +76,7 @@ export class AdminComponent implements OnInit {
76 76
77 if (this.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' }) 77 if (this.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' })
78 if (this.hasServerFollowRight()) this.menuEntries.push(federationItems) 78 if (this.hasServerFollowRight()) this.menuEntries.push(federationItems)
79 if (this.hasVideoAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems) 79 if (this.hasAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems)
80 if (this.hasConfigRight()) this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' }) 80 if (this.hasConfigRight()) this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' })
81 if (this.hasPluginsRight()) this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' }) 81 if (this.hasPluginsRight()) this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' })
82 if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.menuEntries.push({ label: this.i18n('System'), routerLink: '/admin/system' }) 82 if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.menuEntries.push({ label: this.i18n('System'), routerLink: '/admin/system' })
@@ -90,7 +90,7 @@ export class AdminComponent implements OnInit {
90 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW) 90 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW)
91 } 91 }
92 92
93 hasVideoAbusesRight () { 93 hasAbusesRight () {
94 return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES) 94 return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES)
95 } 95 }
96 96
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 297e6104c..2e7b322ca 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -37,14 +37,14 @@
37 </a> 37 </a>
38 </div> 38 </div>
39 <div> 39 <div>
40 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + user?.account.displayName + '&quot;' }"> 40 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + user?.account.displayName + '&quot;' }">
41 <div class="dashboard-num">{{ user.videoAbusesCount }}</div> 41 <div class="dashboard-num">{{ user.abusesCount }}</div>
42 <div class="dashboard-label" i18n>Incriminated in reports</div> 42 <div class="dashboard-label" i18n>Incriminated in reports</div>
43 </a> 43 </a>
44 </div> 44 </div>
45 <div> 45 <div>
46 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + user?.account.displayName + '&quot; state:accepted' }"> 46 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + user?.account.displayName + '&quot; state:accepted' }">
47 <div class="dashboard-num">{{ user.videoAbusesAcceptedCount }} / {{ user.videoAbusesCreatedCount }}</div> 47 <div class="dashboard-num">{{ user.abusesAcceptedCount }} / {{ user.abusesCreatedCount }}</div>
48 <div class="dashboard-label" i18n>Authored reports accepted</div> 48 <div class="dashboard-label" i18n>Authored reports accepted</div>
49 </a> 49 </a>
50 </div> 50 </div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index adc18b587..8562e564b 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -33,7 +33,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
33 this.labelNotifications = { 33 this.labelNotifications = {
34 newVideoFromSubscription: this.i18n('New video from your subscriptions'), 34 newVideoFromSubscription: this.i18n('New video from your subscriptions'),
35 newCommentOnMyVideo: this.i18n('New comment on your video'), 35 newCommentOnMyVideo: this.i18n('New comment on your video'),
36 videoAbuseAsModerator: this.i18n('New video abuse'), 36 abuseAsModerator: this.i18n('New abuse'),
37 videoAutoBlacklistAsModerator: this.i18n('Video blocked automatically waiting review'), 37 videoAutoBlacklistAsModerator: this.i18n('Video blocked automatically waiting review'),
38 blacklistOnMyVideo: this.i18n('One of your video is blocked/unblocked'), 38 blacklistOnMyVideo: this.i18n('One of your video is blocked/unblocked'),
39 myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'), 39 myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
@@ -47,7 +47,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
47 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] 47 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
48 48
49 this.rightNotifications = { 49 this.rightNotifications = {
50 videoAbuseAsModerator: UserRight.MANAGE_ABUSES, 50 abuseAsModerator: UserRight.MANAGE_ABUSES,
51 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, 51 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
52 newUserRegistration: UserRight.MANAGE_USERS, 52 newUserRegistration: UserRight.MANAGE_USERS,
53 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, 53 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts
index 8ecdf9fcd..31b9c2152 100644
--- a/client/src/app/core/users/user.model.ts
+++ b/client/src/app/core/users/user.model.ts
@@ -51,12 +51,14 @@ export class User implements UserServerModel {
51 videoQuotaDaily: number 51 videoQuotaDaily: number
52 videoQuotaUsed?: number 52 videoQuotaUsed?: number
53 videoQuotaUsedDaily?: number 53 videoQuotaUsedDaily?: number
54
54 videosCount?: number 55 videosCount?: number
55 videoAbusesCount?: number
56 videoAbusesAcceptedCount?: number
57 videoAbusesCreatedCount?: number
58 videoCommentsCount?: number 56 videoCommentsCount?: number
59 57
58 abusesCount?: number
59 abusesAcceptedCount?: number
60 abusesCreatedCount?: number
61
60 theme: string 62 theme: string
61 63
62 account: Account 64 account: Account
@@ -89,9 +91,9 @@ export class User implements UserServerModel {
89 this.videoQuotaUsed = hash.videoQuotaUsed 91 this.videoQuotaUsed = hash.videoQuotaUsed
90 this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily 92 this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
91 this.videosCount = hash.videosCount 93 this.videosCount = hash.videosCount
92 this.videoAbusesCount = hash.videoAbusesCount 94 this.abusesCount = hash.abusesCount
93 this.videoAbusesAcceptedCount = hash.videoAbusesAcceptedCount 95 this.abusesAcceptedCount = hash.abusesAcceptedCount
94 this.videoAbusesCreatedCount = hash.videoAbusesCreatedCount 96 this.abusesCreatedCount = hash.abusesCreatedCount
95 this.videoCommentsCount = hash.videoCommentsCount 97 this.videoCommentsCount = hash.videoCommentsCount
96 98
97 this.nsfwPolicy = hash.nsfwPolicy 99 this.nsfwPolicy = hash.nsfwPolicy
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index 017f5219e..0be51c128 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -68,7 +68,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
68 const values: UserNotificationSetting = { 68 const values: UserNotificationSetting = {
69 newVideoFromSubscription: body.newVideoFromSubscription, 69 newVideoFromSubscription: body.newVideoFromSubscription,
70 newCommentOnMyVideo: body.newCommentOnMyVideo, 70 newCommentOnMyVideo: body.newCommentOnMyVideo,
71 videoAbuseAsModerator: body.videoAbuseAsModerator, 71 abuseAsModerator: body.abuseAsModerator,
72 videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator, 72 videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
73 blacklistOnMyVideo: body.blacklistOnMyVideo, 73 blacklistOnMyVideo: body.blacklistOnMyVideo,
74 myVideoPublished: body.myVideoPublished, 74 myVideoPublished: body.myVideoPublished,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 8f86bbbef..2e9d3956e 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -23,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
23 23
24// --------------------------------------------------------------------------- 24// ---------------------------------------------------------------------------
25 25
26const LAST_MIGRATION_VERSION = 515 26const LAST_MIGRATION_VERSION = 520
27 27
28// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
29 29
diff --git a/server/initializers/migrations/0470-cleaup-indexes.ts b/server/initializers/migrations/0470-cleanup-indexes.ts
index 7365c30f8..7365c30f8 100644
--- a/server/initializers/migrations/0470-cleaup-indexes.ts
+++ b/server/initializers/migrations/0470-cleanup-indexes.ts
diff --git a/server/initializers/migrations/0520-abuses-split.ts b/server/initializers/migrations/0520-abuses-split.ts
new file mode 100644
index 000000000..5898d501f
--- /dev/null
+++ b/server/initializers/migrations/0520-abuses-split.ts
@@ -0,0 +1,92 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 await utils.queryInterface.renameTable('videoAbuse', 'abuse')
9
10 await utils.sequelize.query(`
11 ALTER TABLE "abuse"
12 ADD COLUMN "flaggedAccountId" INTEGER REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE
13 `)
14
15 await utils.sequelize.query(`
16 UPDATE "abuse" SET "videoId" = NULL
17 WHERE "videoId" NOT IN (SELECT "id" FROM "video")
18 `)
19
20 await utils.sequelize.query(`
21 UPDATE "abuse" SET "flaggedAccountId" = "videoChannel"."accountId"
22 FROM "video" INNER JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"
23 WHERE "abuse"."videoId" = "video"."id"
24 `)
25
26 await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_video_id;')
27 await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_reporter_account_id;')
28
29 await utils.sequelize.query(`
30 CREATE TABLE IF NOT EXISTS "videoAbuse" (
31 "id" serial,
32 "startAt" integer DEFAULT NULL,
33 "endAt" integer DEFAULT NULL,
34 "deletedVideo" jsonb DEFAULT NULL,
35 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
36 "videoId" integer REFERENCES "video" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
37 "createdAt" TIMESTAMP WITH time zone NOT NULL,
38 "updatedAt" timestamp WITH time zone NOT NULL,
39 PRIMARY KEY ("id")
40 );
41 `)
42
43 await utils.sequelize.query(`
44 CREATE TABLE IF NOT EXISTS "commentAbuse" (
45 "id" serial,
46 "deletedComment" jsonb DEFAULT NULL,
47 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
48 "videoCommentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
49 "createdAt" timestamp WITH time zone NOT NULL,
50 "updatedAt" timestamp WITH time zone NOT NULL,
51 "commentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
52 PRIMARY KEY ("id")
53 );
54 `)
55
56 await utils.sequelize.query(`
57 INSERT INTO "videoAbuse" ("startAt", "endAt", "deletedVideo", "abuseId", "videoId", "createdAt", "updatedAt")
58 SELECT "abuse"."startAt", "abuse"."endAt", "abuse"."deletedVideo", "abuse"."id", "abuse"."videoId",
59 "abuse"."createdAt", "abuse"."updatedAt"
60 FROM "abuse"
61 `)
62
63 await utils.queryInterface.removeColumn('abuse', 'startAt')
64 await utils.queryInterface.removeColumn('abuse', 'endAt')
65 await utils.queryInterface.removeColumn('abuse', 'deletedVideo')
66 await utils.queryInterface.removeColumn('abuse', 'videoId')
67
68 await utils.sequelize.query('DROP INDEX IF EXISTS user_notification_video_abuse_id')
69 await utils.queryInterface.renameColumn('userNotification', 'videoAbuseId', 'abuseId')
70 await utils.sequelize.query(
71 'ALTER TABLE "userNotification" RENAME CONSTRAINT "userNotification_videoAbuseId_fkey" TO "userNotification_abuseId_fkey"'
72 )
73
74 await utils.sequelize.query(
75 'ALTER TABLE "abuse" RENAME CONSTRAINT "videoAbuse_reporterAccountId_fkey" TO "abuse_reporterAccountId_fkey"'
76 )
77
78 await utils.sequelize.query(
79 'ALTER INDEX IF EXISTS "videoAbuse_pkey" RENAME TO "abuse_pkey"'
80 )
81
82 await utils.queryInterface.renameColumn('userNotificationSetting', 'videoAbuseAsModerator', 'abuseAsModerator')
83}
84
85function down (options) {
86 throw new Error('Not implemented.')
87}
88
89export {
90 up,
91 down
92}
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index e821aea5f..a5664408d 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -320,7 +320,7 @@ class Emailer {
320 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId() 320 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
321 321
322 emailPayload = { 322 emailPayload = {
323 template: 'comment-abuse-new', 323 template: 'video-comment-abuse-new',
324 to, 324 to,
325 subject: `New comment abuse report from ${reporter}`, 325 subject: `New comment abuse report from ${reporter}`,
326 locals: { 326 locals: {
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 40cff66d2..969e393fa 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -18,7 +18,7 @@ import { CONFIG } from '../initializers/config'
18import { AccountBlocklistModel } from '../models/account/account-blocklist' 18import { AccountBlocklistModel } from '../models/account/account-blocklist'
19import { UserModel } from '../models/account/user' 19import { UserModel } from '../models/account/user'
20import { UserNotificationModel } from '../models/account/user-notification' 20import { UserNotificationModel } from '../models/account/user-notification'
21import { MAbuseFull, MAbuseVideo, MAccountServer, MActorFollowFull } from '../types/models' 21import { MAbuseFull, MAccountServer, MActorFollowFull } from '../types/models'
22import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' 22import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
23import { isBlockedByServerOrAccount } from './blocklist' 23import { isBlockedByServerOrAccount } from './blocklist'
24import { Emailer } from './emailer' 24import { Emailer } from './emailer'
@@ -359,12 +359,14 @@ class Notifier {
359 const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) 359 const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
360 if (moderators.length === 0) return 360 if (moderators.length === 0) return
361 361
362 const url = abuseInstance.VideoAbuse?.Video?.url || abuseInstance.VideoCommentAbuse?.VideoComment?.url 362 const url = abuseInstance.VideoAbuse?.Video?.url ||
363 abuseInstance.VideoCommentAbuse?.VideoComment?.url ||
364 abuseInstance.FlaggedAccount.Actor.url
363 365
364 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url) 366 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
365 367
366 function settingGetter (user: MUserWithNotificationSetting) { 368 function settingGetter (user: MUserWithNotificationSetting) {
367 return user.NotificationSetting.videoAbuseAsModerator 369 return user.NotificationSetting.abuseAsModerator
368 } 370 }
369 371
370 async function notificationCreator (user: MUserWithNotificationSetting) { 372 async function notificationCreator (user: MUserWithNotificationSetting) {
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 43eef8ab1..642549879 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -133,7 +133,7 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
133 newCommentOnMyVideo: UserNotificationSettingValue.WEB, 133 newCommentOnMyVideo: UserNotificationSettingValue.WEB,
134 myVideoImportFinished: UserNotificationSettingValue.WEB, 134 myVideoImportFinished: UserNotificationSettingValue.WEB,
135 myVideoPublished: UserNotificationSettingValue.WEB, 135 myVideoPublished: UserNotificationSettingValue.WEB,
136 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 136 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
137 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 137 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
138 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 138 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
139 newUserRegistration: UserNotificationSettingValue.WEB, 139 newUserRegistration: UserNotificationSettingValue.WEB,
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts
index fbfcb0a4c..21a7be08d 100644
--- a/server/middlewares/validators/user-notifications.ts
+++ b/server/middlewares/validators/user-notifications.ts
@@ -25,8 +25,8 @@ const updateNotificationSettingsValidator = [
25 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), 25 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'),
26 body('newCommentOnMyVideo') 26 body('newCommentOnMyVideo')
27 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), 27 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'),
28 body('videoAbuseAsModerator') 28 body('abuseAsModerator')
29 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'), 29 .custom(isUserNotificationSettingValid).withMessage('Should have a valid abuse as moderator notification setting'),
30 body('videoAutoBlacklistAsModerator') 30 body('videoAutoBlacklistAsModerator')
31 .custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'), 31 .custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'),
32 body('blacklistOnMyVideo') 32 body('blacklistOnMyVideo')
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts
index 087c77bd3..9c17c4d51 100644
--- a/server/models/abuse/abuse.ts
+++ b/server/models/abuse/abuse.ts
@@ -31,15 +31,15 @@ import {
31} from '@shared/models' 31} from '@shared/models'
32import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' 32import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
33import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' 33import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
34import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' 34import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
35import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' 35import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
36import { ThumbnailModel } from '../video/thumbnail' 36import { ThumbnailModel } from '../video/thumbnail'
37import { VideoModel } from '../video/video' 37import { VideoModel } from '../video/video'
38import { VideoBlacklistModel } from '../video/video-blacklist' 38import { VideoBlacklistModel } from '../video/video-blacklist'
39import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' 39import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel'
40import { VideoCommentModel } from '../video/video-comment'
40import { VideoAbuseModel } from './video-abuse' 41import { VideoAbuseModel } from './video-abuse'
41import { VideoCommentAbuseModel } from './video-comment-abuse' 42import { VideoCommentAbuseModel } from './video-comment-abuse'
42import { VideoCommentModel } from '../video/video-comment'
43 43
44export enum ScopeNames { 44export enum ScopeNames {
45 FOR_API = 'FOR_API' 45 FOR_API = 'FOR_API'
@@ -149,7 +149,7 @@ export enum ScopeNames {
149 '(' + 149 '(' +
150 'SELECT count(*) ' + 150 'SELECT count(*) ' +
151 'FROM "videoAbuse" ' + 151 'FROM "videoAbuse" ' +
152 'WHERE "videoId" = "VideoAbuse"."videoId" ' + 152 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' +
153 ')' 153 ')'
154 ), 154 ),
155 'countReportsForVideo' 155 'countReportsForVideo'
@@ -164,7 +164,7 @@ export enum ScopeNames {
164 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + 164 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
165 'FROM "videoAbuse" ' + 165 'FROM "videoAbuse" ' +
166 ') t ' + 166 ') t ' +
167 'WHERE t.id = "VideoAbuse".id' + 167 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' +
168 ')' 168 ')'
169 ), 169 ),
170 'nthReportForVideo' 170 'nthReportForVideo'
@@ -172,51 +172,22 @@ export enum ScopeNames {
172 [ 172 [
173 literal( 173 literal(
174 '(' + 174 '(' +
175 'SELECT count("videoAbuse"."id") ' + 175 'SELECT count("abuse"."id") ' +
176 'FROM "videoAbuse" ' + 176 'FROM "abuse" ' +
177 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + 177 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' +
178 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
179 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
180 'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
181 ')'
182 ),
183 'countReportsForReporter__video'
184 ],
185 [
186 literal(
187 '(' +
188 'SELECT count(DISTINCT "videoAbuse"."id") ' +
189 'FROM "videoAbuse" ' +
190 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` +
191 ')'
192 ),
193 'countReportsForReporter__deletedVideo'
194 ],
195 [
196 literal(
197 '(' +
198 'SELECT count(DISTINCT "videoAbuse"."id") ' +
199 'FROM "videoAbuse" ' +
200 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
201 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
202 'INNER JOIN "account" ON ' +
203 '"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' +
204 `OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
205 ')' 178 ')'
206 ), 179 ),
207 'countReportsForReportee__video' 180 'countReportsForReporter'
208 ], 181 ],
209 [ 182 [
210 literal( 183 literal(
211 '(' + 184 '(' +
212 'SELECT count(DISTINCT "videoAbuse"."id") ' + 185 'SELECT count("abuse"."id") ' +
213 'FROM "videoAbuse" ' + 186 'FROM "abuse" ' +
214 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuse->Video->VideoChannel"."accountId" ` + 187 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' +
215 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
216 `CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
217 ')' 188 ')'
218 ), 189 ),
219 'countReportsForReportee__deletedVideo' 190 'countReportsForReportee'
220 ] 191 ]
221 ] 192 ]
222 }, 193 },
@@ -224,13 +195,18 @@ export enum ScopeNames {
224 { 195 {
225 model: AccountModel.scope(AccountScopeNames.SUMMARY), 196 model: AccountModel.scope(AccountScopeNames.SUMMARY),
226 as: 'ReporterAccount', 197 as: 'ReporterAccount',
227 required: true, 198 required: !!options.searchReporter,
228 where: searchAttribute(options.searchReporter, 'name') 199 where: searchAttribute(options.searchReporter, 'name')
229 }, 200 },
230 { 201 {
231 model: AccountModel.scope(AccountScopeNames.SUMMARY), 202 model: AccountModel.scope({
203 method: [
204 AccountScopeNames.SUMMARY,
205 { actorRequired: false } as AccountSummaryOptions
206 ]
207 }),
232 as: 'FlaggedAccount', 208 as: 'FlaggedAccount',
233 required: true, 209 required: !!options.searchReportee,
234 where: searchAttribute(options.searchReportee, 'name') 210 where: searchAttribute(options.searchReportee, 'name')
235 }, 211 },
236 { 212 {
@@ -243,35 +219,36 @@ export enum ScopeNames {
243 include: [ 219 include: [
244 { 220 {
245 model: VideoModel.unscoped(), 221 model: VideoModel.unscoped(),
246 attributes: [ 'name', 'id', 'uuid' ], 222 attributes: [ 'name', 'id', 'uuid' ]
247 required: true
248 } 223 }
249 ] 224 ]
250 } 225 }
251 ] 226 ]
252 }, 227 },
253 { 228 {
254 model: VideoAbuseModel, 229 model: VideoAbuseModel.unscoped(),
255 required: options.filter === 'video' || !!options.videoIs || videoRequired, 230 required: options.filter === 'video' || !!options.videoIs || videoRequired,
256 include: [ 231 include: [
257 { 232 {
258 model: VideoModel, 233 attributes: [ 'id', 'uuid', 'name', 'nsfw' ],
234 model: VideoModel.unscoped(),
259 required: videoRequired, 235 required: videoRequired,
260 where: searchAttribute(options.searchVideo, 'name'), 236 where: searchAttribute(options.searchVideo, 'name'),
261 include: [ 237 include: [
262 { 238 {
239 attributes: [ 'filename', 'fileUrl' ],
263 model: ThumbnailModel 240 model: ThumbnailModel
264 }, 241 },
265 { 242 {
266 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: false } as SummaryOptions ] }), 243 model: VideoChannelModel.scope({
244 method: [
245 VideoChannelScopeNames.SUMMARY,
246 { withAccount: false, actorRequired: false } as ChannelSummaryOptions
247 ]
248 }),
249
267 where: searchAttribute(options.searchVideoChannel, 'name'), 250 where: searchAttribute(options.searchVideoChannel, 'name'),
268 required: true, 251 required: !!options.searchVideoChannel
269 include: [
270 {
271 model: AccountModel.scope(AccountScopeNames.SUMMARY),
272 required: true
273 }
274 ]
275 }, 252 },
276 { 253 {
277 attributes: [ 'id', 'reason', 'unfederated' ], 254 attributes: [ 'id', 'reason', 'unfederated' ],
@@ -304,19 +281,19 @@ export class AbuseModel extends Model<AbuseModel> {
304 281
305 @AllowNull(false) 282 @AllowNull(false)
306 @Default(null) 283 @Default(null)
307 @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) 284 @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
308 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max)) 285 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
309 reason: string 286 reason: string
310 287
311 @AllowNull(false) 288 @AllowNull(false)
312 @Default(null) 289 @Default(null)
313 @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) 290 @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
314 @Column 291 @Column
315 state: AbuseState 292 state: AbuseState
316 293
317 @AllowNull(true) 294 @AllowNull(true)
318 @Default(null) 295 @Default(null)
319 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true)) 296 @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
320 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max)) 297 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
321 moderationComment: string 298 moderationComment: string
322 299
@@ -486,12 +463,12 @@ export class AbuseModel extends Model<AbuseModel> {
486 463
487 toFormattedJSON (this: MAbuseFormattable): Abuse { 464 toFormattedJSON (this: MAbuseFormattable): Abuse {
488 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) 465 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
466
489 const countReportsForVideo = this.get('countReportsForVideo') as number 467 const countReportsForVideo = this.get('countReportsForVideo') as number
490 const nthReportForVideo = this.get('nthReportForVideo') as number 468 const nthReportForVideo = this.get('nthReportForVideo') as number
491 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number 469
492 const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number 470 const countReportsForReporter = this.get('countReportsForReporter') as number
493 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number 471 const countReportsForReportee = this.get('countReportsForReportee') as number
494 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
495 472
496 let video: VideoAbuse 473 let video: VideoAbuse
497 let comment: VideoCommentAbuse 474 let comment: VideoCommentAbuse
@@ -512,7 +489,11 @@ export class AbuseModel extends Model<AbuseModel> {
512 deleted: !abuseModel.Video, 489 deleted: !abuseModel.Video,
513 blacklisted: abuseModel.Video?.isBlacklisted() || false, 490 blacklisted: abuseModel.Video?.isBlacklisted() || false,
514 thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), 491 thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
515 channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel 492
493 channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel,
494
495 countReports: countReportsForVideo,
496 nthReport: nthReportForVideo
516 } 497 }
517 } 498 }
518 499
@@ -539,7 +520,13 @@ export class AbuseModel extends Model<AbuseModel> {
539 reason: this.reason, 520 reason: this.reason,
540 predefinedReasons, 521 predefinedReasons,
541 522
542 reporterAccount: this.ReporterAccount.toFormattedJSON(), 523 reporterAccount: this.ReporterAccount
524 ? this.ReporterAccount.toFormattedJSON()
525 : null,
526
527 flaggedAccount: this.FlaggedAccount
528 ? this.FlaggedAccount.toFormattedJSON()
529 : null,
543 530
544 state: { 531 state: {
545 id: this.state, 532 id: this.state,
@@ -553,14 +540,15 @@ export class AbuseModel extends Model<AbuseModel> {
553 540
554 createdAt: this.createdAt, 541 createdAt: this.createdAt,
555 updatedAt: this.updatedAt, 542 updatedAt: this.updatedAt,
556 count: countReportsForVideo || 0, 543
557 nth: nthReportForVideo || 0, 544 countReportsForReporter: (countReportsForReporter || 0),
558 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), 545 countReportsForReportee: (countReportsForReportee || 0),
559 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0),
560 546
561 // FIXME: deprecated in 2.3, remove this 547 // FIXME: deprecated in 2.3, remove this
562 startAt: null, 548 startAt: null,
563 endAt: null 549 endAt: null,
550 count: countReportsForVideo || 0,
551 nth: nthReportForVideo || 0
564 } 552 }
565 } 553 }
566 554
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 466d6258e..f97519b14 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -42,6 +42,7 @@ export enum ScopeNames {
42} 42}
43 43
44export type SummaryOptions = { 44export type SummaryOptions = {
45 actorRequired?: boolean // Default: true
45 whereActor?: WhereOptions 46 whereActor?: WhereOptions
46 withAccountBlockerIds?: number[] 47 withAccountBlockerIds?: number[]
47} 48}
@@ -65,12 +66,12 @@ export type SummaryOptions = {
65 } 66 }
66 67
67 const query: FindOptions = { 68 const query: FindOptions = {
68 attributes: [ 'id', 'name' ], 69 attributes: [ 'id', 'name', 'actorId' ],
69 include: [ 70 include: [
70 { 71 {
71 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 72 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
72 model: ActorModel.unscoped(), 73 model: ActorModel.unscoped(),
73 required: true, 74 required: options.actorRequired ?? true,
74 where: whereActor, 75 where: whereActor,
75 include: [ 76 include: [
76 serverInclude, 77 serverInclude,
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index b69b47265..d8f3f13da 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -51,11 +51,11 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
51 @AllowNull(false) 51 @AllowNull(false)
52 @Default(null) 52 @Default(null)
53 @Is( 53 @Is(
54 'UserNotificationSettingVideoAbuseAsModerator', 54 'UserNotificationSettingAbuseAsModerator',
55 value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') 55 value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator')
56 ) 56 )
57 @Column 57 @Column
58 videoAbuseAsModerator: UserNotificationSettingValue 58 abuseAsModerator: UserNotificationSettingValue
59 59
60 @AllowNull(false) 60 @AllowNull(false)
61 @Default(null) 61 @Default(null)
@@ -166,7 +166,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
166 return { 166 return {
167 newCommentOnMyVideo: this.newCommentOnMyVideo, 167 newCommentOnMyVideo: this.newCommentOnMyVideo,
168 newVideoFromSubscription: this.newVideoFromSubscription, 168 newVideoFromSubscription: this.newVideoFromSubscription,
169 videoAbuseAsModerator: this.videoAbuseAsModerator, 169 abuseAsModerator: this.abuseAsModerator,
170 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, 170 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
171 blacklistOnMyVideo: this.blacklistOnMyVideo, 171 blacklistOnMyVideo: this.blacklistOnMyVideo,
172 myVideoPublished: this.myVideoPublished, 172 myVideoPublished: this.myVideoPublished,
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index f21eff04b..5f45f8e7c 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -168,28 +168,26 @@ enum ScopeNames {
168 '(' + 168 '(' +
169 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + 169 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
170 'FROM (' + 170 'FROM (' +
171 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + 171 'SELECT COUNT("abuse"."id") AS "abuses", ' +
172 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + 172 `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
173 'FROM "videoAbuse" ' + 173 'FROM "abuse" ' +
174 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + 174 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' +
175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
176 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
177 'WHERE "account"."userId" = "UserModel"."id"' + 175 'WHERE "account"."userId" = "UserModel"."id"' +
178 ') t' + 176 ') t' +
179 ')' 177 ')'
180 ), 178 ),
181 'videoAbusesCount' 179 'abusesCount'
182 ], 180 ],
183 [ 181 [
184 literal( 182 literal(
185 '(' + 183 '(' +
186 'SELECT COUNT("videoAbuse"."id") ' + 184 'SELECT COUNT("abuse"."id") ' +
187 'FROM "videoAbuse" ' + 185 'FROM "abuse" ' +
188 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' + 186 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' +
189 'WHERE "account"."userId" = "UserModel"."id"' + 187 'WHERE "account"."userId" = "UserModel"."id"' +
190 ')' 188 ')'
191 ), 189 ),
192 'videoAbusesCreatedCount' 190 'abusesCreatedCount'
193 ], 191 ],
194 [ 192 [
195 literal( 193 literal(
@@ -780,8 +778,8 @@ export class UserModel extends Model<UserModel> {
780 const videoQuotaUsed = this.get('videoQuotaUsed') 778 const videoQuotaUsed = this.get('videoQuotaUsed')
781 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') 779 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
782 const videosCount = this.get('videosCount') 780 const videosCount = this.get('videosCount')
783 const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':') 781 const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':')
784 const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount') 782 const abusesCreatedCount = this.get('abusesCreatedCount')
785 const videoCommentsCount = this.get('videoCommentsCount') 783 const videoCommentsCount = this.get('videoCommentsCount')
786 784
787 const json: User = { 785 const json: User = {
@@ -815,14 +813,14 @@ export class UserModel extends Model<UserModel> {
815 videosCount: videosCount !== undefined 813 videosCount: videosCount !== undefined
816 ? parseInt(videosCount + '', 10) 814 ? parseInt(videosCount + '', 10)
817 : undefined, 815 : undefined,
818 videoAbusesCount: videoAbusesCount 816 abusesCount: abusesCount
819 ? parseInt(videoAbusesCount, 10) 817 ? parseInt(abusesCount, 10)
820 : undefined, 818 : undefined,
821 videoAbusesAcceptedCount: videoAbusesAcceptedCount 819 abusesAcceptedCount: abusesAcceptedCount
822 ? parseInt(videoAbusesAcceptedCount, 10) 820 ? parseInt(abusesAcceptedCount, 10)
823 : undefined, 821 : undefined,
824 videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined 822 abusesCreatedCount: abusesCreatedCount !== undefined
825 ? parseInt(videoAbusesCreatedCount + '', 10) 823 ? parseInt(abusesCreatedCount + '', 10)
826 : undefined, 824 : undefined,
827 videoCommentsCount: videoCommentsCount !== undefined 825 videoCommentsCount: videoCommentsCount !== undefined
828 ? parseInt(videoCommentsCount + '', 10) 826 ? parseInt(videoCommentsCount + '', 10)
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 9cee64229..03a3cdf81 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -61,6 +61,7 @@ type AvailableWithStatsOptions = {
61} 61}
62 62
63export type SummaryOptions = { 63export type SummaryOptions = {
64 actorRequired?: boolean // Default: true
64 withAccount?: boolean // Default: false 65 withAccount?: boolean // Default: false
65 withAccountBlockerIds?: number[] 66 withAccountBlockerIds?: number[]
66} 67}
@@ -121,7 +122,7 @@ export type SummaryOptions = {
121 { 122 {
122 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 123 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
123 model: ActorModel.unscoped(), 124 model: ActorModel.unscoped(),
124 required: true, 125 required: options.actorRequired ?? true,
125 include: [ 126 include: [
126 { 127 {
127 attributes: [ 'host' ], 128 attributes: [ 'host' ],
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 2048fa667..883b1d29c 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -164,7 +164,7 @@ describe('Test user notifications API validators', function () {
164 const correctFields: UserNotificationSetting = { 164 const correctFields: UserNotificationSetting = {
165 newVideoFromSubscription: UserNotificationSettingValue.WEB, 165 newVideoFromSubscription: UserNotificationSettingValue.WEB,
166 newCommentOnMyVideo: UserNotificationSettingValue.WEB, 166 newCommentOnMyVideo: UserNotificationSettingValue.WEB,
167 videoAbuseAsModerator: UserNotificationSettingValue.WEB, 167 abuseAsModerator: UserNotificationSettingValue.WEB,
168 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB, 168 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
169 blacklistOnMyVideo: UserNotificationSettingValue.WEB, 169 blacklistOnMyVideo: UserNotificationSettingValue.WEB,
170 myVideoImportFinished: UserNotificationSettingValue.WEB, 170 myVideoImportFinished: UserNotificationSettingValue.WEB,
diff --git a/server/tests/api/ci-4.sh b/server/tests/api/ci-4.sh
index 14a014f07..4998de364 100644
--- a/server/tests/api/ci-4.sh
+++ b/server/tests/api/ci-4.sh
@@ -2,6 +2,7 @@
2 2
3set -eu 3set -eu
4 4
5activitypubFiles=$(find server/tests/api/moderation -type f | grep -v index.ts | xargs echo)
5redundancyFiles=$(find server/tests/api/redundancy -type f | grep -v index.ts | xargs echo) 6redundancyFiles=$(find server/tests/api/redundancy -type f | grep -v index.ts | xargs echo)
6activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo) 7activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo)
7 8
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts
index bac77ab2e..b62e2f5f7 100644
--- a/server/tests/api/index.ts
+++ b/server/tests/api/index.ts
@@ -1,6 +1,7 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './activitypub' 2import './activitypub'
3import './check-params' 3import './check-params'
4import './moderation'
4import './notifications' 5import './notifications'
5import './redundancy' 6import './redundancy'
6import './search' 7import './search'
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts
new file mode 100644
index 000000000..28c5a5531
--- /dev/null
+++ b/server/tests/api/moderation/abuses.ts
@@ -0,0 +1,384 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models'
6import {
7 cleanupTests,
8 createUser,
9 deleteVideoAbuse,
10 flushAndRunMultipleServers,
11 getVideoAbusesList,
12 getVideosList,
13 removeVideo,
14 reportVideoAbuse,
15 ServerInfo,
16 setAccessTokensToServers,
17 updateVideoAbuse,
18 uploadVideo,
19 userLogin
20} from '../../../../shared/extra-utils/index'
21import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
22import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
23import {
24 addAccountToServerBlocklist,
25 addServerToServerBlocklist,
26 removeAccountFromServerBlocklist,
27 removeServerFromServerBlocklist
28} from '../../../../shared/extra-utils/users/blocklist'
29
30const expect = chai.expect
31
32describe('Test abuses', function () {
33 let servers: ServerInfo[] = []
34 let abuseServer2: Abuse
35
36 before(async function () {
37 this.timeout(50000)
38
39 // Run servers
40 servers = await flushAndRunMultipleServers(2)
41
42 // Get the access tokens
43 await setAccessTokensToServers(servers)
44
45 // Server 1 and server 2 follow each other
46 await doubleFollow(servers[0], servers[1])
47
48 // Upload some videos on each servers
49 const video1Attributes = {
50 name: 'my super name for server 1',
51 description: 'my super description for server 1'
52 }
53 await uploadVideo(servers[0].url, servers[0].accessToken, video1Attributes)
54
55 const video2Attributes = {
56 name: 'my super name for server 2',
57 description: 'my super description for server 2'
58 }
59 await uploadVideo(servers[1].url, servers[1].accessToken, video2Attributes)
60
61 // Wait videos propagation, server 2 has transcoding enabled
62 await waitJobs(servers)
63
64 const res = await getVideosList(servers[0].url)
65 const videos = res.body.data
66
67 expect(videos.length).to.equal(2)
68
69 servers[0].video = videos.find(video => video.name === 'my super name for server 1')
70 servers[1].video = videos.find(video => video.name === 'my super name for server 2')
71 })
72
73 it('Should not have video abuses', async function () {
74 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
75
76 expect(res.body.total).to.equal(0)
77 expect(res.body.data).to.be.an('array')
78 expect(res.body.data.length).to.equal(0)
79 })
80
81 it('Should report abuse on a local video', async function () {
82 this.timeout(15000)
83
84 const reason = 'my super bad reason'
85 await reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[0].video.id, reason)
86
87 // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2
88 await waitJobs(servers)
89 })
90
91 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
92 const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
93
94 expect(res1.body.total).to.equal(1)
95 expect(res1.body.data).to.be.an('array')
96 expect(res1.body.data.length).to.equal(1)
97
98 const abuse: Abuse = res1.body.data[0]
99 expect(abuse.reason).to.equal('my super bad reason')
100 expect(abuse.reporterAccount.name).to.equal('root')
101 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
102 expect(abuse.video.id).to.equal(servers[0].video.id)
103 expect(abuse.video.channel).to.exist
104 expect(abuse.count).to.equal(1)
105 expect(abuse.nth).to.equal(1)
106 expect(abuse.countReportsForReporter).to.equal(1)
107 expect(abuse.countReportsForReportee).to.equal(1)
108
109 const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
110 expect(res2.body.total).to.equal(0)
111 expect(res2.body.data).to.be.an('array')
112 expect(res2.body.data.length).to.equal(0)
113 })
114
115 it('Should report abuse on a remote video', async function () {
116 this.timeout(10000)
117
118 const reason = 'my super bad reason 2'
119 await reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[1].video.id, reason)
120
121 // We wait requests propagation
122 await waitJobs(servers)
123 })
124
125 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
126 const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
127 expect(res1.body.total).to.equal(2)
128 expect(res1.body.data).to.be.an('array')
129 expect(res1.body.data.length).to.equal(2)
130
131 const abuse1: Abuse = res1.body.data[0]
132 expect(abuse1.reason).to.equal('my super bad reason')
133 expect(abuse1.reporterAccount.name).to.equal('root')
134 expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
135 expect(abuse1.video.id).to.equal(servers[0].video.id)
136 expect(abuse1.state.id).to.equal(AbuseState.PENDING)
137 expect(abuse1.state.label).to.equal('Pending')
138 expect(abuse1.moderationComment).to.be.null
139 expect(abuse1.count).to.equal(1)
140 expect(abuse1.nth).to.equal(1)
141
142 const abuse2: Abuse = res1.body.data[1]
143 expect(abuse2.reason).to.equal('my super bad reason 2')
144 expect(abuse2.reporterAccount.name).to.equal('root')
145 expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
146 expect(abuse2.video.id).to.equal(servers[1].video.id)
147 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
148 expect(abuse2.state.label).to.equal('Pending')
149 expect(abuse2.moderationComment).to.be.null
150
151 const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
152 expect(res2.body.total).to.equal(1)
153 expect(res2.body.data).to.be.an('array')
154 expect(res2.body.data.length).to.equal(1)
155
156 abuseServer2 = res2.body.data[0]
157 expect(abuseServer2.reason).to.equal('my super bad reason 2')
158 expect(abuseServer2.reporterAccount.name).to.equal('root')
159 expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
160 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
161 expect(abuseServer2.state.label).to.equal('Pending')
162 expect(abuseServer2.moderationComment).to.be.null
163 })
164
165 it('Should update the state of a video abuse', async function () {
166 const body = { state: AbuseState.REJECTED }
167 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
168
169 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
170 expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
171 })
172
173 it('Should add a moderation comment', async function () {
174 const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
175 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
176
177 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
178 expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
179 expect(res.body.data[0].moderationComment).to.equal('It is valid')
180 })
181
182 it('Should hide video abuses from blocked accounts', async function () {
183 this.timeout(10000)
184
185 {
186 await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this')
187 await waitJobs(servers)
188
189 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
190 expect(res.body.total).to.equal(3)
191 }
192
193 const accountToBlock = 'root@localhost:' + servers[1].port
194
195 {
196 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
197
198 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
199 expect(res.body.total).to.equal(2)
200
201 const abuse = res.body.data.find(a => a.reason === 'will mute this')
202 expect(abuse).to.be.undefined
203 }
204
205 {
206 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
207
208 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
209 expect(res.body.total).to.equal(3)
210 }
211 })
212
213 it('Should hide video abuses from blocked servers', async function () {
214 const serverToBlock = servers[1].host
215
216 {
217 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
218
219 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
220 expect(res.body.total).to.equal(2)
221
222 const abuse = res.body.data.find(a => a.reason === 'will mute this')
223 expect(abuse).to.be.undefined
224 }
225
226 {
227 await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock)
228
229 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
230 expect(res.body.total).to.equal(3)
231 }
232 })
233
234 it('Should keep the video abuse when deleting the video', async function () {
235 this.timeout(10000)
236
237 await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid)
238
239 await waitJobs(servers)
240
241 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
242 expect(res.body.total).to.equal(2, "wrong number of videos returned")
243 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")
245
246 const abuse: Abuse = res.body.data[0]
247 expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
248 expect(abuse.video.channel).to.exist
249 expect(abuse.video.deleted).to.be.true
250 })
251
252 it('Should include counts of reports from reporter and reportee', async function () {
253 this.timeout(10000)
254
255 // register a second user to have two reporters/reportees
256 const user = { username: 'user2', password: 'password' }
257 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user })
258 const userAccessToken = await userLogin(servers[0], user)
259
260 // upload a third video via this user
261 const video3Attributes = {
262 name: 'my second super name for server 1',
263 description: 'my second super description for server 1'
264 }
265 await uploadVideo(servers[0].url, userAccessToken, video3Attributes)
266
267 const res1 = await getVideosList(servers[0].url)
268 const videos = res1.body.data
269 const video3 = videos.find(video => video.name === 'my second super name for server 1')
270
271 // resume with the test
272 const reason3 = 'my super bad reason 3'
273 await reportVideoAbuse(servers[0].url, servers[0].accessToken, video3.id, reason3)
274 const reason4 = 'my super bad reason 4'
275 await reportVideoAbuse(servers[0].url, userAccessToken, servers[0].video.id, reason4)
276
277 const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
278
279 {
280 for (const abuse of res2.body.data as Abuse[]) {
281 if (abuse.video.id === video3.id) {
282 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")
284 expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse")
285 expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
286 }
287 if (abuse.video.id === servers[0].video.id) {
288 expect(abuse.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse")
289 }
290 }
291 }
292 })
293
294 it('Should list predefined reasons as well as timestamps for the reported video', async function () {
295 this.timeout(10000)
296
297 const reason5 = 'my super bad reason 5'
298 const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
299 const createdAbuse = (await reportVideoAbuse(
300 servers[0].url,
301 servers[0].accessToken,
302 servers[0].video.id,
303 reason5,
304 predefinedReasons5,
305 1,
306 5
307 )).body.abuse
308
309 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
310
311 {
312 const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id)
313 expect(abuse.reason).to.equals(reason5)
314 expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
315 expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
316 expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
317 }
318 })
319
320 it('Should delete the video abuse', async function () {
321 this.timeout(10000)
322
323 await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id)
324
325 await waitJobs(servers)
326
327 {
328 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
329 expect(res.body.total).to.equal(1)
330 expect(res.body.data.length).to.equal(1)
331 expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
332 }
333
334 {
335 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
336 expect(res.body.total).to.equal(6)
337 }
338 })
339
340 it('Should list and filter video abuses', async function () {
341 async function list (query: Omit<Parameters<typeof getVideoAbusesList>[0], 'url' | 'token'>) {
342 const options = {
343 url: servers[0].url,
344 token: servers[0].accessToken
345 }
346
347 Object.assign(options, query)
348
349 const res = await getVideoAbusesList(options)
350
351 return res.body.data as Abuse[]
352 }
353
354 expect(await list({ id: 56 })).to.have.lengthOf(0)
355 expect(await list({ id: 1 })).to.have.lengthOf(1)
356
357 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4)
358 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
359
360 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
361
362 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4)
363 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
364
365 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
366 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
367
368 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
369 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
370
371 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
372 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
373
374 expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
375 expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
376
377 expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
378 expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
379 })
380
381 after(async function () {
382 await cleanupTests(servers)
383 })
384})
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/moderation/blocklist.ts
index 8c9107a50..8c9107a50 100644
--- a/server/tests/api/users/blocklist.ts
+++ b/server/tests/api/moderation/blocklist.ts
diff --git a/server/tests/api/moderation/index.ts b/server/tests/api/moderation/index.ts
new file mode 100644
index 000000000..cb018d88e
--- /dev/null
+++ b/server/tests/api/moderation/index.ts
@@ -0,0 +1,2 @@
1export * from './abuses'
2export * from './blocklist'
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts
index b90732a7a..a27681603 100644
--- a/server/tests/api/notifications/moderation-notifications.ts
+++ b/server/tests/api/notifications/moderation-notifications.ts
@@ -11,7 +11,7 @@ import {
11 MockInstancesIndex, 11 MockInstancesIndex,
12 registerUser, 12 registerUser,
13 removeVideoFromBlacklist, 13 removeVideoFromBlacklist,
14 reportVideoAbuse, 14 reportAbuse,
15 unfollow, 15 unfollow,
16 updateCustomConfig, 16 updateCustomConfig,
17 updateCustomSubConfig, 17 updateCustomSubConfig,
@@ -74,12 +74,12 @@ describe('Test moderation notifications', function () {
74 74
75 const name = 'video for abuse ' + uuidv4() 75 const name = 'video for abuse ' + uuidv4()
76 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) 76 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
77 const uuid = resVideo.body.video.uuid 77 const video = resVideo.body.video
78 78
79 await reportVideoAbuse(servers[0].url, servers[0].accessToken, uuid, 'super reason') 79 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video.id, reason: 'super reason' })
80 80
81 await waitJobs(servers) 81 await waitJobs(servers)
82 await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') 82 await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
83 }) 83 })
84 84
85 it('Should send a notification to moderators on remote video abuse', async function () { 85 it('Should send a notification to moderators on remote video abuse', async function () {
@@ -87,14 +87,14 @@ describe('Test moderation notifications', function () {
87 87
88 const name = 'video for abuse ' + uuidv4() 88 const name = 'video for abuse ' + uuidv4()
89 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) 89 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
90 const uuid = resVideo.body.video.uuid 90 const video = resVideo.body.video
91 91
92 await waitJobs(servers) 92 await waitJobs(servers)
93 93
94 await reportVideoAbuse(servers[1].url, servers[1].accessToken, uuid, 'super reason') 94 await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId: video.id, reason: 'super reason' })
95 95
96 await waitJobs(servers) 96 await waitJobs(servers)
97 await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') 97 await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
98 }) 98 })
99 }) 99 })
100 100
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index 95b64a459..9c3299618 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -1,7 +1,7 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
4import * as chai from 'chai'
5import { 5import {
6 addVideoToBlacklist, 6 addVideoToBlacklist,
7 askResetPassword, 7 askResetPassword,
@@ -11,7 +11,7 @@ import {
11 createUser, 11 createUser,
12 flushAndRunServer, 12 flushAndRunServer,
13 removeVideoFromBlacklist, 13 removeVideoFromBlacklist,
14 reportVideoAbuse, 14 reportAbuse,
15 resetPassword, 15 resetPassword,
16 ServerInfo, 16 ServerInfo,
17 setAccessTokensToServers, 17 setAccessTokensToServers,
@@ -30,10 +30,15 @@ describe('Test emails', function () {
30 let userId: number 30 let userId: number
31 let userId2: number 31 let userId2: number
32 let userAccessToken: string 32 let userAccessToken: string
33
33 let videoUUID: string 34 let videoUUID: string
35 let videoId: number
36
34 let videoUserUUID: string 37 let videoUserUUID: string
38
35 let verificationString: string 39 let verificationString: string
36 let verificationString2: string 40 let verificationString2: string
41
37 const emails: object[] = [] 42 const emails: object[] = []
38 const user = { 43 const user = {
39 username: 'user_1', 44 username: 'user_1',
@@ -76,6 +81,7 @@ describe('Test emails', function () {
76 } 81 }
77 const res = await uploadVideo(server.url, server.accessToken, attributes) 82 const res = await uploadVideo(server.url, server.accessToken, attributes)
78 videoUUID = res.body.video.uuid 83 videoUUID = res.body.video.uuid
84 videoId = res.body.video.id
79 } 85 }
80 }) 86 })
81 87
@@ -179,7 +185,7 @@ describe('Test emails', function () {
179 this.timeout(10000) 185 this.timeout(10000)
180 186
181 const reason = 'my super bad reason' 187 const reason = 'my super bad reason'
182 await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason) 188 await reportAbuse({ url: server.url, token: server.accessToken, videoId, reason })
183 189
184 await waitJobs(server) 190 await waitJobs(server)
185 expect(emails).to.have.lengthOf(3) 191 expect(emails).to.have.lengthOf(3)
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts
index fcd022429..a244a6edb 100644
--- a/server/tests/api/users/index.ts
+++ b/server/tests/api/users/index.ts
@@ -1,5 +1,4 @@
1import './users-verification'
2import './blocklist'
3import './user-subscriptions' 1import './user-subscriptions'
4import './users' 2import './users'
5import './users-multiple-servers' 3import './users-multiple-servers'
4import './users-verification'
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 88b68d977..ea74bde6a 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -1,8 +1,9 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
5import { MyUser, User, UserRole, Video, AbuseState, AbuseUpdate, VideoPlaylistType } from '@shared/models' 4import * as chai from 'chai'
5import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models'
6import { CustomConfig } from '@shared/models/server'
6import { 7import {
7 addVideoCommentThread, 8 addVideoCommentThread,
8 blockUser, 9 blockUser,
@@ -10,6 +11,7 @@ import {
10 createUser, 11 createUser,
11 deleteMe, 12 deleteMe,
12 flushAndRunServer, 13 flushAndRunServer,
14 getAbusesList,
13 getAccountRatings, 15 getAccountRatings,
14 getBlacklistedVideosList, 16 getBlacklistedVideosList,
15 getCustomConfig, 17 getCustomConfig,
@@ -19,7 +21,6 @@ import {
19 getUserInformation, 21 getUserInformation,
20 getUsersList, 22 getUsersList,
21 getUsersListPaginationAndSort, 23 getUsersListPaginationAndSort,
22 getVideoAbusesList,
23 getVideoChannel, 24 getVideoChannel,
24 getVideosList, 25 getVideosList,
25 installPlugin, 26 installPlugin,
@@ -29,15 +30,15 @@ import {
29 registerUserWithChannel, 30 registerUserWithChannel,
30 removeUser, 31 removeUser,
31 removeVideo, 32 removeVideo,
32 reportVideoAbuse, 33 reportAbuse,
33 ServerInfo, 34 ServerInfo,
34 testImage, 35 testImage,
35 unblockUser, 36 unblockUser,
37 updateAbuse,
36 updateCustomSubConfig, 38 updateCustomSubConfig,
37 updateMyAvatar, 39 updateMyAvatar,
38 updateMyUser, 40 updateMyUser,
39 updateUser, 41 updateUser,
40 updateVideoAbuse,
41 uploadVideo, 42 uploadVideo,
42 userLogin, 43 userLogin,
43 waitJobs 44 waitJobs
@@ -46,7 +47,6 @@ import { follow } from '../../../../shared/extra-utils/server/follows'
46import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 47import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
47import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' 48import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
48import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 49import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
49import { CustomConfig } from '@shared/models/server'
50 50
51const expect = chai.expect 51const expect = chai.expect
52 52
@@ -302,10 +302,10 @@ describe('Test users', function () {
302 expect(userGet.videosCount).to.equal(0) 302 expect(userGet.videosCount).to.equal(0)
303 expect(userGet.videoCommentsCount).to.be.a('number') 303 expect(userGet.videoCommentsCount).to.be.a('number')
304 expect(userGet.videoCommentsCount).to.equal(0) 304 expect(userGet.videoCommentsCount).to.equal(0)
305 expect(userGet.videoAbusesCount).to.be.a('number') 305 expect(userGet.abusesCount).to.be.a('number')
306 expect(userGet.videoAbusesCount).to.equal(0) 306 expect(userGet.abusesCount).to.equal(0)
307 expect(userGet.videoAbusesAcceptedCount).to.be.a('number') 307 expect(userGet.abusesAcceptedCount).to.be.a('number')
308 expect(userGet.videoAbusesAcceptedCount).to.equal(0) 308 expect(userGet.abusesAcceptedCount).to.equal(0)
309 }) 309 })
310 }) 310 })
311 311
@@ -895,9 +895,9 @@ describe('Test users', function () {
895 895
896 expect(user.videosCount).to.equal(0) 896 expect(user.videosCount).to.equal(0)
897 expect(user.videoCommentsCount).to.equal(0) 897 expect(user.videoCommentsCount).to.equal(0)
898 expect(user.videoAbusesCount).to.equal(0) 898 expect(user.abusesCount).to.equal(0)
899 expect(user.videoAbusesCreatedCount).to.equal(0) 899 expect(user.abusesCreatedCount).to.equal(0)
900 expect(user.videoAbusesAcceptedCount).to.equal(0) 900 expect(user.abusesAcceptedCount).to.equal(0)
901 }) 901 })
902 902
903 it('Should report correct videos count', async function () { 903 it('Should report correct videos count', async function () {
@@ -924,26 +924,26 @@ describe('Test users', function () {
924 expect(user.videoCommentsCount).to.equal(1) 924 expect(user.videoCommentsCount).to.equal(1)
925 }) 925 })
926 926
927 it('Should report correct video abuses counts', async function () { 927 it('Should report correct abuses counts', async function () {
928 const reason = 'my super bad reason' 928 const reason = 'my super bad reason'
929 await reportVideoAbuse(server.url, user17AccessToken, videoId, reason) 929 await reportAbuse({ url: server.url, token: user17AccessToken, videoId, reason })
930 930
931 const res1 = await getVideoAbusesList({ url: server.url, token: server.accessToken }) 931 const res1 = await getAbusesList({ url: server.url, token: server.accessToken })
932 const abuseId = res1.body.data[0].id 932 const abuseId = res1.body.data[0].id
933 933
934 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true) 934 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
935 const user2: User = res2.body 935 const user2: User = res2.body
936 936
937 expect(user2.videoAbusesCount).to.equal(1) // number of incriminations 937 expect(user2.abusesCount).to.equal(1) // number of incriminations
938 expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created 938 expect(user2.abusesCreatedCount).to.equal(1) // number of reports created
939 939
940 const body: AbuseUpdate = { state: AbuseState.ACCEPTED } 940 const body: AbuseUpdate = { state: AbuseState.ACCEPTED }
941 await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body) 941 await updateAbuse(server.url, server.accessToken, abuseId, body)
942 942
943 const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true) 943 const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
944 const user3: User = res3.body 944 const user3: User = res3.body
945 945
946 expect(user3.videoAbusesAcceptedCount).to.equal(1) // number of reports created accepted 946 expect(user3.abusesAcceptedCount).to.equal(1) // number of reports created accepted
947 }) 947 })
948 }) 948 })
949 949
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index 20975aa4a..baeb543e0 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -103,8 +103,8 @@ describe('Test video abuses', function () {
103 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port) 103 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
104 expect(abuse.video.id).to.equal(servers[0].video.id) 104 expect(abuse.video.id).to.equal(servers[0].video.id)
105 expect(abuse.video.channel).to.exist 105 expect(abuse.video.channel).to.exist
106 expect(abuse.count).to.equal(1) 106 expect(abuse.video.countReports).to.equal(1)
107 expect(abuse.nth).to.equal(1) 107 expect(abuse.video.nthReport).to.equal(1)
108 expect(abuse.countReportsForReporter).to.equal(1) 108 expect(abuse.countReportsForReporter).to.equal(1)
109 expect(abuse.countReportsForReportee).to.equal(1) 109 expect(abuse.countReportsForReportee).to.equal(1)
110 110
@@ -138,8 +138,8 @@ describe('Test video abuses', function () {
138 expect(abuse1.state.id).to.equal(AbuseState.PENDING) 138 expect(abuse1.state.id).to.equal(AbuseState.PENDING)
139 expect(abuse1.state.label).to.equal('Pending') 139 expect(abuse1.state.label).to.equal('Pending')
140 expect(abuse1.moderationComment).to.be.null 140 expect(abuse1.moderationComment).to.be.null
141 expect(abuse1.count).to.equal(1) 141 expect(abuse1.video.countReports).to.equal(1)
142 expect(abuse1.nth).to.equal(1) 142 expect(abuse1.video.nthReport).to.equal(1)
143 143
144 const abuse2: Abuse = res1.body.data[1] 144 const abuse2: Abuse = res1.body.data[1]
145 expect(abuse2.reason).to.equal('my super bad reason 2') 145 expect(abuse2.reason).to.equal('my super bad reason 2')
@@ -281,8 +281,8 @@ describe('Test video abuses', function () {
281 { 281 {
282 for (const abuse of res2.body.data as Abuse[]) { 282 for (const abuse of res2.body.data as Abuse[]) {
283 if (abuse.video.id === video3.id) { 283 if (abuse.video.id === video3.id) {
284 expect(abuse.count).to.equal(1, "wrong reports count for video 3") 284 expect(abuse.video.countReports).to.equal(1, "wrong reports count for video 3")
285 expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3") 285 expect(abuse.video.nthReport).to.equal(1, "wrong report position in report list for video 3")
286 expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse") 286 expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse")
287 expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse") 287 expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
288 } 288 }
diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts
index 8e12be874..a0bf4b08f 100644
--- a/server/types/models/moderation/abuse.ts
+++ b/server/types/models/moderation/abuse.ts
@@ -98,5 +98,6 @@ export type MAbuseFull =
98export type MAbuseFormattable = 98export type MAbuseFormattable =
99 MAbuse & 99 MAbuse &
100 Use<'ReporterAccount', MAccountFormattable> & 100 Use<'ReporterAccount', MAccountFormattable> &
101 Use<'FlaggedAccount', MAccountFormattable> &
101 Use<'VideoAbuse', MVideoAbuseFormattable> & 102 Use<'VideoAbuse', MVideoAbuseFormattable> &
102 Use<'VideoCommentAbuse', MCommentAbuseFormattable> 103 Use<'VideoCommentAbuse', MCommentAbuseFormattable>
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts
index 62f3418c5..4a5bc30fe 100644
--- a/shared/extra-utils/users/user-notifications.ts
+++ b/shared/extra-utils/users/user-notifications.ts
@@ -516,7 +516,7 @@ function getAllNotificationsSettings () {
516 return { 516 return {
517 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 517 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
518 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 518 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
519 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 519 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
520 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 520 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
521 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 521 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
522 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 522 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
diff --git a/shared/models/moderation/abuse/abuse.model.ts b/shared/models/moderation/abuse/abuse.model.ts
index a120803e6..086911ad5 100644
--- a/shared/models/moderation/abuse/abuse.model.ts
+++ b/shared/models/moderation/abuse/abuse.model.ts
@@ -18,6 +18,9 @@ export interface VideoAbuse {
18 18
19 thumbnailPath?: string 19 thumbnailPath?: string
20 channel?: VideoChannel 20 channel?: VideoChannel
21
22 countReports: number
23 nthReport: number
21} 24}
22 25
23export interface VideoCommentAbuse { 26export interface VideoCommentAbuse {
@@ -36,9 +39,12 @@ export interface VideoCommentAbuse {
36 39
37export interface Abuse { 40export interface Abuse {
38 id: number 41 id: number
42
39 reason: string 43 reason: string
40 predefinedReasons?: AbusePredefinedReasonsString[] 44 predefinedReasons?: AbusePredefinedReasonsString[]
45
41 reporterAccount: Account 46 reporterAccount: Account
47 flaggedAccount: Account
42 48
43 state: VideoConstant<AbuseState> 49 state: VideoConstant<AbuseState>
44 moderationComment?: string 50 moderationComment?: string
@@ -49,13 +55,18 @@ export interface Abuse {
49 createdAt: Date 55 createdAt: Date
50 updatedAt: Date 56 updatedAt: Date
51 57
52 // FIXME: deprecated in 2.3, remove this
53 startAt: null
54 endAt: null
55
56 count?: number
57 nth?: number
58
59 countReportsForReporter?: number 58 countReportsForReporter?: number
60 countReportsForReportee?: number 59 countReportsForReportee?: number
60
61 // FIXME: deprecated in 2.3, remove the following properties
62
63 // // @deprecated
64 // startAt: null
65 // // @deprecated
66 // endAt: null
67
68 // // @deprecated
69 // count?: number
70 // // @deprecated
71 // nth?: number
61} 72}
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts
index 451f40d58..4e2230a76 100644
--- a/shared/models/users/user-notification-setting.model.ts
+++ b/shared/models/users/user-notification-setting.model.ts
@@ -7,7 +7,7 @@ export enum UserNotificationSettingValue {
7export interface UserNotificationSetting { 7export interface UserNotificationSetting {
8 newVideoFromSubscription: UserNotificationSettingValue 8 newVideoFromSubscription: UserNotificationSettingValue
9 newCommentOnMyVideo: UserNotificationSettingValue 9 newCommentOnMyVideo: UserNotificationSettingValue
10 videoAbuseAsModerator: UserNotificationSettingValue 10 abuseAsModerator: UserNotificationSettingValue
11 videoAutoBlacklistAsModerator: UserNotificationSettingValue 11 videoAutoBlacklistAsModerator: UserNotificationSettingValue
12 blacklistOnMyVideo: UserNotificationSettingValue 12 blacklistOnMyVideo: UserNotificationSettingValue
13 myVideoPublished: UserNotificationSettingValue 13 myVideoPublished: UserNotificationSettingValue
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts
index 6c959ceea..859736b2f 100644
--- a/shared/models/users/user.model.ts
+++ b/shared/models/users/user.model.ts
@@ -31,10 +31,13 @@ export interface User {
31 videoQuotaDaily: number 31 videoQuotaDaily: number
32 videoQuotaUsed?: number 32 videoQuotaUsed?: number
33 videoQuotaUsedDaily?: number 33 videoQuotaUsedDaily?: number
34
34 videosCount?: number 35 videosCount?: number
35 videoAbusesCount?: number 36
36 videoAbusesAcceptedCount?: number 37 abusesCount?: number
37 videoAbusesCreatedCount?: number 38 abusesAcceptedCount?: number
39 abusesCreatedCount?: number
40
38 videoCommentsCount? : number 41 videoCommentsCount? : number
39 42
40 theme: string 43 theme: string
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 79f75063f..03e60925b 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -893,7 +893,7 @@ paths:
893 $ref: '#/components/schemas/NotificationSettingValue' 893 $ref: '#/components/schemas/NotificationSettingValue'
894 newCommentOnMyVideo: 894 newCommentOnMyVideo:
895 $ref: '#/components/schemas/NotificationSettingValue' 895 $ref: '#/components/schemas/NotificationSettingValue'
896 videoAbuseAsModerator: 896 abuseAsModerator:
897 $ref: '#/components/schemas/NotificationSettingValue' 897 $ref: '#/components/schemas/NotificationSettingValue'
898 videoAutoBlacklistAsModerator: 898 videoAutoBlacklistAsModerator:
899 $ref: '#/components/schemas/NotificationSettingValue' 899 $ref: '#/components/schemas/NotificationSettingValue'
@@ -1618,7 +1618,7 @@ paths:
1618 type: object 1618 type: object
1619 properties: 1619 properties:
1620 state: 1620 state:
1621 $ref: '#/components/schemas/VideoAbuseStateSet' 1621 $ref: '#/components/schemas/AbuseStateSet'
1622 moderationComment: 1622 moderationComment:
1623 type: string 1623 type: string
1624 description: Update the report comment visible only to the moderation team 1624 description: Update the report comment visible only to the moderation team
@@ -3584,20 +3584,20 @@ components:
3584 label: 3584 label:
3585 type: string 3585 type: string
3586 3586
3587 VideoAbuseStateSet: 3587 AbuseStateSet:
3588 type: integer 3588 type: integer
3589 enum: 3589 enum:
3590 - 1 3590 - 1
3591 - 2 3591 - 2
3592 - 3 3592 - 3
3593 description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)' 3593 description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)'
3594 VideoAbuseStateConstant: 3594 AbuseStateConstant:
3595 properties: 3595 properties:
3596 id: 3596 id:
3597 $ref: '#/components/schemas/VideoAbuseStateSet' 3597 $ref: '#/components/schemas/AbuseStateSet'
3598 label: 3598 label:
3599 type: string 3599 type: string
3600 VideoAbusePredefinedReasons: 3600 AbusePredefinedReasons:
3601 type: array 3601 type: array
3602 items: 3602 items:
3603 type: string 3603 type: string
@@ -3960,11 +3960,11 @@ components:
3960 type: string 3960 type: string
3961 example: The video is a spam 3961 example: The video is a spam
3962 predefinedReasons: 3962 predefinedReasons:
3963 $ref: '#/components/schemas/VideoAbusePredefinedReasons' 3963 $ref: '#/components/schemas/AbusePredefinedReasons'
3964 reporterAccount: 3964 reporterAccount:
3965 $ref: '#/components/schemas/Account' 3965 $ref: '#/components/schemas/Account'
3966 state: 3966 state:
3967 $ref: '#/components/schemas/VideoAbuseStateConstant' 3967 $ref: '#/components/schemas/AbuseStateConstant'
3968 moderationComment: 3968 moderationComment:
3969 type: string 3969 type: string
3970 example: Decided to ban the server since it spams us regularly 3970 example: Decided to ban the server since it spams us regularly
@@ -4690,11 +4690,11 @@ components:
4690 description: The user daily video quota 4690 description: The user daily video quota
4691 videosCount: 4691 videosCount:
4692 type: integer 4692 type: integer
4693 videoAbusesCount: 4693 abusesCount:
4694 type: integer 4694 type: integer
4695 videoAbusesAcceptedCount: 4695 abusesAcceptedCount:
4696 type: integer 4696 type: integer
4697 videoAbusesCreatedCount: 4697 abusesCreatedCount:
4698 type: integer 4698 type: integer
4699 videoCommentsCount: 4699 videoCommentsCount:
4700 type: integer 4700 type: integer