Add migrations

pull/2962/head
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: []
}
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)
}

View File

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

View File

@ -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,

View File

@ -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

View File

@ -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,

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()
emailPayload = {
template: 'comment-abuse-new',
template: 'video-comment-abuse-new',
to,
subject: `New comment abuse report from ${reporter}`,
locals: {

View File

@ -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) {

View File

@ -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,

View File

@ -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')

View File

@ -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<AbuseModel> {
@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<AbuseModel> {
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<AbuseModel> {
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<AbuseModel> {
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<AbuseModel> {
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
}
}

View File

@ -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,

View File

@ -51,11 +51,11 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
@AllowNull(false)
@Default(null)
@Is(
'UserNotificationSettingVideoAbuseAsModerator',
value => 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<UserNotificationSettingM
return {
newCommentOnMyVideo: this.newCommentOnMyVideo,
newVideoFromSubscription: this.newVideoFromSubscription,
videoAbuseAsModerator: this.videoAbuseAsModerator,
abuseAsModerator: this.abuseAsModerator,
videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
blacklistOnMyVideo: this.blacklistOnMyVideo,
myVideoPublished: this.myVideoPublished,

View File

@ -168,28 +168,26 @@ enum ScopeNames {
'(' +
`SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
'FROM (' +
'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
`COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
'FROM "videoAbuse" ' +
'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
'SELECT COUNT("abuse"."id") AS "abuses", ' +
`COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
'FROM "abuse" ' +
'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' +
'WHERE "account"."userId" = "UserModel"."id"' +
') t' +
')'
),
'videoAbusesCount'
'abusesCount'
],
[
literal(
'(' +
'SELECT COUNT("videoAbuse"."id") ' +
'FROM "videoAbuse" ' +
'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' +
'SELECT COUNT("abuse"."id") ' +
'FROM "abuse" ' +
'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' +
'WHERE "account"."userId" = "UserModel"."id"' +
')'
),
'videoAbusesCreatedCount'
'abusesCreatedCount'
],
[
literal(
@ -780,8 +778,8 @@ export class UserModel extends Model<UserModel> {
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<UserModel> {
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)

View File

@ -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' ],

View File

@ -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,

View File

@ -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)

View File

@ -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'

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,
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')
})
})

View File

@ -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)

View File

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

View File

@ -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
})
})

View File

@ -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")
}

View File

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

View File

@ -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,

View File

@ -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<AbuseState>
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
}

View File

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

View File

@ -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

View File

@ -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