X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo-abuse.ts;h=1319332f0738fa80b645d67f42d71c8fd33b20cc;hb=e234debc4d62e1f58b34f8af5a6139074fb7724d;hp=d68608ca668cc33a757c1d49b0a1b42b43db8803;hpb=5fd4ca0051c7e7f3f8c47bcbde5cab0c56532e64;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index d68608ca6..1319332f0 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -1,6 +1,27 @@ +import * as Bluebird from 'bluebird' +import { literal, Op } from 'sequelize' import { - AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, Scopes + AllowNull, + BelongsTo, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + Is, + Model, + Scopes, + Table, + UpdatedAt } from 'sequelize-typescript' +import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' +import { + VideoAbuseState, + VideoDetails, + VideoAbusePredefinedReasons, + VideoAbusePredefinedReasonsString, + videoAbusePredefinedReasonsMap +} from '../../../shared' import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' import { VideoAbuse } from '../../../shared/models/videos' import { @@ -8,17 +29,15 @@ import { isVideoAbuseReasonValid, isVideoAbuseStateValid } from '../../helpers/custom-validators/video-abuses' -import { AccountModel } from '../account/account' -import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils' -import { VideoModel } from './video' -import { VideoAbuseState, VideoDetails } from '../../../shared' import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' -import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models' -import * as Bluebird from 'bluebird' -import { literal, Op } from 'sequelize' +import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models' +import { AccountModel } from '../account/account' +import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' import { ThumbnailModel } from './thumbnail' -import { VideoChannelModel } from './video-channel' +import { VideoModel } from './video' import { VideoBlacklistModel } from './video-blacklist' +import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' +import { invert } from 'lodash' export enum ScopeNames { FOR_API = 'FOR_API' @@ -26,75 +45,99 @@ export enum ScopeNames { @Scopes(() => ({ [ScopeNames.FOR_API]: (options: { + // search search?: string searchReporter?: string + searchReportee?: string searchVideo?: string searchVideoChannel?: string + + // filters + id?: number + predefinedReasonId?: number + + state?: VideoAbuseState + videoIs?: VideoAbuseVideoIs + + // accountIds serverAccountId: number - userAccountId: any + userAccountId: number }) => { - const search = (sourceField, targetField) => sourceField ? ({ - [targetField]: { - [Op.iLike]: `%${sourceField}%` - } - }) : {} - - let where = { + const where = { reporterAccountId: { - [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')') + [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') } } if (options.search) { - where = Object.assign(where, { + Object.assign(where, { [Op.or]: [ { [Op.and]: [ { videoId: { [Op.not]: null } }, - { '$Video.name$': { [Op.iLike]: `%${options.search}%` } } + searchAttribute(options.search, '$Video.name$') ] }, { [Op.and]: [ { videoId: { [Op.not]: null } }, - { '$Video.VideoChannel.name$': { [Op.iLike]: `%${options.search}%` } } + searchAttribute(options.search, '$Video.VideoChannel.name$') ] }, { [Op.and]: [ { deletedVideo: { [Op.not]: null } }, - { deletedVideo: { name: { [Op.iLike]: `%${options.search}%` } } } + { deletedVideo: searchAttribute(options.search, 'name') } ] }, { [Op.and]: [ { deletedVideo: { [Op.not]: null } }, - { deletedVideo: { channel: { displayName: { [Op.iLike]: `%${options.search}%` } } } } + { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } } ] }, - { '$Account.name$': { [Op.iLike]: `%${options.search}%` } } + searchAttribute(options.search, '$Account.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, { + deletedVideo: { + [Op.not]: null + } + }) + } + + if (options.predefinedReasonId) { + Object.assign(where, { + predefinedReasons: { + [Op.contains]: [ options.predefinedReasonId ] + } + }) + } + + const onlyBlacklisted = options.videoIs === 'blacklisted' + return { attributes: { include: [ [ + // we don't care about this count for deleted videos, so there are not included literal( '(' + - 'SELECT t.count ' + - 'FROM ( ' + - 'SELECT id, ' + - 'count(id) OVER (PARTITION BY "videoId") ' + - 'FROM "videoAbuse" ' + - ') t ' + - 'WHERE t.id = "VideoAbuseModel".id ' + + 'SELECT count(*) ' + + 'FROM "videoAbuse" ' + + 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' + ')' ), 'countReportsForVideo' ], [ + // we don't care about this count for deleted videos, so there are not included literal( '(' + 'SELECT t.nth ' + @@ -119,28 +162,43 @@ export enum ScopeNames { 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' + ')' ), - 'countReportsForReporter' + 'countReportsForReporter__video' ], [ literal( '(' + - 'WITH ' + - 'ids AS ( ' + - 'SELECT "account"."id" ' + - 'FROM "account" ' + - 'INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ' + - 'INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" ' + - 'WHERE "video"."id" = "VideoAbuseModel"."videoId" ' + - ') ' + - 'SELECT count("videoAbuse"."id") ' + + 'SELECT count(DISTINCT "videoAbuse"."id") ' + + 'FROM "videoAbuse" ' + + `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` + + ')' + ), + '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" = "account"."id" ' + - 'INNER JOIN ids ON "account"."id" = ids.id ' + + 'INNER JOIN "account" ON ' + + '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' + + `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + + ')' + ), + 'countReportsForReportee__video' + ], + [ + literal( + '(' + + 'SELECT count(DISTINCT "videoAbuse"."id") ' + + 'FROM "videoAbuse" ' + + `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` + + `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` + + `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + ')' ), - 'countReportsForReportee' + 'countReportsForReportee__deletedVideo' ] ] }, @@ -148,23 +206,30 @@ export enum ScopeNames { { model: AccountModel, required: true, - where: { ...search(options.searchReporter, 'name') } + where: searchAttribute(options.searchReporter, 'name') }, { model: VideoModel, - required: false, - where: { ...search(options.searchVideo, 'name') }, + required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel), + where: searchAttribute(options.searchVideo, 'name'), include: [ { model: ThumbnailModel }, { - model: VideoChannelModel.scope([ 'WITH_ACTOR', 'WITH_ACCOUNT' ]), - where: { ...search(options.searchVideoChannel, 'name') } + model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), + where: searchAttribute(options.searchVideoChannel, 'name'), + include: [ + { + model: AccountModel, + where: searchAttribute(options.searchReportee, 'name') + } + ] }, { attributes: [ 'id', 'reason', 'unfederated' ], - model: VideoBlacklistModel + model: VideoBlacklistModel, + required: onlyBlacklisted } ] } @@ -209,6 +274,21 @@ export class VideoAbuseModel extends Model { @Column(DataType.JSONB) deletedVideo: VideoDetails + @AllowNull(true) + @Default(null) + @Column(DataType.ARRAY(DataType.INTEGER)) + predefinedReasons: VideoAbusePredefinedReasons[] + + @AllowNull(true) + @Default(null) + @Column + startAt: number + + @AllowNull(true) + @Default(null) + @Column + endAt: number + @CreatedAt createdAt: Date @@ -257,12 +337,40 @@ export class VideoAbuseModel extends Model { start: number count: number sort: string - search?: string + serverAccountId: number user?: MUserAccountId + + id?: number + predefinedReason?: VideoAbusePredefinedReasonsString + state?: VideoAbuseState + videoIs?: VideoAbuseVideoIs + + search?: string + searchReporter?: string + searchReportee?: string + searchVideo?: string + searchVideoChannel?: string }) { - const { start, count, sort, search, user, serverAccountId } = parameters + const { + start, + count, + sort, + search, + user, + serverAccountId, + state, + videoIs, + predefinedReason, + searchReportee, + searchVideo, + searchVideoChannel, + searchReporter, + id + } = parameters + const userAccountId = user ? user.Account.id : undefined + const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined const query = { offset: start, @@ -273,13 +381,23 @@ export class VideoAbuseModel extends Model { } const filters = { + id, + predefinedReasonId, search, + state, + videoIs, + searchReportee, + searchVideo, + searchVideoChannel, + searchReporter, serverAccountId, userAccountId } return VideoAbuseModel - .scope({ method: [ ScopeNames.FOR_API, filters ] }) + .scope([ + { method: [ ScopeNames.FOR_API, filters ] } + ]) .findAndCountAll(query) .then(({ rows, count }) => { return { total: count, data: rows } @@ -287,10 +405,13 @@ export class VideoAbuseModel extends Model { } toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { + const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) const countReportsForVideo = this.get('countReportsForVideo') as number const nthReportForVideo = this.get('nthReportForVideo') as number - const countReportsForReporter = this.get('countReportsForReporter') as number - const countReportsForReportee = this.get('countReportsForReportee') 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 video = this.Video ? this.Video @@ -299,6 +420,7 @@ export class VideoAbuseModel extends Model { return { id: this.id, reason: this.reason, + predefinedReasons, reporterAccount: this.Account.toFormattedJSON(), state: { id: this.state, @@ -311,28 +433,47 @@ export class VideoAbuseModel extends Model { name: video.name, nsfw: video.nsfw, deleted: !this.Video, - blacklisted: this.Video && this.Video.isBlacklisted(), + blacklisted: this.Video?.isBlacklisted() || false, thumbnailPath: this.Video?.getMiniatureStaticPath(), channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel }, createdAt: this.createdAt, updatedAt: this.updatedAt, + startAt: this.startAt, + endAt: this.endAt, count: countReportsForVideo || 0, nth: nthReportForVideo || 0, - countReportsForReporter: countReportsForReporter || 0, - countReportsForReportee: countReportsForReportee || 0 + countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), + countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0) } } toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject { + const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) + + const startAt = this.startAt + const endAt = this.endAt + return { type: 'Flag' as 'Flag', content: this.reason, - object: this.Video.url + object: this.Video.url, + tag: predefinedReasons.map(r => ({ + type: 'Hashtag' as 'Hashtag', + name: r + })), + startAt, + endAt } } private static getStateLabel (id: number) { return VIDEO_ABUSE_STATES[id] || 'Unknown' } + + private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] { + return (predefinedReasons || []) + .filter(r => r in VideoAbusePredefinedReasons) + .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString) + } }