Add migrations

This commit is contained in:
Chocobozzz 2020-07-07 14:34:16 +02:00 committed by Chocobozzz
parent 57f6896f67
commit 4f32032fed
34 changed files with 675 additions and 183 deletions

View File

@ -45,7 +45,7 @@ export class AdminComponent implements OnInit {
children: [] children: []
} }
if (this.hasVideoAbusesRight()) { if (this.hasAbusesRight()) {
moderationItems.children.push({ moderationItems.children.push({
label: this.i18n('Video reports'), label: this.i18n('Video reports'),
routerLink: '/admin/moderation/video-abuses/list', 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.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' })
if (this.hasServerFollowRight()) this.menuEntries.push(federationItems) 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.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.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' }) 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) return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW)
} }
hasVideoAbusesRight () { hasAbusesRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES) return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES)
} }

View File

@ -37,14 +37,14 @@
</a> </a>
</div> </div>
<div> <div>
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + user?.account.displayName + '&quot;' }"> <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + user?.account.displayName + '&quot;' }">
<div class="dashboard-num">{{ user.videoAbusesCount }}</div> <div class="dashboard-num">{{ user.abusesCount }}</div>
<div class="dashboard-label" i18n>Incriminated in reports</div> <div class="dashboard-label" i18n>Incriminated in reports</div>
</a> </a>
</div> </div>
<div> <div>
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + user?.account.displayName + '&quot; state:accepted' }"> <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + user?.account.displayName + '&quot; state:accepted' }">
<div class="dashboard-num">{{ user.videoAbusesAcceptedCount }} / {{ user.videoAbusesCreatedCount }}</div> <div class="dashboard-num">{{ user.abusesAcceptedCount }} / {{ user.abusesCreatedCount }}</div>
<div class="dashboard-label" i18n>Authored reports accepted</div> <div class="dashboard-label" i18n>Authored reports accepted</div>
</a> </a>
</div> </div>

View File

@ -33,7 +33,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
this.labelNotifications = { this.labelNotifications = {
newVideoFromSubscription: this.i18n('New video from your subscriptions'), newVideoFromSubscription: this.i18n('New video from your subscriptions'),
newCommentOnMyVideo: this.i18n('New comment on your video'), 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'), videoAutoBlacklistAsModerator: this.i18n('Video blocked automatically waiting review'),
blacklistOnMyVideo: this.i18n('One of your video is blocked/unblocked'), blacklistOnMyVideo: this.i18n('One of your video is blocked/unblocked'),
myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'), 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.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
this.rightNotifications = { this.rightNotifications = {
videoAbuseAsModerator: UserRight.MANAGE_ABUSES, abuseAsModerator: UserRight.MANAGE_ABUSES,
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
newUserRegistration: UserRight.MANAGE_USERS, newUserRegistration: UserRight.MANAGE_USERS,
newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,

View File

@ -51,12 +51,14 @@ export class User implements UserServerModel {
videoQuotaDaily: number videoQuotaDaily: number
videoQuotaUsed?: number videoQuotaUsed?: number
videoQuotaUsedDaily?: number videoQuotaUsedDaily?: number
videosCount?: number videosCount?: number
videoAbusesCount?: number
videoAbusesAcceptedCount?: number
videoAbusesCreatedCount?: number
videoCommentsCount?: number videoCommentsCount?: number
abusesCount?: number
abusesAcceptedCount?: number
abusesCreatedCount?: number
theme: string theme: string
account: Account account: Account
@ -89,9 +91,9 @@ export class User implements UserServerModel {
this.videoQuotaUsed = hash.videoQuotaUsed this.videoQuotaUsed = hash.videoQuotaUsed
this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
this.videosCount = hash.videosCount this.videosCount = hash.videosCount
this.videoAbusesCount = hash.videoAbusesCount this.abusesCount = hash.abusesCount
this.videoAbusesAcceptedCount = hash.videoAbusesAcceptedCount this.abusesAcceptedCount = hash.abusesAcceptedCount
this.videoAbusesCreatedCount = hash.videoAbusesCreatedCount this.abusesCreatedCount = hash.abusesCreatedCount
this.videoCommentsCount = hash.videoCommentsCount this.videoCommentsCount = hash.videoCommentsCount
this.nsfwPolicy = hash.nsfwPolicy this.nsfwPolicy = hash.nsfwPolicy

View File

@ -68,7 +68,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
const values: UserNotificationSetting = { const values: UserNotificationSetting = {
newVideoFromSubscription: body.newVideoFromSubscription, newVideoFromSubscription: body.newVideoFromSubscription,
newCommentOnMyVideo: body.newCommentOnMyVideo, newCommentOnMyVideo: body.newCommentOnMyVideo,
videoAbuseAsModerator: body.videoAbuseAsModerator, abuseAsModerator: body.abuseAsModerator,
videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator, videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: body.blacklistOnMyVideo, blacklistOnMyVideo: body.blacklistOnMyVideo,
myVideoPublished: body.myVideoPublished, myVideoPublished: body.myVideoPublished,

View File

@ -23,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 515 const LAST_MIGRATION_VERSION = 520
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,92 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
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
}

View File

@ -320,7 +320,7 @@ class Emailer {
const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId() const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
emailPayload = { emailPayload = {
template: 'comment-abuse-new', template: 'video-comment-abuse-new',
to, to,
subject: `New comment abuse report from ${reporter}`, subject: `New comment abuse report from ${reporter}`,
locals: { locals: {

View File

@ -18,7 +18,7 @@ import { CONFIG } from '../initializers/config'
import { AccountBlocklistModel } from '../models/account/account-blocklist' import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { UserNotificationModel } from '../models/account/user-notification' 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 { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
import { isBlockedByServerOrAccount } from './blocklist' import { isBlockedByServerOrAccount } from './blocklist'
import { Emailer } from './emailer' import { Emailer } from './emailer'
@ -359,12 +359,14 @@ class Notifier {
const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES) const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
if (moderators.length === 0) return 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) logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
function settingGetter (user: MUserWithNotificationSetting) { function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.videoAbuseAsModerator return user.NotificationSetting.abuseAsModerator
} }
async function notificationCreator (user: MUserWithNotificationSetting) { async function notificationCreator (user: MUserWithNotificationSetting) {

View File

@ -133,7 +133,7 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
newCommentOnMyVideo: UserNotificationSettingValue.WEB, newCommentOnMyVideo: UserNotificationSettingValue.WEB,
myVideoImportFinished: UserNotificationSettingValue.WEB, myVideoImportFinished: UserNotificationSettingValue.WEB,
myVideoPublished: UserNotificationSettingValue.WEB, myVideoPublished: UserNotificationSettingValue.WEB,
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newUserRegistration: UserNotificationSettingValue.WEB, newUserRegistration: UserNotificationSettingValue.WEB,

View File

@ -25,8 +25,8 @@ const updateNotificationSettingsValidator = [
.custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'),
body('newCommentOnMyVideo') body('newCommentOnMyVideo')
.custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'),
body('videoAbuseAsModerator') body('abuseAsModerator')
.custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'), .custom(isUserNotificationSettingValid).withMessage('Should have a valid abuse as moderator notification setting'),
body('videoAutoBlacklistAsModerator') body('videoAutoBlacklistAsModerator')
.custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'), .custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'),
body('blacklistOnMyVideo') body('blacklistOnMyVideo')

View File

@ -31,15 +31,15 @@ import {
} from '@shared/models' } from '@shared/models'
import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' 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 { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
import { ThumbnailModel } from '../video/thumbnail' import { ThumbnailModel } from '../video/thumbnail'
import { VideoModel } from '../video/video' import { VideoModel } from '../video/video'
import { VideoBlacklistModel } from '../video/video-blacklist' 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 { VideoAbuseModel } from './video-abuse'
import { VideoCommentAbuseModel } from './video-comment-abuse' import { VideoCommentAbuseModel } from './video-comment-abuse'
import { VideoCommentModel } from '../video/video-comment'
export enum ScopeNames { export enum ScopeNames {
FOR_API = 'FOR_API' FOR_API = 'FOR_API'
@ -149,7 +149,7 @@ export enum ScopeNames {
'(' + '(' +
'SELECT count(*) ' + 'SELECT count(*) ' +
'FROM "videoAbuse" ' + 'FROM "videoAbuse" ' +
'WHERE "videoId" = "VideoAbuse"."videoId" ' + 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' +
')' ')'
), ),
'countReportsForVideo' 'countReportsForVideo'
@ -164,7 +164,7 @@ export enum ScopeNames {
'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
'FROM "videoAbuse" ' + 'FROM "videoAbuse" ' +
') t ' + ') t ' +
'WHERE t.id = "VideoAbuse".id' + 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' +
')' ')'
), ),
'nthReportForVideo' 'nthReportForVideo'
@ -172,51 +172,22 @@ export enum ScopeNames {
[ [
literal( literal(
'(' + '(' +
'SELECT count("videoAbuse"."id") ' + 'SELECT count("abuse"."id") ' +
'FROM "videoAbuse" ' + 'FROM "abuse" ' +
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
')' ')'
), ),
'countReportsForReporter__video' 'countReportsForReporter'
], ],
[ [
literal( literal(
'(' + '(' +
'SELECT count(DISTINCT "videoAbuse"."id") ' + 'SELECT count("abuse"."id") ' +
'FROM "videoAbuse" ' + 'FROM "abuse" ' +
`WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` + 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' +
')' ')'
), ),
'countReportsForReporter__deletedVideo' 'countReportsForReportee'
],
[
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'
] ]
] ]
}, },
@ -224,13 +195,18 @@ export enum ScopeNames {
{ {
model: AccountModel.scope(AccountScopeNames.SUMMARY), model: AccountModel.scope(AccountScopeNames.SUMMARY),
as: 'ReporterAccount', as: 'ReporterAccount',
required: true, required: !!options.searchReporter,
where: searchAttribute(options.searchReporter, 'name') where: searchAttribute(options.searchReporter, 'name')
}, },
{ {
model: AccountModel.scope(AccountScopeNames.SUMMARY), model: AccountModel.scope({
method: [
AccountScopeNames.SUMMARY,
{ actorRequired: false } as AccountSummaryOptions
]
}),
as: 'FlaggedAccount', as: 'FlaggedAccount',
required: true, required: !!options.searchReportee,
where: searchAttribute(options.searchReportee, 'name') where: searchAttribute(options.searchReportee, 'name')
}, },
{ {
@ -243,35 +219,36 @@ export enum ScopeNames {
include: [ include: [
{ {
model: VideoModel.unscoped(), model: VideoModel.unscoped(),
attributes: [ 'name', 'id', 'uuid' ], attributes: [ 'name', 'id', 'uuid' ]
required: true
} }
] ]
} }
] ]
}, },
{ {
model: VideoAbuseModel, model: VideoAbuseModel.unscoped(),
required: options.filter === 'video' || !!options.videoIs || videoRequired, required: options.filter === 'video' || !!options.videoIs || videoRequired,
include: [ include: [
{ {
model: VideoModel, attributes: [ 'id', 'uuid', 'name', 'nsfw' ],
model: VideoModel.unscoped(),
required: videoRequired, required: videoRequired,
where: searchAttribute(options.searchVideo, 'name'), where: searchAttribute(options.searchVideo, 'name'),
include: [ include: [
{ {
attributes: [ 'filename', 'fileUrl' ],
model: ThumbnailModel 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'), where: searchAttribute(options.searchVideoChannel, 'name'),
required: true, required: !!options.searchVideoChannel
include: [
{
model: AccountModel.scope(AccountScopeNames.SUMMARY),
required: true
}
]
}, },
{ {
attributes: [ 'id', 'reason', 'unfederated' ], attributes: [ 'id', 'reason', 'unfederated' ],
@ -304,19 +281,19 @@ export class AbuseModel extends Model<AbuseModel> {
@AllowNull(false) @AllowNull(false)
@Default(null) @Default(null)
@Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
reason: string reason: string
@AllowNull(false) @AllowNull(false)
@Default(null) @Default(null)
@Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
@Column @Column
state: AbuseState state: AbuseState
@AllowNull(true) @AllowNull(true)
@Default(null) @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)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
moderationComment: string moderationComment: string
@ -486,12 +463,12 @@ export class AbuseModel extends Model<AbuseModel> {
toFormattedJSON (this: MAbuseFormattable): Abuse { toFormattedJSON (this: MAbuseFormattable): Abuse {
const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
const countReportsForVideo = this.get('countReportsForVideo') as number const countReportsForVideo = this.get('countReportsForVideo') as number
const nthReportForVideo = this.get('nthReportForVideo') 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 countReportsForReporter = this.get('countReportsForReporter') as number
const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number const countReportsForReportee = this.get('countReportsForReportee') as number
const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
let video: VideoAbuse let video: VideoAbuse
let comment: VideoCommentAbuse let comment: VideoCommentAbuse
@ -512,7 +489,11 @@ export class AbuseModel extends Model<AbuseModel> {
deleted: !abuseModel.Video, deleted: !abuseModel.Video,
blacklisted: abuseModel.Video?.isBlacklisted() || false, blacklisted: abuseModel.Video?.isBlacklisted() || false,
thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), 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<AbuseModel> {
reason: this.reason, reason: this.reason,
predefinedReasons, predefinedReasons,
reporterAccount: this.ReporterAccount.toFormattedJSON(), reporterAccount: this.ReporterAccount
? this.ReporterAccount.toFormattedJSON()
: null,
flaggedAccount: this.FlaggedAccount
? this.FlaggedAccount.toFormattedJSON()
: null,
state: { state: {
id: this.state, id: this.state,
@ -553,14 +540,15 @@ export class AbuseModel extends Model<AbuseModel> {
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
count: countReportsForVideo || 0,
nth: nthReportForVideo || 0, countReportsForReporter: (countReportsForReporter || 0),
countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), countReportsForReportee: (countReportsForReportee || 0),
countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0),
// FIXME: deprecated in 2.3, remove this // FIXME: deprecated in 2.3, remove this
startAt: null, startAt: null,
endAt: null endAt: null,
count: countReportsForVideo || 0,
nth: nthReportForVideo || 0
} }
} }

View File

@ -42,6 +42,7 @@ export enum ScopeNames {
} }
export type SummaryOptions = { export type SummaryOptions = {
actorRequired?: boolean // Default: true
whereActor?: WhereOptions whereActor?: WhereOptions
withAccountBlockerIds?: number[] withAccountBlockerIds?: number[]
} }
@ -65,12 +66,12 @@ export type SummaryOptions = {
} }
const query: FindOptions = { const query: FindOptions = {
attributes: [ 'id', 'name' ], attributes: [ 'id', 'name', 'actorId' ],
include: [ include: [
{ {
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
model: ActorModel.unscoped(), model: ActorModel.unscoped(),
required: true, required: options.actorRequired ?? true,
where: whereActor, where: whereActor,
include: [ include: [
serverInclude, serverInclude,

View File

@ -51,11 +51,11 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
@AllowNull(false) @AllowNull(false)
@Default(null) @Default(null)
@Is( @Is(
'UserNotificationSettingVideoAbuseAsModerator', 'UserNotificationSettingAbuseAsModerator',
value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator')
) )
@Column @Column
videoAbuseAsModerator: UserNotificationSettingValue abuseAsModerator: UserNotificationSettingValue
@AllowNull(false) @AllowNull(false)
@Default(null) @Default(null)
@ -166,7 +166,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
return { return {
newCommentOnMyVideo: this.newCommentOnMyVideo, newCommentOnMyVideo: this.newCommentOnMyVideo,
newVideoFromSubscription: this.newVideoFromSubscription, newVideoFromSubscription: this.newVideoFromSubscription,
videoAbuseAsModerator: this.videoAbuseAsModerator, abuseAsModerator: this.abuseAsModerator,
videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: this.blacklistOnMyVideo, blacklistOnMyVideo: this.blacklistOnMyVideo,
myVideoPublished: this.myVideoPublished, myVideoPublished: this.myVideoPublished,

View File

@ -168,28 +168,26 @@ enum ScopeNames {
'(' + '(' +
`SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
'FROM (' + 'FROM (' +
'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + 'SELECT COUNT("abuse"."id") AS "abuses", ' +
`COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
'FROM "videoAbuse" ' + 'FROM "abuse" ' +
'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
'WHERE "account"."userId" = "UserModel"."id"' + 'WHERE "account"."userId" = "UserModel"."id"' +
') t' + ') t' +
')' ')'
), ),
'videoAbusesCount' 'abusesCount'
], ],
[ [
literal( literal(
'(' + '(' +
'SELECT COUNT("videoAbuse"."id") ' + 'SELECT COUNT("abuse"."id") ' +
'FROM "videoAbuse" ' + 'FROM "abuse" ' +
'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' + 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' +
'WHERE "account"."userId" = "UserModel"."id"' + 'WHERE "account"."userId" = "UserModel"."id"' +
')' ')'
), ),
'videoAbusesCreatedCount' 'abusesCreatedCount'
], ],
[ [
literal( literal(
@ -780,8 +778,8 @@ export class UserModel extends Model<UserModel> {
const videoQuotaUsed = this.get('videoQuotaUsed') const videoQuotaUsed = this.get('videoQuotaUsed')
const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
const videosCount = this.get('videosCount') const videosCount = this.get('videosCount')
const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':') const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':')
const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount') const abusesCreatedCount = this.get('abusesCreatedCount')
const videoCommentsCount = this.get('videoCommentsCount') const videoCommentsCount = this.get('videoCommentsCount')
const json: User = { const json: User = {
@ -815,14 +813,14 @@ export class UserModel extends Model<UserModel> {
videosCount: videosCount !== undefined videosCount: videosCount !== undefined
? parseInt(videosCount + '', 10) ? parseInt(videosCount + '', 10)
: undefined, : undefined,
videoAbusesCount: videoAbusesCount abusesCount: abusesCount
? parseInt(videoAbusesCount, 10) ? parseInt(abusesCount, 10)
: undefined, : undefined,
videoAbusesAcceptedCount: videoAbusesAcceptedCount abusesAcceptedCount: abusesAcceptedCount
? parseInt(videoAbusesAcceptedCount, 10) ? parseInt(abusesAcceptedCount, 10)
: undefined, : undefined,
videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined abusesCreatedCount: abusesCreatedCount !== undefined
? parseInt(videoAbusesCreatedCount + '', 10) ? parseInt(abusesCreatedCount + '', 10)
: undefined, : undefined,
videoCommentsCount: videoCommentsCount !== undefined videoCommentsCount: videoCommentsCount !== undefined
? parseInt(videoCommentsCount + '', 10) ? parseInt(videoCommentsCount + '', 10)

View File

@ -61,6 +61,7 @@ type AvailableWithStatsOptions = {
} }
export type SummaryOptions = { export type SummaryOptions = {
actorRequired?: boolean // Default: true
withAccount?: boolean // Default: false withAccount?: boolean // Default: false
withAccountBlockerIds?: number[] withAccountBlockerIds?: number[]
} }
@ -121,7 +122,7 @@ export type SummaryOptions = {
{ {
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
model: ActorModel.unscoped(), model: ActorModel.unscoped(),
required: true, required: options.actorRequired ?? true,
include: [ include: [
{ {
attributes: [ 'host' ], attributes: [ 'host' ],

View File

@ -164,7 +164,7 @@ describe('Test user notifications API validators', function () {
const correctFields: UserNotificationSetting = { const correctFields: UserNotificationSetting = {
newVideoFromSubscription: UserNotificationSettingValue.WEB, newVideoFromSubscription: UserNotificationSettingValue.WEB,
newCommentOnMyVideo: UserNotificationSettingValue.WEB, newCommentOnMyVideo: UserNotificationSettingValue.WEB,
videoAbuseAsModerator: UserNotificationSettingValue.WEB, abuseAsModerator: UserNotificationSettingValue.WEB,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB, videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
blacklistOnMyVideo: UserNotificationSettingValue.WEB, blacklistOnMyVideo: UserNotificationSettingValue.WEB,
myVideoImportFinished: UserNotificationSettingValue.WEB, myVideoImportFinished: UserNotificationSettingValue.WEB,

View File

@ -2,6 +2,7 @@
set -eu 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) 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) activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo)

View File

@ -1,6 +1,7 @@
// Order of the tests we want to execute // Order of the tests we want to execute
import './activitypub' import './activitypub'
import './check-params' import './check-params'
import './moderation'
import './notifications' import './notifications'
import './redundancy' import './redundancy'
import './search' import './search'

View File

@ -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<Parameters<typeof getVideoAbusesList>[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)
})
})

View File

@ -0,0 +1,2 @@
export * from './abuses'
export * from './blocklist'

View File

@ -11,7 +11,7 @@ import {
MockInstancesIndex, MockInstancesIndex,
registerUser, registerUser,
removeVideoFromBlacklist, removeVideoFromBlacklist,
reportVideoAbuse, reportAbuse,
unfollow, unfollow,
updateCustomConfig, updateCustomConfig,
updateCustomSubConfig, updateCustomSubConfig,
@ -74,12 +74,12 @@ describe('Test moderation notifications', function () {
const name = 'video for abuse ' + uuidv4() const name = 'video for abuse ' + uuidv4()
const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) 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 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 () { 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 name = 'video for abuse ' + uuidv4()
const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
const uuid = resVideo.body.video.uuid const video = resVideo.body.video
await waitJobs(servers) 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 waitJobs(servers)
await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
}) })
}) })

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import * as chai from 'chai'
import 'mocha' import 'mocha'
import * as chai from 'chai'
import { import {
addVideoToBlacklist, addVideoToBlacklist,
askResetPassword, askResetPassword,
@ -11,7 +11,7 @@ import {
createUser, createUser,
flushAndRunServer, flushAndRunServer,
removeVideoFromBlacklist, removeVideoFromBlacklist,
reportVideoAbuse, reportAbuse,
resetPassword, resetPassword,
ServerInfo, ServerInfo,
setAccessTokensToServers, setAccessTokensToServers,
@ -30,10 +30,15 @@ describe('Test emails', function () {
let userId: number let userId: number
let userId2: number let userId2: number
let userAccessToken: string let userAccessToken: string
let videoUUID: string let videoUUID: string
let videoId: number
let videoUserUUID: string let videoUserUUID: string
let verificationString: string let verificationString: string
let verificationString2: string let verificationString2: string
const emails: object[] = [] const emails: object[] = []
const user = { const user = {
username: 'user_1', username: 'user_1',
@ -76,6 +81,7 @@ describe('Test emails', function () {
} }
const res = await uploadVideo(server.url, server.accessToken, attributes) const res = await uploadVideo(server.url, server.accessToken, attributes)
videoUUID = res.body.video.uuid videoUUID = res.body.video.uuid
videoId = res.body.video.id
} }
}) })
@ -179,7 +185,7 @@ describe('Test emails', function () {
this.timeout(10000) this.timeout(10000)
const reason = 'my super bad reason' 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) await waitJobs(server)
expect(emails).to.have.lengthOf(3) expect(emails).to.have.lengthOf(3)

View File

@ -1,5 +1,4 @@
import './users-verification'
import './blocklist'
import './user-subscriptions' import './user-subscriptions'
import './users' import './users'
import './users-multiple-servers' import './users-multiple-servers'
import './users-verification'

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import * as chai from 'chai'
import 'mocha' 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 { import {
addVideoCommentThread, addVideoCommentThread,
blockUser, blockUser,
@ -10,6 +11,7 @@ import {
createUser, createUser,
deleteMe, deleteMe,
flushAndRunServer, flushAndRunServer,
getAbusesList,
getAccountRatings, getAccountRatings,
getBlacklistedVideosList, getBlacklistedVideosList,
getCustomConfig, getCustomConfig,
@ -19,7 +21,6 @@ import {
getUserInformation, getUserInformation,
getUsersList, getUsersList,
getUsersListPaginationAndSort, getUsersListPaginationAndSort,
getVideoAbusesList,
getVideoChannel, getVideoChannel,
getVideosList, getVideosList,
installPlugin, installPlugin,
@ -29,15 +30,15 @@ import {
registerUserWithChannel, registerUserWithChannel,
removeUser, removeUser,
removeVideo, removeVideo,
reportVideoAbuse, reportAbuse,
ServerInfo, ServerInfo,
testImage, testImage,
unblockUser, unblockUser,
updateAbuse,
updateCustomSubConfig, updateCustomSubConfig,
updateMyAvatar, updateMyAvatar,
updateMyUser, updateMyUser,
updateUser, updateUser,
updateVideoAbuse,
uploadVideo, uploadVideo,
userLogin, userLogin,
waitJobs waitJobs
@ -46,7 +47,6 @@ import { follow } from '../../../../shared/extra-utils/server/follows'
import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
import { CustomConfig } from '@shared/models/server'
const expect = chai.expect const expect = chai.expect
@ -302,10 +302,10 @@ describe('Test users', function () {
expect(userGet.videosCount).to.equal(0) expect(userGet.videosCount).to.equal(0)
expect(userGet.videoCommentsCount).to.be.a('number') expect(userGet.videoCommentsCount).to.be.a('number')
expect(userGet.videoCommentsCount).to.equal(0) expect(userGet.videoCommentsCount).to.equal(0)
expect(userGet.videoAbusesCount).to.be.a('number') expect(userGet.abusesCount).to.be.a('number')
expect(userGet.videoAbusesCount).to.equal(0) expect(userGet.abusesCount).to.equal(0)
expect(userGet.videoAbusesAcceptedCount).to.be.a('number') expect(userGet.abusesAcceptedCount).to.be.a('number')
expect(userGet.videoAbusesAcceptedCount).to.equal(0) expect(userGet.abusesAcceptedCount).to.equal(0)
}) })
}) })
@ -895,9 +895,9 @@ describe('Test users', function () {
expect(user.videosCount).to.equal(0) expect(user.videosCount).to.equal(0)
expect(user.videoCommentsCount).to.equal(0) expect(user.videoCommentsCount).to.equal(0)
expect(user.videoAbusesCount).to.equal(0) expect(user.abusesCount).to.equal(0)
expect(user.videoAbusesCreatedCount).to.equal(0) expect(user.abusesCreatedCount).to.equal(0)
expect(user.videoAbusesAcceptedCount).to.equal(0) expect(user.abusesAcceptedCount).to.equal(0)
}) })
it('Should report correct videos count', async function () { it('Should report correct videos count', async function () {
@ -924,26 +924,26 @@ describe('Test users', function () {
expect(user.videoCommentsCount).to.equal(1) 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' 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 abuseId = res1.body.data[0].id
const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true) const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user2: User = res2.body const user2: User = res2.body
expect(user2.videoAbusesCount).to.equal(1) // number of incriminations expect(user2.abusesCount).to.equal(1) // number of incriminations
expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created expect(user2.abusesCreatedCount).to.equal(1) // number of reports created
const body: AbuseUpdate = { state: AbuseState.ACCEPTED } 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 res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user3: User = res3.body 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
}) })
}) })

View File

@ -103,8 +103,8 @@ describe('Test video abuses', function () {
expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port) expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
expect(abuse.video.id).to.equal(servers[0].video.id) expect(abuse.video.id).to.equal(servers[0].video.id)
expect(abuse.video.channel).to.exist expect(abuse.video.channel).to.exist
expect(abuse.count).to.equal(1) expect(abuse.video.countReports).to.equal(1)
expect(abuse.nth).to.equal(1) expect(abuse.video.nthReport).to.equal(1)
expect(abuse.countReportsForReporter).to.equal(1) expect(abuse.countReportsForReporter).to.equal(1)
expect(abuse.countReportsForReportee).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.id).to.equal(AbuseState.PENDING)
expect(abuse1.state.label).to.equal('Pending') expect(abuse1.state.label).to.equal('Pending')
expect(abuse1.moderationComment).to.be.null expect(abuse1.moderationComment).to.be.null
expect(abuse1.count).to.equal(1) expect(abuse1.video.countReports).to.equal(1)
expect(abuse1.nth).to.equal(1) expect(abuse1.video.nthReport).to.equal(1)
const abuse2: Abuse = res1.body.data[1] const abuse2: Abuse = res1.body.data[1]
expect(abuse2.reason).to.equal('my super bad reason 2') 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[]) { for (const abuse of res2.body.data as Abuse[]) {
if (abuse.video.id === video3.id) { if (abuse.video.id === video3.id) {
expect(abuse.count).to.equal(1, "wrong reports count for video 3") expect(abuse.video.countReports).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.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.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") expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
} }

View File

@ -98,5 +98,6 @@ export type MAbuseFull =
export type MAbuseFormattable = export type MAbuseFormattable =
MAbuse & MAbuse &
Use<'ReporterAccount', MAccountFormattable> & Use<'ReporterAccount', MAccountFormattable> &
Use<'FlaggedAccount', MAccountFormattable> &
Use<'VideoAbuse', MVideoAbuseFormattable> & Use<'VideoAbuse', MVideoAbuseFormattable> &
Use<'VideoCommentAbuse', MCommentAbuseFormattable> Use<'VideoCommentAbuse', MCommentAbuseFormattable>

View File

@ -516,7 +516,7 @@ function getAllNotificationsSettings () {
return { return {
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,

View File

@ -18,6 +18,9 @@ export interface VideoAbuse {
thumbnailPath?: string thumbnailPath?: string
channel?: VideoChannel channel?: VideoChannel
countReports: number
nthReport: number
} }
export interface VideoCommentAbuse { export interface VideoCommentAbuse {
@ -36,9 +39,12 @@ export interface VideoCommentAbuse {
export interface Abuse { export interface Abuse {
id: number id: number
reason: string reason: string
predefinedReasons?: AbusePredefinedReasonsString[] predefinedReasons?: AbusePredefinedReasonsString[]
reporterAccount: Account reporterAccount: Account
flaggedAccount: Account
state: VideoConstant<AbuseState> state: VideoConstant<AbuseState>
moderationComment?: string moderationComment?: string
@ -49,13 +55,18 @@ export interface Abuse {
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
// FIXME: deprecated in 2.3, remove this
startAt: null
endAt: null
count?: number
nth?: number
countReportsForReporter?: number countReportsForReporter?: number
countReportsForReportee?: number countReportsForReportee?: number
// FIXME: deprecated in 2.3, remove the following properties
// // @deprecated
// startAt: null
// // @deprecated
// endAt: null
// // @deprecated
// count?: number
// // @deprecated
// nth?: number
} }

View File

@ -7,7 +7,7 @@ export enum UserNotificationSettingValue {
export interface UserNotificationSetting { export interface UserNotificationSetting {
newVideoFromSubscription: UserNotificationSettingValue newVideoFromSubscription: UserNotificationSettingValue
newCommentOnMyVideo: UserNotificationSettingValue newCommentOnMyVideo: UserNotificationSettingValue
videoAbuseAsModerator: UserNotificationSettingValue abuseAsModerator: UserNotificationSettingValue
videoAutoBlacklistAsModerator: UserNotificationSettingValue videoAutoBlacklistAsModerator: UserNotificationSettingValue
blacklistOnMyVideo: UserNotificationSettingValue blacklistOnMyVideo: UserNotificationSettingValue
myVideoPublished: UserNotificationSettingValue myVideoPublished: UserNotificationSettingValue

View File

@ -31,10 +31,13 @@ export interface User {
videoQuotaDaily: number videoQuotaDaily: number
videoQuotaUsed?: number videoQuotaUsed?: number
videoQuotaUsedDaily?: number videoQuotaUsedDaily?: number
videosCount?: number videosCount?: number
videoAbusesCount?: number
videoAbusesAcceptedCount?: number abusesCount?: number
videoAbusesCreatedCount?: number abusesAcceptedCount?: number
abusesCreatedCount?: number
videoCommentsCount? : number videoCommentsCount? : number
theme: string theme: string

View File

@ -893,7 +893,7 @@ paths:
$ref: '#/components/schemas/NotificationSettingValue' $ref: '#/components/schemas/NotificationSettingValue'
newCommentOnMyVideo: newCommentOnMyVideo:
$ref: '#/components/schemas/NotificationSettingValue' $ref: '#/components/schemas/NotificationSettingValue'
videoAbuseAsModerator: abuseAsModerator:
$ref: '#/components/schemas/NotificationSettingValue' $ref: '#/components/schemas/NotificationSettingValue'
videoAutoBlacklistAsModerator: videoAutoBlacklistAsModerator:
$ref: '#/components/schemas/NotificationSettingValue' $ref: '#/components/schemas/NotificationSettingValue'
@ -1618,7 +1618,7 @@ paths:
type: object type: object
properties: properties:
state: state:
$ref: '#/components/schemas/VideoAbuseStateSet' $ref: '#/components/schemas/AbuseStateSet'
moderationComment: moderationComment:
type: string type: string
description: Update the report comment visible only to the moderation team description: Update the report comment visible only to the moderation team
@ -3584,20 +3584,20 @@ components:
label: label:
type: string type: string
VideoAbuseStateSet: AbuseStateSet:
type: integer type: integer
enum: enum:
- 1 - 1
- 2 - 2
- 3 - 3
description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)' description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)'
VideoAbuseStateConstant: AbuseStateConstant:
properties: properties:
id: id:
$ref: '#/components/schemas/VideoAbuseStateSet' $ref: '#/components/schemas/AbuseStateSet'
label: label:
type: string type: string
VideoAbusePredefinedReasons: AbusePredefinedReasons:
type: array type: array
items: items:
type: string type: string
@ -3960,11 +3960,11 @@ components:
type: string type: string
example: The video is a spam example: The video is a spam
predefinedReasons: predefinedReasons:
$ref: '#/components/schemas/VideoAbusePredefinedReasons' $ref: '#/components/schemas/AbusePredefinedReasons'
reporterAccount: reporterAccount:
$ref: '#/components/schemas/Account' $ref: '#/components/schemas/Account'
state: state:
$ref: '#/components/schemas/VideoAbuseStateConstant' $ref: '#/components/schemas/AbuseStateConstant'
moderationComment: moderationComment:
type: string type: string
example: Decided to ban the server since it spams us regularly example: Decided to ban the server since it spams us regularly
@ -4690,11 +4690,11 @@ components:
description: The user daily video quota description: The user daily video quota
videosCount: videosCount:
type: integer type: integer
videoAbusesCount: abusesCount:
type: integer type: integer
videoAbusesAcceptedCount: abusesAcceptedCount:
type: integer type: integer
videoAbusesCreatedCount: abusesCreatedCount:
type: integer type: integer
videoCommentsCount: videoCommentsCount:
type: integer type: integer