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 {
children: []
}
- if (this.hasVideoAbusesRight()) {
+ if (this.hasAbusesRight()) {
moderationItems.children.push({
label: this.i18n('Video reports'),
routerLink: '/admin/moderation/video-abuses/list',
@@ -76,7 +76,7 @@ export class AdminComponent implements OnInit {
if (this.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' })
if (this.hasServerFollowRight()) this.menuEntries.push(federationItems)
- if (this.hasVideoAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems)
+ if (this.hasAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems)
if (this.hasConfigRight()) this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' })
if (this.hasPluginsRight()) this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' })
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 {
return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW)
}
- hasVideoAbusesRight () {
+ hasAbusesRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES)
}
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 @@
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 {
this.labelNotifications = {
newVideoFromSubscription: this.i18n('New video from your subscriptions'),
newCommentOnMyVideo: this.i18n('New comment on your video'),
- videoAbuseAsModerator: this.i18n('New video abuse'),
+ abuseAsModerator: this.i18n('New abuse'),
videoAutoBlacklistAsModerator: this.i18n('Video blocked automatically waiting review'),
blacklistOnMyVideo: this.i18n('One of your video is blocked/unblocked'),
myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
@@ -47,7 +47,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
this.rightNotifications = {
- videoAbuseAsModerator: UserRight.MANAGE_ABUSES,
+ abuseAsModerator: UserRight.MANAGE_ABUSES,
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
newUserRegistration: UserRight.MANAGE_USERS,
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 {
videoQuotaDaily: number
videoQuotaUsed?: number
videoQuotaUsedDaily?: number
+
videosCount?: number
- videoAbusesCount?: number
- videoAbusesAcceptedCount?: number
- videoAbusesCreatedCount?: number
videoCommentsCount?: number
+ abusesCount?: number
+ abusesAcceptedCount?: number
+ abusesCreatedCount?: number
+
theme: string
account: Account
@@ -89,9 +91,9 @@ export class User implements UserServerModel {
this.videoQuotaUsed = hash.videoQuotaUsed
this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
this.videosCount = hash.videosCount
- this.videoAbusesCount = hash.videoAbusesCount
- this.videoAbusesAcceptedCount = hash.videoAbusesAcceptedCount
- this.videoAbusesCreatedCount = hash.videoAbusesCreatedCount
+ this.abusesCount = hash.abusesCount
+ this.abusesAcceptedCount = hash.abusesAcceptedCount
+ this.abusesCreatedCount = hash.abusesCreatedCount
this.videoCommentsCount = hash.videoCommentsCount
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
const values: UserNotificationSetting = {
newVideoFromSubscription: body.newVideoFromSubscription,
newCommentOnMyVideo: body.newCommentOnMyVideo,
- videoAbuseAsModerator: body.videoAbuseAsModerator,
+ abuseAsModerator: body.abuseAsModerator,
videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: body.blacklistOnMyVideo,
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'
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 515
+const LAST_MIGRATION_VERSION = 520
// ---------------------------------------------------------------------------
diff --git a/server/initializers/migrations/0470-cleaup-indexes.ts b/server/initializers/migrations/0470-cleanup-indexes.ts
similarity index 100%
rename from server/initializers/migrations/0470-cleaup-indexes.ts
rename to 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 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+}): Promise {
+ await utils.queryInterface.renameTable('videoAbuse', 'abuse')
+
+ await utils.sequelize.query(`
+ ALTER TABLE "abuse"
+ ADD COLUMN "flaggedAccountId" INTEGER REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+ `)
+
+ await utils.sequelize.query(`
+ UPDATE "abuse" SET "videoId" = NULL
+ WHERE "videoId" NOT IN (SELECT "id" FROM "video")
+ `)
+
+ await utils.sequelize.query(`
+ UPDATE "abuse" SET "flaggedAccountId" = "videoChannel"."accountId"
+ FROM "video" INNER JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"
+ WHERE "abuse"."videoId" = "video"."id"
+ `)
+
+ await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_video_id;')
+ await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_reporter_account_id;')
+
+ await utils.sequelize.query(`
+ CREATE TABLE IF NOT EXISTS "videoAbuse" (
+ "id" serial,
+ "startAt" integer DEFAULT NULL,
+ "endAt" integer DEFAULT NULL,
+ "deletedVideo" jsonb DEFAULT NULL,
+ "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ "videoId" integer REFERENCES "video" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
+ "createdAt" TIMESTAMP WITH time zone NOT NULL,
+ "updatedAt" timestamp WITH time zone NOT NULL,
+ PRIMARY KEY ("id")
+ );
+ `)
+
+ await utils.sequelize.query(`
+ CREATE TABLE IF NOT EXISTS "commentAbuse" (
+ "id" serial,
+ "deletedComment" jsonb DEFAULT NULL,
+ "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ "videoCommentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
+ "createdAt" timestamp WITH time zone NOT NULL,
+ "updatedAt" timestamp WITH time zone NOT NULL,
+ "commentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
+ PRIMARY KEY ("id")
+ );
+ `)
+
+ await utils.sequelize.query(`
+ INSERT INTO "videoAbuse" ("startAt", "endAt", "deletedVideo", "abuseId", "videoId", "createdAt", "updatedAt")
+ SELECT "abuse"."startAt", "abuse"."endAt", "abuse"."deletedVideo", "abuse"."id", "abuse"."videoId",
+ "abuse"."createdAt", "abuse"."updatedAt"
+ FROM "abuse"
+ `)
+
+ await utils.queryInterface.removeColumn('abuse', 'startAt')
+ await utils.queryInterface.removeColumn('abuse', 'endAt')
+ await utils.queryInterface.removeColumn('abuse', 'deletedVideo')
+ await utils.queryInterface.removeColumn('abuse', 'videoId')
+
+ await utils.sequelize.query('DROP INDEX IF EXISTS user_notification_video_abuse_id')
+ await utils.queryInterface.renameColumn('userNotification', 'videoAbuseId', 'abuseId')
+ await utils.sequelize.query(
+ 'ALTER TABLE "userNotification" RENAME CONSTRAINT "userNotification_videoAbuseId_fkey" TO "userNotification_abuseId_fkey"'
+ )
+
+ await utils.sequelize.query(
+ 'ALTER TABLE "abuse" RENAME CONSTRAINT "videoAbuse_reporterAccountId_fkey" TO "abuse_reporterAccountId_fkey"'
+ )
+
+ await utils.sequelize.query(
+ 'ALTER INDEX IF EXISTS "videoAbuse_pkey" RENAME TO "abuse_pkey"'
+ )
+
+ await utils.queryInterface.renameColumn('userNotificationSetting', 'videoAbuseAsModerator', 'abuseAsModerator')
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
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 {
const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
emailPayload = {
- template: 'comment-abuse-new',
+ template: 'video-comment-abuse-new',
to,
subject: `New comment abuse report from ${reporter}`,
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'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { UserModel } from '../models/account/user'
import { UserNotificationModel } from '../models/account/user-notification'
-import { MAbuseFull, MAbuseVideo, MAccountServer, MActorFollowFull } from '../types/models'
+import { MAbuseFull, MAccountServer, MActorFollowFull } from '../types/models'
import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
import { isBlockedByServerOrAccount } from './blocklist'
import { Emailer } from './emailer'
@@ -359,12 +359,14 @@ class Notifier {
const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
if (moderators.length === 0) return
- const url = abuseInstance.VideoAbuse?.Video?.url || abuseInstance.VideoCommentAbuse?.VideoComment?.url
+ const url = abuseInstance.VideoAbuse?.Video?.url ||
+ abuseInstance.VideoCommentAbuse?.VideoComment?.url ||
+ abuseInstance.FlaggedAccount.Actor.url
logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
function settingGetter (user: MUserWithNotificationSetting) {
- return user.NotificationSetting.videoAbuseAsModerator
+ return user.NotificationSetting.abuseAsModerator
}
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 |
newCommentOnMyVideo: UserNotificationSettingValue.WEB,
myVideoImportFinished: UserNotificationSettingValue.WEB,
myVideoPublished: UserNotificationSettingValue.WEB,
- videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
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 = [
.custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'),
body('newCommentOnMyVideo')
.custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'),
- body('videoAbuseAsModerator')
- .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'),
+ body('abuseAsModerator')
+ .custom(isUserNotificationSettingValid).withMessage('Should have a valid abuse as moderator notification setting'),
body('videoAutoBlacklistAsModerator')
.custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'),
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 {
} from '@shared/models'
import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
-import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
+import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
import { ThumbnailModel } from '../video/thumbnail'
import { VideoModel } from '../video/video'
import { VideoBlacklistModel } from '../video/video-blacklist'
-import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
+import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel'
+import { VideoCommentModel } from '../video/video-comment'
import { VideoAbuseModel } from './video-abuse'
import { VideoCommentAbuseModel } from './video-comment-abuse'
-import { VideoCommentModel } from '../video/video-comment'
export enum ScopeNames {
FOR_API = 'FOR_API'
@@ -149,7 +149,7 @@ export enum ScopeNames {
'(' +
'SELECT count(*) ' +
'FROM "videoAbuse" ' +
- 'WHERE "videoId" = "VideoAbuse"."videoId" ' +
+ 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' +
')'
),
'countReportsForVideo'
@@ -164,7 +164,7 @@ export enum ScopeNames {
'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
'FROM "videoAbuse" ' +
') t ' +
- 'WHERE t.id = "VideoAbuse".id' +
+ 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' +
')'
),
'nthReportForVideo'
@@ -172,51 +172,22 @@ export enum ScopeNames {
[
literal(
'(' +
- 'SELECT count("videoAbuse"."id") ' +
- 'FROM "videoAbuse" ' +
- 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
- 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
- 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
- 'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
+ 'SELECT count("abuse"."id") ' +
+ 'FROM "abuse" ' +
+ 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' +
')'
),
- 'countReportsForReporter__video'
+ 'countReportsForReporter'
],
[
literal(
'(' +
- 'SELECT count(DISTINCT "videoAbuse"."id") ' +
- 'FROM "videoAbuse" ' +
- `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` +
+ 'SELECT count("abuse"."id") ' +
+ 'FROM "abuse" ' +
+ 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' +
')'
),
- 'countReportsForReporter__deletedVideo'
- ],
- [
- literal(
- '(' +
- 'SELECT count(DISTINCT "videoAbuse"."id") ' +
- 'FROM "videoAbuse" ' +
- 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
- 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
- 'INNER JOIN "account" ON ' +
- '"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' +
- `OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
- ')'
- ),
- 'countReportsForReportee__video'
- ],
- [
- literal(
- '(' +
- 'SELECT count(DISTINCT "videoAbuse"."id") ' +
- 'FROM "videoAbuse" ' +
- `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuse->Video->VideoChannel"."accountId" ` +
- `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
- `CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
- ')'
- ),
- 'countReportsForReportee__deletedVideo'
+ 'countReportsForReportee'
]
]
},
@@ -224,13 +195,18 @@ export enum ScopeNames {
{
model: AccountModel.scope(AccountScopeNames.SUMMARY),
as: 'ReporterAccount',
- required: true,
+ required: !!options.searchReporter,
where: searchAttribute(options.searchReporter, 'name')
},
{
- model: AccountModel.scope(AccountScopeNames.SUMMARY),
+ model: AccountModel.scope({
+ method: [
+ AccountScopeNames.SUMMARY,
+ { actorRequired: false } as AccountSummaryOptions
+ ]
+ }),
as: 'FlaggedAccount',
- required: true,
+ required: !!options.searchReportee,
where: searchAttribute(options.searchReportee, 'name')
},
{
@@ -243,35 +219,36 @@ export enum ScopeNames {
include: [
{
model: VideoModel.unscoped(),
- attributes: [ 'name', 'id', 'uuid' ],
- required: true
+ attributes: [ 'name', 'id', 'uuid' ]
}
]
}
]
},
{
- model: VideoAbuseModel,
+ model: VideoAbuseModel.unscoped(),
required: options.filter === 'video' || !!options.videoIs || videoRequired,
include: [
{
- model: VideoModel,
+ attributes: [ 'id', 'uuid', 'name', 'nsfw' ],
+ model: VideoModel.unscoped(),
required: videoRequired,
where: searchAttribute(options.searchVideo, 'name'),
include: [
{
+ attributes: [ 'filename', 'fileUrl' ],
model: ThumbnailModel
},
{
- model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: false } as SummaryOptions ] }),
+ model: VideoChannelModel.scope({
+ method: [
+ VideoChannelScopeNames.SUMMARY,
+ { withAccount: false, actorRequired: false } as ChannelSummaryOptions
+ ]
+ }),
+
where: searchAttribute(options.searchVideoChannel, 'name'),
- required: true,
- include: [
- {
- model: AccountModel.scope(AccountScopeNames.SUMMARY),
- required: true
- }
- ]
+ required: !!options.searchVideoChannel
},
{
attributes: [ 'id', 'reason', 'unfederated' ],
@@ -304,19 +281,19 @@ export class AbuseModel extends Model {
@AllowNull(false)
@Default(null)
- @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
+ @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
reason: string
@AllowNull(false)
@Default(null)
- @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
+ @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
@Column
state: AbuseState
@AllowNull(true)
@Default(null)
- @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
+ @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
moderationComment: string
@@ -486,12 +463,12 @@ export class AbuseModel extends Model {
toFormattedJSON (this: MAbuseFormattable): Abuse {
const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+
const countReportsForVideo = this.get('countReportsForVideo') as number
const nthReportForVideo = this.get('nthReportForVideo') as number
- const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
- const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number
- const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
- const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
+
+ const countReportsForReporter = this.get('countReportsForReporter') as number
+ const countReportsForReportee = this.get('countReportsForReportee') as number
let video: VideoAbuse
let comment: VideoCommentAbuse
@@ -512,7 +489,11 @@ export class AbuseModel extends Model {
deleted: !abuseModel.Video,
blacklisted: abuseModel.Video?.isBlacklisted() || false,
thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
- channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel
+
+ channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel,
+
+ countReports: countReportsForVideo,
+ nthReport: nthReportForVideo
}
}
@@ -539,7 +520,13 @@ export class AbuseModel extends Model {
reason: this.reason,
predefinedReasons,
- reporterAccount: this.ReporterAccount.toFormattedJSON(),
+ reporterAccount: this.ReporterAccount
+ ? this.ReporterAccount.toFormattedJSON()
+ : null,
+
+ flaggedAccount: this.FlaggedAccount
+ ? this.FlaggedAccount.toFormattedJSON()
+ : null,
state: {
id: this.state,
@@ -553,14 +540,15 @@ export class AbuseModel extends Model {
createdAt: this.createdAt,
updatedAt: this.updatedAt,
- count: countReportsForVideo || 0,
- nth: nthReportForVideo || 0,
- countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
- countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0),
+
+ countReportsForReporter: (countReportsForReporter || 0),
+ countReportsForReportee: (countReportsForReportee || 0),
// FIXME: deprecated in 2.3, remove this
startAt: null,
- endAt: null
+ endAt: null,
+ count: countReportsForVideo || 0,
+ nth: nthReportForVideo || 0
}
}
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 {
}
export type SummaryOptions = {
+ actorRequired?: boolean // Default: true
whereActor?: WhereOptions
withAccountBlockerIds?: number[]
}
@@ -65,12 +66,12 @@ export type SummaryOptions = {
}
const query: FindOptions = {
- attributes: [ 'id', 'name' ],
+ attributes: [ 'id', 'name', 'actorId' ],
include: [
{
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
model: ActorModel.unscoped(),
- required: true,
+ required: options.actorRequired ?? true,
where: whereActor,
include: [
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 throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator')
+ 'UserNotificationSettingAbuseAsModerator',
+ value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator')
)
@Column
- videoAbuseAsModerator: UserNotificationSettingValue
+ abuseAsModerator: UserNotificationSettingValue
@AllowNull(false)
@Default(null)
@@ -166,7 +166,7 @@ export class UserNotificationSettingModel extends Model {
const videoQuotaUsed = this.get('videoQuotaUsed')
const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
const videosCount = this.get('videosCount')
- const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':')
- const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount')
+ const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':')
+ const abusesCreatedCount = this.get('abusesCreatedCount')
const videoCommentsCount = this.get('videoCommentsCount')
const json: User = {
@@ -815,14 +813,14 @@ export class UserModel extends Model {
videosCount: videosCount !== undefined
? parseInt(videosCount + '', 10)
: undefined,
- videoAbusesCount: videoAbusesCount
- ? parseInt(videoAbusesCount, 10)
+ abusesCount: abusesCount
+ ? parseInt(abusesCount, 10)
: undefined,
- videoAbusesAcceptedCount: videoAbusesAcceptedCount
- ? parseInt(videoAbusesAcceptedCount, 10)
+ abusesAcceptedCount: abusesAcceptedCount
+ ? parseInt(abusesAcceptedCount, 10)
: undefined,
- videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined
- ? parseInt(videoAbusesCreatedCount + '', 10)
+ abusesCreatedCount: abusesCreatedCount !== undefined
+ ? parseInt(abusesCreatedCount + '', 10)
: undefined,
videoCommentsCount: videoCommentsCount !== undefined
? 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 = {
}
export type SummaryOptions = {
+ actorRequired?: boolean // Default: true
withAccount?: boolean // Default: false
withAccountBlockerIds?: number[]
}
@@ -121,7 +122,7 @@ export type SummaryOptions = {
{
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
model: ActorModel.unscoped(),
- required: true,
+ required: options.actorRequired ?? true,
include: [
{
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 () {
const correctFields: UserNotificationSetting = {
newVideoFromSubscription: UserNotificationSettingValue.WEB,
newCommentOnMyVideo: UserNotificationSettingValue.WEB,
- videoAbuseAsModerator: UserNotificationSettingValue.WEB,
+ abuseAsModerator: UserNotificationSettingValue.WEB,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
blacklistOnMyVideo: UserNotificationSettingValue.WEB,
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 @@
set -eu
+activitypubFiles=$(find server/tests/api/moderation -type f | grep -v index.ts | xargs echo)
redundancyFiles=$(find server/tests/api/redundancy -type f | grep -v index.ts | xargs echo)
activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo)
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 @@
// Order of the tests we want to execute
import './activitypub'
import './check-params'
+import './moderation'
import './notifications'
import './redundancy'
import './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 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import * as chai from 'chai'
+import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models'
+import {
+ cleanupTests,
+ createUser,
+ deleteVideoAbuse,
+ flushAndRunMultipleServers,
+ getVideoAbusesList,
+ getVideosList,
+ removeVideo,
+ reportVideoAbuse,
+ ServerInfo,
+ setAccessTokensToServers,
+ updateVideoAbuse,
+ uploadVideo,
+ userLogin
+} from '../../../../shared/extra-utils/index'
+import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
+import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
+import {
+ addAccountToServerBlocklist,
+ addServerToServerBlocklist,
+ removeAccountFromServerBlocklist,
+ removeServerFromServerBlocklist
+} from '../../../../shared/extra-utils/users/blocklist'
+
+const expect = chai.expect
+
+describe('Test abuses', function () {
+ let servers: ServerInfo[] = []
+ let abuseServer2: Abuse
+
+ before(async function () {
+ this.timeout(50000)
+
+ // Run servers
+ servers = await flushAndRunMultipleServers(2)
+
+ // Get the access tokens
+ await setAccessTokensToServers(servers)
+
+ // Server 1 and server 2 follow each other
+ await doubleFollow(servers[0], servers[1])
+
+ // Upload some videos on each servers
+ const video1Attributes = {
+ name: 'my super name for server 1',
+ description: 'my super description for server 1'
+ }
+ await uploadVideo(servers[0].url, servers[0].accessToken, video1Attributes)
+
+ const video2Attributes = {
+ name: 'my super name for server 2',
+ description: 'my super description for server 2'
+ }
+ await uploadVideo(servers[1].url, servers[1].accessToken, video2Attributes)
+
+ // Wait videos propagation, server 2 has transcoding enabled
+ await waitJobs(servers)
+
+ const res = await getVideosList(servers[0].url)
+ const videos = res.body.data
+
+ expect(videos.length).to.equal(2)
+
+ servers[0].video = videos.find(video => video.name === 'my super name for server 1')
+ servers[1].video = videos.find(video => video.name === 'my super name for server 2')
+ })
+
+ it('Should not have video abuses', async function () {
+ const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+
+ expect(res.body.total).to.equal(0)
+ expect(res.body.data).to.be.an('array')
+ expect(res.body.data.length).to.equal(0)
+ })
+
+ it('Should report abuse on a local video', async function () {
+ this.timeout(15000)
+
+ const reason = 'my super bad reason'
+ await reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[0].video.id, reason)
+
+ // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2
+ await waitJobs(servers)
+ })
+
+ it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
+ const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+
+ expect(res1.body.total).to.equal(1)
+ expect(res1.body.data).to.be.an('array')
+ expect(res1.body.data.length).to.equal(1)
+
+ const abuse: Abuse = res1.body.data[0]
+ expect(abuse.reason).to.equal('my super bad reason')
+ expect(abuse.reporterAccount.name).to.equal('root')
+ expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
+ expect(abuse.video.id).to.equal(servers[0].video.id)
+ expect(abuse.video.channel).to.exist
+ expect(abuse.count).to.equal(1)
+ expect(abuse.nth).to.equal(1)
+ expect(abuse.countReportsForReporter).to.equal(1)
+ expect(abuse.countReportsForReportee).to.equal(1)
+
+ const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+ expect(res2.body.total).to.equal(0)
+ expect(res2.body.data).to.be.an('array')
+ expect(res2.body.data.length).to.equal(0)
+ })
+
+ it('Should report abuse on a remote video', async function () {
+ this.timeout(10000)
+
+ const reason = 'my super bad reason 2'
+ await reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[1].video.id, reason)
+
+ // We wait requests propagation
+ await waitJobs(servers)
+ })
+
+ it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
+ const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+ expect(res1.body.total).to.equal(2)
+ expect(res1.body.data).to.be.an('array')
+ expect(res1.body.data.length).to.equal(2)
+
+ const abuse1: Abuse = res1.body.data[0]
+ expect(abuse1.reason).to.equal('my super bad reason')
+ expect(abuse1.reporterAccount.name).to.equal('root')
+ expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
+ expect(abuse1.video.id).to.equal(servers[0].video.id)
+ expect(abuse1.state.id).to.equal(AbuseState.PENDING)
+ expect(abuse1.state.label).to.equal('Pending')
+ expect(abuse1.moderationComment).to.be.null
+ expect(abuse1.count).to.equal(1)
+ expect(abuse1.nth).to.equal(1)
+
+ const abuse2: Abuse = res1.body.data[1]
+ expect(abuse2.reason).to.equal('my super bad reason 2')
+ expect(abuse2.reporterAccount.name).to.equal('root')
+ expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
+ expect(abuse2.video.id).to.equal(servers[1].video.id)
+ expect(abuse2.state.id).to.equal(AbuseState.PENDING)
+ expect(abuse2.state.label).to.equal('Pending')
+ expect(abuse2.moderationComment).to.be.null
+
+ const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+ expect(res2.body.total).to.equal(1)
+ expect(res2.body.data).to.be.an('array')
+ expect(res2.body.data.length).to.equal(1)
+
+ abuseServer2 = res2.body.data[0]
+ expect(abuseServer2.reason).to.equal('my super bad reason 2')
+ expect(abuseServer2.reporterAccount.name).to.equal('root')
+ expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
+ expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
+ expect(abuseServer2.state.label).to.equal('Pending')
+ expect(abuseServer2.moderationComment).to.be.null
+ })
+
+ it('Should update the state of a video abuse', async function () {
+ const body = { state: AbuseState.REJECTED }
+ await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
+
+ const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+ expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
+ })
+
+ it('Should add a moderation comment', async function () {
+ const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
+ await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
+
+ const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+ expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
+ expect(res.body.data[0].moderationComment).to.equal('It is valid')
+ })
+
+ it('Should hide video abuses from blocked accounts', async function () {
+ this.timeout(10000)
+
+ {
+ await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this')
+ await waitJobs(servers)
+
+ const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+ expect(res.body.total).to.equal(3)
+ }
+
+ const accountToBlock = 'root@localhost:' + servers[1].port
+
+ {
+ await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
+
+ const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+ expect(res.body.total).to.equal(2)
+
+ const abuse = res.body.data.find(a => a.reason === 'will mute this')
+ expect(abuse).to.be.undefined
+ }
+
+ {
+ await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
+
+ const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+ expect(res.body.total).to.equal(3)
+ }
+ })
+
+ it('Should hide video abuses from blocked servers', async function () {
+ const serverToBlock = servers[1].host
+
+ {
+ await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
+
+ const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+ expect(res.body.total).to.equal(2)
+
+ const abuse = res.body.data.find(a => a.reason === 'will mute this')
+ expect(abuse).to.be.undefined
+ }
+
+ {
+ await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock)
+
+ const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+ expect(res.body.total).to.equal(3)
+ }
+ })
+
+ it('Should keep the video abuse when deleting the video', async function () {
+ this.timeout(10000)
+
+ await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid)
+
+ await waitJobs(servers)
+
+ const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+ expect(res.body.total).to.equal(2, "wrong number of videos returned")
+ expect(res.body.data.length).to.equal(2, "wrong number of videos returned")
+ expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video")
+
+ const abuse: Abuse = res.body.data[0]
+ expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
+ expect(abuse.video.channel).to.exist
+ expect(abuse.video.deleted).to.be.true
+ })
+
+ it('Should include counts of reports from reporter and reportee', async function () {
+ this.timeout(10000)
+
+ // register a second user to have two reporters/reportees
+ const user = { username: 'user2', password: 'password' }
+ await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user })
+ const userAccessToken = await userLogin(servers[0], user)
+
+ // upload a third video via this user
+ const video3Attributes = {
+ name: 'my second super name for server 1',
+ description: 'my second super description for server 1'
+ }
+ await uploadVideo(servers[0].url, userAccessToken, video3Attributes)
+
+ const res1 = await getVideosList(servers[0].url)
+ const videos = res1.body.data
+ const video3 = videos.find(video => video.name === 'my second super name for server 1')
+
+ // resume with the test
+ const reason3 = 'my super bad reason 3'
+ await reportVideoAbuse(servers[0].url, servers[0].accessToken, video3.id, reason3)
+ const reason4 = 'my super bad reason 4'
+ await reportVideoAbuse(servers[0].url, userAccessToken, servers[0].video.id, reason4)
+
+ const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+
+ {
+ for (const abuse of res2.body.data as Abuse[]) {
+ if (abuse.video.id === video3.id) {
+ expect(abuse.count).to.equal(1, "wrong reports count for video 3")
+ expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3")
+ expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse")
+ expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
+ }
+ if (abuse.video.id === servers[0].video.id) {
+ expect(abuse.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse")
+ }
+ }
+ }
+ })
+
+ it('Should list predefined reasons as well as timestamps for the reported video', async function () {
+ this.timeout(10000)
+
+ const reason5 = 'my super bad reason 5'
+ const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
+ const createdAbuse = (await reportVideoAbuse(
+ servers[0].url,
+ servers[0].accessToken,
+ servers[0].video.id,
+ reason5,
+ predefinedReasons5,
+ 1,
+ 5
+ )).body.abuse
+
+ const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+
+ {
+ const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id)
+ expect(abuse.reason).to.equals(reason5)
+ expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
+ expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
+ expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
+ }
+ })
+
+ it('Should delete the video abuse', async function () {
+ this.timeout(10000)
+
+ await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id)
+
+ await waitJobs(servers)
+
+ {
+ const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+ expect(res.body.total).to.equal(1)
+ expect(res.body.data.length).to.equal(1)
+ expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
+ }
+
+ {
+ const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+ expect(res.body.total).to.equal(6)
+ }
+ })
+
+ it('Should list and filter video abuses', async function () {
+ async function list (query: Omit[0], 'url' | 'token'>) {
+ const options = {
+ url: servers[0].url,
+ token: servers[0].accessToken
+ }
+
+ Object.assign(options, query)
+
+ const res = await getVideoAbusesList(options)
+
+ return res.body.data as Abuse[]
+ }
+
+ expect(await list({ id: 56 })).to.have.lengthOf(0)
+ expect(await list({ id: 1 })).to.have.lengthOf(1)
+
+ expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4)
+ expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
+
+ expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
+
+ expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4)
+ expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
+
+ expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
+ expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
+
+ expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
+ expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
+
+ expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
+ expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
+
+ expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
+ expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
+
+ expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
+ expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
+ })
+
+ after(async function () {
+ await cleanupTests(servers)
+ })
+})
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/moderation/blocklist.ts
similarity index 100%
rename from server/tests/api/users/blocklist.ts
rename to 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 @@
+export * from './abuses'
+export * 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 {
MockInstancesIndex,
registerUser,
removeVideoFromBlacklist,
- reportVideoAbuse,
+ reportAbuse,
unfollow,
updateCustomConfig,
updateCustomSubConfig,
@@ -74,12 +74,12 @@ describe('Test moderation notifications', function () {
const name = 'video for abuse ' + uuidv4()
const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
- const uuid = resVideo.body.video.uuid
+ const video = resVideo.body.video
- await reportVideoAbuse(servers[0].url, servers[0].accessToken, uuid, 'super reason')
+ await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video.id, reason: 'super reason' })
await waitJobs(servers)
- await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence')
+ await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
})
it('Should send a notification to moderators on remote video abuse', async function () {
@@ -87,14 +87,14 @@ describe('Test moderation notifications', function () {
const name = 'video for abuse ' + uuidv4()
const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
- const uuid = resVideo.body.video.uuid
+ const video = resVideo.body.video
await waitJobs(servers)
- await reportVideoAbuse(servers[1].url, servers[1].accessToken, uuid, 'super reason')
+ await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId: video.id, reason: 'super reason' })
await waitJobs(servers)
- await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence')
+ await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
})
})
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 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import * as chai from 'chai'
import 'mocha'
+import * as chai from 'chai'
import {
addVideoToBlacklist,
askResetPassword,
@@ -11,7 +11,7 @@ import {
createUser,
flushAndRunServer,
removeVideoFromBlacklist,
- reportVideoAbuse,
+ reportAbuse,
resetPassword,
ServerInfo,
setAccessTokensToServers,
@@ -30,10 +30,15 @@ describe('Test emails', function () {
let userId: number
let userId2: number
let userAccessToken: string
+
let videoUUID: string
+ let videoId: number
+
let videoUserUUID: string
+
let verificationString: string
let verificationString2: string
+
const emails: object[] = []
const user = {
username: 'user_1',
@@ -76,6 +81,7 @@ describe('Test emails', function () {
}
const res = await uploadVideo(server.url, server.accessToken, attributes)
videoUUID = res.body.video.uuid
+ videoId = res.body.video.id
}
})
@@ -179,7 +185,7 @@ describe('Test emails', function () {
this.timeout(10000)
const reason = 'my super bad reason'
- await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason)
+ await reportAbuse({ url: server.url, token: server.accessToken, videoId, reason })
await waitJobs(server)
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 @@
-import './users-verification'
-import './blocklist'
import './user-subscriptions'
import './users'
import './users-multiple-servers'
+import './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 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import * as chai from 'chai'
import 'mocha'
-import { MyUser, User, UserRole, Video, AbuseState, AbuseUpdate, VideoPlaylistType } from '@shared/models'
+import * as chai from 'chai'
+import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models'
+import { CustomConfig } from '@shared/models/server'
import {
addVideoCommentThread,
blockUser,
@@ -10,6 +11,7 @@ import {
createUser,
deleteMe,
flushAndRunServer,
+ getAbusesList,
getAccountRatings,
getBlacklistedVideosList,
getCustomConfig,
@@ -19,7 +21,6 @@ import {
getUserInformation,
getUsersList,
getUsersListPaginationAndSort,
- getVideoAbusesList,
getVideoChannel,
getVideosList,
installPlugin,
@@ -29,15 +30,15 @@ import {
registerUserWithChannel,
removeUser,
removeVideo,
- reportVideoAbuse,
+ reportAbuse,
ServerInfo,
testImage,
unblockUser,
+ updateAbuse,
updateCustomSubConfig,
updateMyAvatar,
updateMyUser,
updateUser,
- updateVideoAbuse,
uploadVideo,
userLogin,
waitJobs
@@ -46,7 +47,6 @@ import { follow } from '../../../../shared/extra-utils/server/follows'
import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
-import { CustomConfig } from '@shared/models/server'
const expect = chai.expect
@@ -302,10 +302,10 @@ describe('Test users', function () {
expect(userGet.videosCount).to.equal(0)
expect(userGet.videoCommentsCount).to.be.a('number')
expect(userGet.videoCommentsCount).to.equal(0)
- expect(userGet.videoAbusesCount).to.be.a('number')
- expect(userGet.videoAbusesCount).to.equal(0)
- expect(userGet.videoAbusesAcceptedCount).to.be.a('number')
- expect(userGet.videoAbusesAcceptedCount).to.equal(0)
+ expect(userGet.abusesCount).to.be.a('number')
+ expect(userGet.abusesCount).to.equal(0)
+ expect(userGet.abusesAcceptedCount).to.be.a('number')
+ expect(userGet.abusesAcceptedCount).to.equal(0)
})
})
@@ -895,9 +895,9 @@ describe('Test users', function () {
expect(user.videosCount).to.equal(0)
expect(user.videoCommentsCount).to.equal(0)
- expect(user.videoAbusesCount).to.equal(0)
- expect(user.videoAbusesCreatedCount).to.equal(0)
- expect(user.videoAbusesAcceptedCount).to.equal(0)
+ expect(user.abusesCount).to.equal(0)
+ expect(user.abusesCreatedCount).to.equal(0)
+ expect(user.abusesAcceptedCount).to.equal(0)
})
it('Should report correct videos count', async function () {
@@ -924,26 +924,26 @@ describe('Test users', function () {
expect(user.videoCommentsCount).to.equal(1)
})
- it('Should report correct video abuses counts', async function () {
+ it('Should report correct abuses counts', async function () {
const reason = 'my super bad reason'
- await reportVideoAbuse(server.url, user17AccessToken, videoId, reason)
+ await reportAbuse({ url: server.url, token: user17AccessToken, videoId, reason })
- const res1 = await getVideoAbusesList({ url: server.url, token: server.accessToken })
+ const res1 = await getAbusesList({ url: server.url, token: server.accessToken })
const abuseId = res1.body.data[0].id
const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user2: User = res2.body
- expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
- expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created
+ expect(user2.abusesCount).to.equal(1) // number of incriminations
+ expect(user2.abusesCreatedCount).to.equal(1) // number of reports created
const body: AbuseUpdate = { state: AbuseState.ACCEPTED }
- await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body)
+ await updateAbuse(server.url, server.accessToken, abuseId, body)
const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user3: User = res3.body
- expect(user3.videoAbusesAcceptedCount).to.equal(1) // number of reports created accepted
+ expect(user3.abusesAcceptedCount).to.equal(1) // number of reports created accepted
})
})
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 () {
expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
expect(abuse.video.id).to.equal(servers[0].video.id)
expect(abuse.video.channel).to.exist
- expect(abuse.count).to.equal(1)
- expect(abuse.nth).to.equal(1)
+ expect(abuse.video.countReports).to.equal(1)
+ expect(abuse.video.nthReport).to.equal(1)
expect(abuse.countReportsForReporter).to.equal(1)
expect(abuse.countReportsForReportee).to.equal(1)
@@ -138,8 +138,8 @@ describe('Test video abuses', function () {
expect(abuse1.state.id).to.equal(AbuseState.PENDING)
expect(abuse1.state.label).to.equal('Pending')
expect(abuse1.moderationComment).to.be.null
- expect(abuse1.count).to.equal(1)
- expect(abuse1.nth).to.equal(1)
+ expect(abuse1.video.countReports).to.equal(1)
+ expect(abuse1.video.nthReport).to.equal(1)
const abuse2: Abuse = res1.body.data[1]
expect(abuse2.reason).to.equal('my super bad reason 2')
@@ -281,8 +281,8 @@ describe('Test video abuses', function () {
{
for (const abuse of res2.body.data as Abuse[]) {
if (abuse.video.id === video3.id) {
- expect(abuse.count).to.equal(1, "wrong reports count for video 3")
- expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3")
+ expect(abuse.video.countReports).to.equal(1, "wrong reports count for video 3")
+ expect(abuse.video.nthReport).to.equal(1, "wrong report position in report list for video 3")
expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse")
expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
}
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 =
export type MAbuseFormattable =
MAbuse &
Use<'ReporterAccount', MAccountFormattable> &
+ Use<'FlaggedAccount', MAccountFormattable> &
Use<'VideoAbuse', MVideoAbuseFormattable> &
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 () {
return {
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
- videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+ abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
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 {
thumbnailPath?: string
channel?: VideoChannel
+
+ countReports: number
+ nthReport: number
}
export interface VideoCommentAbuse {
@@ -36,9 +39,12 @@ export interface VideoCommentAbuse {
export interface Abuse {
id: number
+
reason: string
predefinedReasons?: AbusePredefinedReasonsString[]
+
reporterAccount: Account
+ flaggedAccount: Account
state: VideoConstant
moderationComment?: string
@@ -49,13 +55,18 @@ export interface Abuse {
createdAt: Date
updatedAt: Date
- // FIXME: deprecated in 2.3, remove this
- startAt: null
- endAt: null
-
- count?: number
- nth?: number
-
countReportsForReporter?: number
countReportsForReportee?: number
+
+ // FIXME: deprecated in 2.3, remove the following properties
+
+ // // @deprecated
+ // startAt: null
+ // // @deprecated
+ // endAt: null
+
+ // // @deprecated
+ // count?: number
+ // // @deprecated
+ // nth?: number
}
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 {
export interface UserNotificationSetting {
newVideoFromSubscription: UserNotificationSettingValue
newCommentOnMyVideo: UserNotificationSettingValue
- videoAbuseAsModerator: UserNotificationSettingValue
+ abuseAsModerator: UserNotificationSettingValue
videoAutoBlacklistAsModerator: UserNotificationSettingValue
blacklistOnMyVideo: UserNotificationSettingValue
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 {
videoQuotaDaily: number
videoQuotaUsed?: number
videoQuotaUsedDaily?: number
+
videosCount?: number
- videoAbusesCount?: number
- videoAbusesAcceptedCount?: number
- videoAbusesCreatedCount?: number
+
+ abusesCount?: number
+ abusesAcceptedCount?: number
+ abusesCreatedCount?: number
+
videoCommentsCount? : number
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:
$ref: '#/components/schemas/NotificationSettingValue'
newCommentOnMyVideo:
$ref: '#/components/schemas/NotificationSettingValue'
- videoAbuseAsModerator:
+ abuseAsModerator:
$ref: '#/components/schemas/NotificationSettingValue'
videoAutoBlacklistAsModerator:
$ref: '#/components/schemas/NotificationSettingValue'
@@ -1618,7 +1618,7 @@ paths:
type: object
properties:
state:
- $ref: '#/components/schemas/VideoAbuseStateSet'
+ $ref: '#/components/schemas/AbuseStateSet'
moderationComment:
type: string
description: Update the report comment visible only to the moderation team
@@ -3584,20 +3584,20 @@ components:
label:
type: string
- VideoAbuseStateSet:
+ AbuseStateSet:
type: integer
enum:
- 1
- 2
- 3
description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)'
- VideoAbuseStateConstant:
+ AbuseStateConstant:
properties:
id:
- $ref: '#/components/schemas/VideoAbuseStateSet'
+ $ref: '#/components/schemas/AbuseStateSet'
label:
type: string
- VideoAbusePredefinedReasons:
+ AbusePredefinedReasons:
type: array
items:
type: string
@@ -3960,11 +3960,11 @@ components:
type: string
example: The video is a spam
predefinedReasons:
- $ref: '#/components/schemas/VideoAbusePredefinedReasons'
+ $ref: '#/components/schemas/AbusePredefinedReasons'
reporterAccount:
$ref: '#/components/schemas/Account'
state:
- $ref: '#/components/schemas/VideoAbuseStateConstant'
+ $ref: '#/components/schemas/AbuseStateConstant'
moderationComment:
type: string
example: Decided to ban the server since it spams us regularly
@@ -4690,11 +4690,11 @@ components:
description: The user daily video quota
videosCount:
type: integer
- videoAbusesCount:
+ abusesCount:
type: integer
- videoAbusesAcceptedCount:
+ abusesAcceptedCount:
type: integer
- videoAbusesCreatedCount:
+ abusesCreatedCount:
type: integer
videoCommentsCount:
type: integer