import * as Bluebird from 'bluebird'
-import { literal, Op } from 'sequelize'
+import { invert } from 'lodash'
+import { literal, Op, WhereOptions } from 'sequelize'
import {
AllowNull,
BelongsTo,
DataType,
Default,
ForeignKey,
+ HasOne,
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 { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
import {
- isVideoAbuseModerationCommentValid,
- isVideoAbuseReasonValid,
- isVideoAbuseStateValid
-} from '../../helpers/custom-validators/video-abuses'
-import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
-import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models'
-import { AccountModel } from '../account/account'
+ Abuse,
+ AbuseObject,
+ AbusePredefinedReasons,
+ abusePredefinedReasonsMap,
+ AbusePredefinedReasonsString,
+ AbuseState,
+ AbuseVideoIs,
+ VideoAbuse
+} from '@shared/models'
+import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter'
+import { CONSTRAINTS_FIELDS, ABUSE_STATES } 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 { ThumbnailModel } from './thumbnail'
-import { VideoModel } from './video'
-import { VideoBlacklistModel } from './video-blacklist'
-import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
-import { invert } from 'lodash'
+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 { VideoAbuseModel } from './video-abuse'
+import { VideoCommentAbuseModel } from './video-comment-abuse'
export enum ScopeNames {
FOR_API = 'FOR_API'
search?: string
searchReporter?: string
searchReportee?: string
+
+ // video releated
searchVideo?: string
searchVideoChannel?: string
+ videoIs?: AbuseVideoIs
// filters
id?: number
predefinedReasonId?: number
+ filter?: AbuseFilter
- state?: VideoAbuseState
- videoIs?: VideoAbuseVideoIs
+ 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]: [
- { videoId: { [Op.not]: null } },
- searchAttribute(options.search, '$Video.name$')
+ { '$VideoAbuse.videoId$': { [Op.not]: null } },
+ searchAttribute(options.search, '$VideoAbuse.Video.name$')
]
},
{
[Op.and]: [
- { videoId: { [Op.not]: null } },
- searchAttribute(options.search, '$Video.VideoChannel.name$')
+ { '$VideoAbuse.videoId$': { [Op.not]: null } },
+ searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$')
]
},
{
[Op.and]: [
- { deletedVideo: { [Op.not]: null } },
- { deletedVideo: searchAttribute(options.search, 'name') }
+ { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
+ literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`)
]
},
{
[Op.and]: [
- { deletedVideo: { [Op.not]: null } },
- { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
+ { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
+ literal(`"VideoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE ${escapedSearch}`)
]
},
- searchAttribute(options.search, '$Account.name$')
+ searchAttribute(options.search, '$ReporterAccount.name$'),
+ searchAttribute(options.search, '$FlaggedAccount.name$')
]
})
}
if (options.videoIs === 'deleted') {
Object.assign(where, {
- deletedVideo: {
+ '$VideoAbuse.deletedVideo$': {
[Op.not]: null
}
})
})
}
- const onlyBlacklisted = options.videoIs === 'blacklisted'
-
return {
attributes: {
include: [
'(' +
'SELECT count(*) ' +
'FROM "videoAbuse" ' +
- 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
+ 'WHERE "videoId" = "VideoAbuse"."videoId" ' +
')'
),
'countReportsForVideo'
'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
'FROM "videoAbuse" ' +
') t ' +
- 'WHERE t.id = "VideoAbuseModel".id ' +
+ 'WHERE t.id = "VideoAbuse".id' +
')'
),
'nthReportForVideo'
'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" ' +
+ 'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
')'
),
'countReportsForReporter__video'
'(' +
'SELECT count(DISTINCT "videoAbuse"."id") ' +
'FROM "videoAbuse" ' +
- `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
+ `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` +
')'
),
'countReportsForReporter__deletedVideo'
'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) ` +
+ '"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' +
+ `OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
')'
),
'countReportsForReportee__video'
'(' +
'SELECT count(DISTINCT "videoAbuse"."id") ' +
'FROM "videoAbuse" ' +
- `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` +
+ `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuse->Video->VideoChannel"."accountId" ` +
`OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
- `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
+ `CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
')'
),
'countReportsForReportee__deletedVideo'
},
include: [
{
- model: AccountModel,
+ model: AccountModel.scope(AccountScopeNames.SUMMARY),
+ as: 'ReporterAccount',
required: true,
where: searchAttribute(options.searchReporter, 'name')
},
{
- model: VideoModel,
- required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
- where: searchAttribute(options.searchVideo, 'name'),
+ model: AccountModel.scope(AccountScopeNames.SUMMARY),
+ as: 'FlaggedAccount',
+ required: true,
+ where: searchAttribute(options.searchReportee, 'name')
+ },
+ {
+ model: VideoAbuseModel,
+ required: options.filter === 'video' || !!options.videoIs || videoRequired,
include: [
{
- model: ThumbnailModel
- },
- {
- model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
- where: searchAttribute(options.searchVideoChannel, 'name'),
+ model: VideoModel,
+ required: videoRequired,
+ where: searchAttribute(options.searchVideo, 'name'),
include: [
{
- model: AccountModel,
- where: searchAttribute(options.searchReportee, 'name')
+ 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')
+ }
+ ]
+ },
+ {
+ attributes: [ 'id', 'reason', 'unfederated' ],
+ model: VideoBlacklistModel,
+ required: onlyBlacklisted
}
]
- },
- {
- attributes: [ 'id', 'reason', 'unfederated' ],
- model: VideoBlacklistModel,
- required: onlyBlacklisted
}
]
}
}
}))
@Table({
- tableName: 'videoAbuse',
+ tableName: 'abuse',
indexes: [
{
- fields: [ 'videoId' ]
+ fields: [ 'reporterAccountId' ]
},
{
- fields: [ 'reporterAccountId' ]
+ fields: [ 'flaggedAccountId' ]
}
]
})
-export class VideoAbuseModel extends Model<VideoAbuseModel> {
+export class AbuseModel extends Model<AbuseModel> {
@AllowNull(false)
@Default(null)
- @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
+ @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
reason: string
@AllowNull(false)
@Default(null)
- @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
+ @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
@Column
- state: VideoAbuseState
+ state: AbuseState
@AllowNull(true)
@Default(null)
- @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
+ @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
moderationComment: string
- @AllowNull(true)
- @Default(null)
- @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
+ predefinedReasons: AbusePredefinedReasons[]
@CreatedAt
createdAt: Date
@BelongsTo(() => AccountModel, {
foreignKey: {
+ name: 'reporterAccountId',
allowNull: true
},
+ as: 'ReporterAccount',
onDelete: 'set null'
})
- Account: AccountModel
+ ReporterAccount: AccountModel
- @ForeignKey(() => VideoModel)
+ @ForeignKey(() => AccountModel)
@Column
- videoId: number
+ flaggedAccountId: number
- @BelongsTo(() => VideoModel, {
+ @BelongsTo(() => AccountModel, {
foreignKey: {
+ name: 'flaggedAccountId',
allowNull: true
},
+ as: 'FlaggedAccount',
onDelete: 'set null'
})
- Video: VideoModel
+ FlaggedAccount: AccountModel
+
+ @HasOne(() => VideoCommentAbuseModel, {
+ foreignKey: {
+ name: 'abuseId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoCommentAbuse: VideoCommentAbuseModel
- static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
- const videoAttributes = {}
- if (videoId) videoAttributes['videoId'] = videoId
- if (uuid) videoAttributes['deletedVideo'] = { uuid }
+ @HasOne(() => VideoAbuseModel, {
+ foreignKey: {
+ name: 'abuseId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoAbuse: VideoAbuseModel
+
+ static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
+ const videoWhere: WhereOptions = {}
+
+ if (videoId) videoWhere.videoId = videoId
+ if (uuid) videoWhere.deletedVideo = { uuid }
const query = {
+ include: [
+ {
+ model: VideoAbuseModel,
+ required: true,
+ where: videoWhere
+ }
+ ],
where: {
- id,
- ...videoAttributes
+ id
}
}
- return VideoAbuseModel.findOne(query)
+ return AbuseModel.findOne(query)
}
static listForApi (parameters: {
count: number
sort: string
+ filter?: AbuseFilter
+
serverAccountId: number
user?: MUserAccountId
id?: number
- predefinedReason?: VideoAbusePredefinedReasonsString
- state?: VideoAbuseState
- videoIs?: VideoAbuseVideoIs
+ predefinedReason?: AbusePredefinedReasonsString
+ state?: AbuseState
+ videoIs?: AbuseVideoIs
search?: string
searchReporter?: string
predefinedReason,
searchReportee,
searchVideo,
+ filter,
searchVideoChannel,
searchReporter,
id
} = parameters
const userAccountId = user ? user.Account.id : undefined
- const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
+ const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
const query = {
offset: start,
limit: count,
order: getSort(sort),
- col: 'VideoAbuseModel.id',
+ col: 'AbuseModel.id',
distinct: true
}
const filters = {
id,
+ filter,
predefinedReasonId,
search,
state,
userAccountId
}
- return VideoAbuseModel
+ return AbuseModel
.scope([
{ method: [ ScopeNames.FOR_API, filters ] }
])
})
}
- toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
- const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+ 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 countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
- const video = this.Video
- ? this.Video
- : this.deletedVideo
+ let video: VideoAbuse
+
+ if (this.VideoAbuse) {
+ const abuseModel = this.VideoAbuse
+ const entity = abuseModel.Video || abuseModel.deletedVideo
+
+ video = {
+ 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
+ }
+ }
return {
id: this.id,
reason: this.reason,
predefinedReasons,
- reporterAccount: this.Account.toFormattedJSON(),
+
+ reporterAccount: this.ReporterAccount.toFormattedJSON(),
+
state: {
id: this.state,
- label: VideoAbuseModel.getStateLabel(this.state)
+ label: AbuseModel.getStateLabel(this.state)
},
+
moderationComment: this.moderationComment,
- video: {
- id: video.id,
- uuid: video.uuid,
- name: video.name,
- nsfw: video.nsfw,
- deleted: !this.Video,
- blacklisted: this.Video?.isBlacklisted() || false,
- thumbnailPath: this.Video?.getMiniatureStaticPath(),
- channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
- },
+
+ video,
+ comment: null,
+
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)
+ countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0),
+
+ // FIXME: deprecated in 2.3, remove this
+ startAt: null,
+ endAt: null
}
}
- toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
- const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+ 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.startAt
- const endAt = this.endAt
+ const startAt = this.VideoAbuse?.startAt
+ const endAt = this.VideoAbuse?.endAt
return {
type: 'Flag' as 'Flag',
content: this.reason,
- object: this.Video.url,
+ object,
tag: predefinedReasons.map(r => ({
type: 'Hashtag' as 'Hashtag',
name: r
}
private static getStateLabel (id: number) {
- return VIDEO_ABUSE_STATES[id] || 'Unknown'
+ return ABUSE_STATES[id] || 'Unknown'
}
- private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] {
+ private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] {
return (predefinedReasons || [])
- .filter(r => r in VideoAbusePredefinedReasons)
- .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
+ .filter(r => r in AbusePredefinedReasons)
+ .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString)
}
}