X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo-abuse.ts;h=1319332f0738fa80b645d67f42d71c8fd33b20cc;hb=1dee8d68cb89ea98610ab61291e640991d3506e6;hp=5ead02ecae5d46b446b1a18d2be99e2848ddac7e;hpb=844db39ee56ff0dd59a96acfc68f10f9ac53000b;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index 5ead02eca..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,18 +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, Video } 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 { ActorModel } from '../activitypub/actor' +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' @@ -27,86 +45,191 @@ 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$') ] }) } - console.log(where) + 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 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 ' + + 'FROM ( ' + + 'SELECT id, ' + + 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + + 'FROM "videoAbuse" ' + + ') t ' + + 'WHERE t.id = "VideoAbuseModel".id ' + + ')' + ), + 'nthReportForVideo' + ], + [ + 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" = "VideoAbuseModel"."reporterAccountId" ' + + ')' + ), + 'countReportsForReporter__video' + ], + [ + literal( + '(' + + '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" = "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__deletedVideo' + ] + ] + }, include: [ { 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.unscoped(), - where: { ...search(options.searchVideoChannel, 'name') }, + model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), + where: searchAttribute(options.searchVideoChannel, 'name'), include: [ { - model: ActorModel + model: AccountModel, + where: searchAttribute(options.searchReportee, 'name') } ] }, { attributes: [ 'id', 'reason', 'unfederated' ], - model: VideoBlacklistModel + model: VideoBlacklistModel, + required: onlyBlacklisted } ] } @@ -149,7 +272,22 @@ export class VideoAbuseModel extends Model { @AllowNull(true) @Default(null) @Column(DataType.JSONB) - deletedVideo: Video + 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 @@ -199,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, @@ -215,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 } @@ -229,6 +405,14 @@ 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 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 : this.deletedVideo @@ -236,6 +420,7 @@ export class VideoAbuseModel extends Model { return { id: this.id, reason: this.reason, + predefinedReasons, reporterAccount: this.Account.toFormattedJSON(), state: { id: this.state, @@ -248,23 +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.toFormattedSummaryJSON() || this.deletedVideo?.channel + channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel }, - createdAt: this.createdAt + createdAt: this.createdAt, + updatedAt: this.updatedAt, + startAt: this.startAt, + endAt: this.endAt, + count: countReportsForVideo || 0, + nth: nthReportForVideo || 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) + } }