123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- import { invert } from 'lodash'
- import { literal, Op, QueryTypes } from 'sequelize'
- import {
- AllowNull,
- BelongsTo,
- Column,
- CreatedAt,
- DataType,
- Default,
- ForeignKey,
- HasOne,
- Is,
- Model,
- Scopes,
- Table,
- UpdatedAt
- } from 'sequelize-typescript'
- import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
- import { abusePredefinedReasonsMap } from '@shared/core-utils'
- import {
- AbuseFilter,
- AbuseObject,
- AbusePredefinedReasons,
- AbusePredefinedReasonsString,
- AbuseState,
- AbuseVideoIs,
- AdminAbuse,
- AdminVideoAbuse,
- AdminVideoCommentAbuse,
- UserAbuse,
- UserVideoAbuse
- } from '@shared/models'
- import { AttributesOnly } from '@shared/typescript-utils'
- import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
- import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models'
- import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
- import { getSort, throwIfNotValid } from '../utils'
- import { ThumbnailModel } from '../video/thumbnail'
- import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video'
- import { VideoBlacklistModel } from '../video/video-blacklist'
- import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel'
- import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment'
- import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder'
- import { VideoAbuseModel } from './video-abuse'
- import { VideoCommentAbuseModel } from './video-comment-abuse'
- export enum ScopeNames {
- FOR_API = 'FOR_API'
- }
- @Scopes(() => ({
- [ScopeNames.FOR_API]: () => {
- return {
- attributes: {
- include: [
- [
- literal(
- '(' +
- 'SELECT count(*) ' +
- 'FROM "abuseMessage" ' +
- 'WHERE "abuseId" = "AbuseModel"."id"' +
- ')'
- ),
- 'countMessages'
- ],
- [
- // we don't care about this count for deleted videos, so there are not included
- literal(
- '(' +
- 'SELECT count(*) ' +
- 'FROM "videoAbuse" ' +
- 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' +
- ')'
- ),
- '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 = "VideoAbuse".id AND t.id IS NOT NULL' +
- ')'
- ),
- 'nthReportForVideo'
- ],
- [
- literal(
- '(' +
- 'SELECT count("abuse"."id") ' +
- 'FROM "abuse" ' +
- 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' +
- ')'
- ),
- 'countReportsForReporter'
- ],
- [
- literal(
- '(' +
- 'SELECT count("abuse"."id") ' +
- 'FROM "abuse" ' +
- 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' +
- ')'
- ),
- 'countReportsForReportee'
- ]
- ]
- },
- include: [
- {
- model: AccountModel.scope({
- method: [
- AccountScopeNames.SUMMARY,
- { actorRequired: false } as AccountSummaryOptions
- ]
- }),
- as: 'ReporterAccount'
- },
- {
- model: AccountModel.scope({
- method: [
- AccountScopeNames.SUMMARY,
- { actorRequired: false } as AccountSummaryOptions
- ]
- }),
- as: 'FlaggedAccount'
- },
- {
- model: VideoCommentAbuseModel.unscoped(),
- include: [
- {
- 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', 'type' ],
- model: ThumbnailModel
- },
- {
- model: VideoChannelModel.scope({
- method: [
- VideoChannelScopeNames.SUMMARY,
- { withAccount: false, actorRequired: false } as ChannelSummaryOptions
- ]
- }),
- required: false
- },
- {
- attributes: [ 'id', 'reason', 'unfederated' ],
- required: false,
- model: VideoBlacklistModel
- }
- ]
- }
- ]
- }
- ]
- }
- }
- }))
- @Table({
- tableName: 'abuse',
- indexes: [
- {
- fields: [ 'reporterAccountId' ]
- },
- {
- fields: [ 'flaggedAccountId' ]
- }
- ]
- })
- export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> {
- @AllowNull(false)
- @Default(null)
- @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
- reason: string
- @AllowNull(false)
- @Default(null)
- @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
- @Column
- state: AbuseState
- @AllowNull(true)
- @Default(null)
- @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
- moderationComment: string
- @AllowNull(true)
- @Default(null)
- @Column(DataType.ARRAY(DataType.INTEGER))
- predefinedReasons: AbusePredefinedReasons[]
- @CreatedAt
- createdAt: Date
- @UpdatedAt
- updatedAt: Date
- @ForeignKey(() => AccountModel)
- @Column
- reporterAccountId: number
- @BelongsTo(() => AccountModel, {
- foreignKey: {
- name: 'reporterAccountId',
- allowNull: true
- },
- as: 'ReporterAccount',
- onDelete: 'set null'
- })
- ReporterAccount: AccountModel
- @ForeignKey(() => AccountModel)
- @Column
- flaggedAccountId: number
- @BelongsTo(() => AccountModel, {
- foreignKey: {
- name: 'flaggedAccountId',
- allowNull: true
- },
- as: 'FlaggedAccount',
- onDelete: 'set null'
- })
- FlaggedAccount: AccountModel
- @HasOne(() => VideoCommentAbuseModel, {
- foreignKey: {
- name: 'abuseId',
- allowNull: false
- },
- onDelete: 'cascade'
- })
- VideoCommentAbuse: VideoCommentAbuseModel
- @HasOne(() => VideoAbuseModel, {
- foreignKey: {
- name: 'abuseId',
- allowNull: false
- },
- onDelete: 'cascade'
- })
- VideoAbuse: VideoAbuseModel
- static loadByIdWithReporter (id: number): Promise<MAbuseReporter> {
- const query = {
- where: {
- id
- },
- include: [
- {
- model: AccountModel,
- as: 'ReporterAccount'
- }
- ]
- }
- return AbuseModel.findOne(query)
- }
- static loadFull (id: number): Promise<MAbuseFull> {
- const query = {
- where: {
- id
- },
- include: [
- {
- model: AccountModel.scope(AccountScopeNames.SUMMARY),
- required: false,
- as: 'ReporterAccount'
- },
- {
- model: AccountModel.scope(AccountScopeNames.SUMMARY),
- as: 'FlaggedAccount'
- },
- {
- model: VideoAbuseModel,
- required: false,
- include: [
- {
- model: VideoModel.scope([ VideoScopeNames.WITH_ACCOUNT_DETAILS ])
- }
- ]
- },
- {
- model: VideoCommentAbuseModel,
- required: false,
- include: [
- {
- model: VideoCommentModel.scope([
- CommentScopeNames.WITH_ACCOUNT
- ]),
- include: [
- {
- model: VideoModel
- }
- ]
- }
- ]
- }
- ]
- }
- return AbuseModel.findOne(query)
- }
- static async listForAdminApi (parameters: {
- start: number
- count: number
- sort: string
- filter?: AbuseFilter
- serverAccountId: number
- user?: MUserAccountId
- id?: number
- predefinedReason?: AbusePredefinedReasonsString
- state?: AbuseState
- videoIs?: AbuseVideoIs
- search?: string
- searchReporter?: string
- searchReportee?: string
- searchVideo?: string
- searchVideoChannel?: string
- }) {
- const {
- start,
- count,
- sort,
- search,
- user,
- serverAccountId,
- state,
- videoIs,
- predefinedReason,
- searchReportee,
- searchVideo,
- filter,
- searchVideoChannel,
- searchReporter,
- id
- } = parameters
- const userAccountId = user ? user.Account.id : undefined
- const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
- const queryOptions: BuildAbusesQueryOptions = {
- start,
- count,
- sort,
- id,
- filter,
- predefinedReasonId,
- search,
- state,
- videoIs,
- searchReportee,
- searchVideo,
- searchVideoChannel,
- searchReporter,
- serverAccountId,
- userAccountId
- }
- const [ total, data ] = await Promise.all([
- AbuseModel.internalCountForApi(queryOptions),
- AbuseModel.internalListForApi(queryOptions)
- ])
- return { total, data }
- }
- static async listForUserApi (parameters: {
- user: MUserAccountId
- start: number
- count: number
- sort: string
- id?: number
- search?: string
- state?: AbuseState
- }) {
- const {
- start,
- count,
- sort,
- search,
- user,
- state,
- id
- } = parameters
- const queryOptions: BuildAbusesQueryOptions = {
- start,
- count,
- sort,
- id,
- search,
- state,
- reporterAccountId: user.Account.id
- }
- const [ total, data ] = await Promise.all([
- AbuseModel.internalCountForApi(queryOptions),
- AbuseModel.internalListForApi(queryOptions)
- ])
- return { total, data }
- }
- buildBaseVideoCommentAbuse (this: MAbuseUserFormattable) {
- // Associated video comment could have been destroyed if the video has been deleted
- if (!this.VideoCommentAbuse || !this.VideoCommentAbuse.VideoComment) return null
- const entity = this.VideoCommentAbuse.VideoComment
- return {
- id: entity.id,
- threadId: entity.getThreadId(),
- text: entity.text ?? '',
- deleted: entity.isDeleted(),
- video: {
- id: entity.Video.id,
- name: entity.Video.name,
- uuid: entity.Video.uuid
- }
- }
- }
- buildBaseVideoAbuse (this: MAbuseUserFormattable): UserVideoAbuse {
- if (!this.VideoAbuse) return null
- const abuseModel = this.VideoAbuse
- const entity = abuseModel.Video || abuseModel.deletedVideo
- return {
- id: entity.id,
- uuid: entity.uuid,
- name: entity.name,
- nsfw: entity.nsfw,
- startAt: abuseModel.startAt,
- endAt: abuseModel.endAt,
- deleted: !abuseModel.Video,
- blacklisted: abuseModel.Video?.isBlacklisted() || false,
- thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
- channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel
- }
- }
- buildBaseAbuse (this: MAbuseUserFormattable, countMessages: number): UserAbuse {
- const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
- return {
- id: this.id,
- reason: this.reason,
- predefinedReasons,
- flaggedAccount: this.FlaggedAccount
- ? this.FlaggedAccount.toFormattedJSON()
- : null,
- state: {
- id: this.state,
- label: AbuseModel.getStateLabel(this.state)
- },
- countMessages,
- createdAt: this.createdAt,
- updatedAt: this.updatedAt
- }
- }
- toFormattedAdminJSON (this: MAbuseAdminFormattable): AdminAbuse {
- 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 countMessages = this.get('countMessages') as number
- const baseVideo = this.buildBaseVideoAbuse()
- const video: AdminVideoAbuse = baseVideo
- ? Object.assign(baseVideo, {
- countReports: countReportsForVideo,
- nthReport: nthReportForVideo
- })
- : null
- const comment: AdminVideoCommentAbuse = this.buildBaseVideoCommentAbuse()
- const abuse = this.buildBaseAbuse(countMessages || 0)
- return Object.assign(abuse, {
- video,
- comment,
- moderationComment: this.moderationComment,
- reporterAccount: this.ReporterAccount
- ? this.ReporterAccount.toFormattedJSON()
- : null,
- countReportsForReporter: (countReportsForReporter || 0),
- countReportsForReportee: (countReportsForReportee || 0)
- })
- }
- toFormattedUserJSON (this: MAbuseUserFormattable): UserAbuse {
- const countMessages = this.get('countMessages') as number
- const video = this.buildBaseVideoAbuse()
- const comment = this.buildBaseVideoCommentAbuse()
- const abuse = this.buildBaseAbuse(countMessages || 0)
- return Object.assign(abuse, {
- video,
- comment
- })
- }
- toActivityPubObject (this: MAbuseAP): AbuseObject {
- const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
- const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
- const startAt = this.VideoAbuse?.startAt
- const endAt = this.VideoAbuse?.endAt
- return {
- type: 'Flag' as 'Flag',
- content: this.reason,
- mediaType: 'text/markdown',
- object,
- tag: predefinedReasons.map(r => ({
- type: 'Hashtag' as 'Hashtag',
- name: r
- })),
- startAt,
- endAt
- }
- }
- 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'
- }
- private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] {
- const invertedPredefinedReasons = invert(abusePredefinedReasonsMap)
- return (predefinedReasons || [])
- .map(r => invertedPredefinedReasons[r] as AbusePredefinedReasonsString)
- .filter(v => !!v)
- }
- }
|