X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fabuse%2Fabuse.ts;h=dffd503b391ee648decdf1dc8d490160e653313a;hb=310b5219b38427f0c2c7ba57225afdd8f3064380;hp=4f99f9c9b836e7a6955fa0f2229995f4fc8096b6;hpb=d95d15598847c7f020aa056e7e6e0c02d2bbf732;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 4f99f9c9b..dffd503b3 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts @@ -1,6 +1,6 @@ import * as Bluebird from 'bluebird' import { invert } from 'lodash' -import { literal, Op, WhereOptions } from 'sequelize' +import { literal, Op, QueryTypes, WhereOptions } from 'sequelize' import { AllowNull, BelongsTo, @@ -19,23 +19,26 @@ import { import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' import { Abuse, + AbuseFilter, AbuseObject, AbusePredefinedReasons, abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseState, AbuseVideoIs, - VideoAbuse + VideoAbuse, + VideoCommentAbuse } from '@shared/models' -import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter' -import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants' +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 { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' +import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' +import { getSort, 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 { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' import { VideoAbuseModel } from './video-abuse' import { VideoCommentAbuseModel } from './video-comment-abuse' @@ -44,91 +47,7 @@ export enum ScopeNames { } @Scopes(() => ({ - [ScopeNames.FOR_API]: (options: { - // search - search?: string - searchReporter?: string - searchReportee?: string - - // video releated - searchVideo?: string - searchVideoChannel?: string - videoIs?: AbuseVideoIs - - // filters - id?: number - predefinedReasonId?: number - filter?: AbuseFilter - - state?: AbuseState - - // accountIds - serverAccountId: number - userAccountId: number - }) => { - const onlyBlacklisted = options.videoIs === 'blacklisted' - const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel) - - const where = { - reporterAccountId: { - [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') - } - } - - if (options.search) { - const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%') - - Object.assign(where, { - [Op.or]: [ - { - [Op.and]: [ - { '$VideoAbuse.videoId$': { [Op.not]: null } }, - searchAttribute(options.search, '$VideoAbuse.Video.name$') - ] - }, - { - [Op.and]: [ - { '$VideoAbuse.videoId$': { [Op.not]: null } }, - searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$') - ] - }, - { - [Op.and]: [ - { '$VideoAbuse.deletedVideo$': { [Op.not]: null } }, - literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`) - ] - }, - { - [Op.and]: [ - { '$VideoAbuse.deletedVideo$': { [Op.not]: null } }, - literal(`"VideoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE ${escapedSearch}`) - ] - }, - searchAttribute(options.search, '$ReporterAccount.name$'), - searchAttribute(options.search, '$FlaggedAccount.name$') - ] - }) - } - - if (options.id) Object.assign(where, { id: options.id }) - if (options.state) Object.assign(where, { state: options.state }) - - if (options.videoIs === 'deleted') { - Object.assign(where, { - '$VideoAbuse.deletedVideo$': { - [Op.not]: null - } - }) - } - - if (options.predefinedReasonId) { - Object.assign(where, { - predefinedReasons: { - [Op.contains]: [ options.predefinedReasonId ] - } - }) - } - + [ScopeNames.FOR_API]: () => { return { attributes: { include: [ @@ -138,7 +57,7 @@ export enum ScopeNames { '(' + 'SELECT count(*) ' + 'FROM "videoAbuse" ' + - 'WHERE "videoId" = "VideoAbuse"."videoId" ' + + 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' + ')' ), 'countReportsForVideo' @@ -153,7 +72,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' @@ -161,102 +80,88 @@ 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" ' + - ')' - ), - 'countReportsForReporter__video' - ], - [ - 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"."reporterAccountId" = "AbuseModel"."reporterAccountId"' + ')' ), - 'countReportsForReporter__deletedVideo' + 'countReportsForReporter' ], [ 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) ` + + 'SELECT count("abuse"."id") ' + + 'FROM "abuse" ' + + 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' + ')' ), - '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' ] ] }, include: [ { - model: AccountModel.scope(AccountScopeNames.SUMMARY), - as: 'ReporterAccount', - required: true, - where: searchAttribute(options.searchReporter, 'name') + model: AccountModel.scope({ + method: [ + AccountScopeNames.SUMMARY, + { actorRequired: false } as AccountSummaryOptions + ] + }), + as: 'ReporterAccount' }, { - model: AccountModel.scope(AccountScopeNames.SUMMARY), - as: 'FlaggedAccount', - required: true, - where: searchAttribute(options.searchReportee, 'name') + model: AccountModel.scope({ + method: [ + AccountScopeNames.SUMMARY, + { actorRequired: false } as AccountSummaryOptions + ] + }), + as: 'FlaggedAccount' }, { - model: VideoAbuseModel, - required: options.filter === 'video' || !!options.videoIs || videoRequired, + model: VideoCommentAbuseModel.unscoped(), include: [ { - model: VideoModel, - required: videoRequired, - where: searchAttribute(options.searchVideo, 'name'), + model: VideoCommentModel.unscoped(), include: [ { + model: VideoModel.unscoped(), + attributes: [ 'name', 'id', 'uuid' ] + } + ] + } + ] + }, + { + model: VideoAbuseModel.unscoped(), + include: [ + { + attributes: [ 'id', 'uuid', 'name', 'nsfw' ], + model: VideoModel.unscoped(), + include: [ + { + attributes: [ 'filename', 'fileUrl' ], model: ThumbnailModel }, { - model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: false } as SummaryOptions ] }), - where: searchAttribute(options.searchVideoChannel, 'name'), - required: true, - include: [ - { - model: AccountModel.scope(AccountScopeNames.SUMMARY), - required: true, - where: searchAttribute(options.searchReportee, 'name') - } - ] + model: VideoChannelModel.scope({ + method: [ + VideoChannelScopeNames.SUMMARY, + { withAccount: false, actorRequired: false } as ChannelSummaryOptions + ] + }), + required: false }, { attributes: [ 'id', 'reason', 'unfederated' ], - model: VideoBlacklistModel, - required: onlyBlacklisted + required: false, + model: VideoBlacklistModel } ] } ] } - ], - where + ] } } })) @@ -275,19 +180,19 @@ export class AbuseModel extends Model { @AllowNull(false) @Default(null) - @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) + @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max)) reason: string @AllowNull(false) @Default(null) - @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) + @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) @Column state: AbuseState @AllowNull(true) @Default(null) - @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true)) + @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max)) moderationComment: string @@ -348,6 +253,7 @@ export class AbuseModel extends Model { }) VideoAbuse: VideoAbuseModel + // FIXME: deprecated in 2.3. Remove these validators static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird { const videoWhere: WhereOptions = {} @@ -369,7 +275,17 @@ export class AbuseModel extends Model { return AbuseModel.findOne(query) } - static listForApi (parameters: { + static loadById (id: number): Bluebird { + const query = { + where: { + id + } + } + + return AbuseModel.findOne(query) + } + + static async listForApi (parameters: { start: number count: number sort: string @@ -411,15 +327,10 @@ export class AbuseModel extends Model { const userAccountId = user ? user.Account.id : undefined const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined - const query = { - offset: start, - limit: count, - order: getSort(sort), - col: 'AbuseModel.id', - distinct: true - } - - const filters = { + const queryOptions: BuildAbusesQueryOptions = { + start, + count, + sort, id, filter, predefinedReasonId, @@ -434,26 +345,25 @@ export class AbuseModel extends Model { userAccountId } - return AbuseModel - .scope([ - { method: [ ScopeNames.FOR_API, filters ] } - ]) - .findAndCountAll(query) - .then(({ rows, count }) => { - return { total: count, data: rows } - }) + const [ total, data ] = await Promise.all([ + AbuseModel.internalCountForApi(queryOptions), + AbuseModel.internalListForApi(queryOptions) + ]) + + return { total, data } } 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 - let video: VideoAbuse + const countReportsForReporter = this.get('countReportsForReporter') as number + const countReportsForReportee = this.get('countReportsForReportee') as number + + let video: VideoAbuse = null + let comment: VideoCommentAbuse = null if (this.VideoAbuse) { const abuseModel = this.VideoAbuse @@ -471,7 +381,29 @@ export class AbuseModel extends Model { deleted: !abuseModel.Video, blacklisted: abuseModel.Video?.isBlacklisted() || false, thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), - channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel + + channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel, + + countReports: countReportsForVideo, + nthReport: nthReportForVideo + } + } + + if (this.VideoCommentAbuse) { + const abuseModel = this.VideoCommentAbuse + const entity = abuseModel.VideoComment + + comment = { + id: entity.id, + text: entity.text ?? '', + + deleted: entity.isDeleted(), + + video: { + id: entity.Video.id, + name: entity.Video.name, + uuid: entity.Video.uuid + } } } @@ -480,7 +412,13 @@ export class AbuseModel extends Model { reason: this.reason, predefinedReasons, - reporterAccount: this.ReporterAccount.toFormattedJSON(), + reporterAccount: this.ReporterAccount + ? this.ReporterAccount.toFormattedJSON() + : null, + + flaggedAccount: this.FlaggedAccount + ? this.FlaggedAccount.toFormattedJSON() + : null, state: { id: this.state, @@ -490,18 +428,19 @@ export class AbuseModel extends Model { moderationComment: this.moderationComment, video, - comment: null, + comment, 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 } } @@ -526,6 +465,42 @@ export class AbuseModel extends Model { } } + private static async internalCountForApi (parameters: BuildAbusesQueryOptions) { + const { query, replacements } = buildAbuseListQuery(parameters, 'count') + const options = { + type: QueryTypes.SELECT as QueryTypes.SELECT, + replacements + } + + const [ { total } ] = await AbuseModel.sequelize.query<{ total: string }>(query, options) + if (total === null) return 0 + + return parseInt(total, 10) + } + + private static async internalListForApi (parameters: BuildAbusesQueryOptions) { + const { query, replacements } = buildAbuseListQuery(parameters, 'id') + const options = { + type: QueryTypes.SELECT as QueryTypes.SELECT, + replacements + } + + const rows = await AbuseModel.sequelize.query<{ id: string }>(query, options) + const ids = rows.map(r => r.id) + + if (ids.length === 0) return [] + + return AbuseModel.scope(ScopeNames.FOR_API) + .findAll({ + order: getSort(parameters.sort), + where: { + id: { + [Op.in]: ids + } + } + }) + } + private static getStateLabel (id: number) { return ABUSE_STATES[id] || 'Unknown' }