]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-abuse.ts
Use video abuse filters on client side
[github/Chocobozzz/PeerTube.git] / server / models / video / video-abuse.ts
CommitLineData
feb34f6b
C
1import * as Bluebird from 'bluebird'
2import { literal, Op } from 'sequelize'
86521a67 3import {
feb34f6b
C
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 Default,
10 ForeignKey,
11 Is,
12 Model,
13 Scopes,
14 Table,
15 UpdatedAt
86521a67 16} from 'sequelize-typescript'
feb34f6b
C
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
18import { VideoAbuseState, VideoDetails } from '../../../shared'
3fd3ab2d 19import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
19a3b914 20import { VideoAbuse } from '../../../shared/models/videos'
268eebed
C
21import {
22 isVideoAbuseModerationCommentValid,
23 isVideoAbuseReasonValid,
24 isVideoAbuseStateValid
25} from '../../helpers/custom-validators/video-abuses'
74dc3bca 26import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
f0a47bc9 27import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
feb34f6b
C
28import { AccountModel } from '../account/account'
29import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
86521a67 30import { ThumbnailModel } from './thumbnail'
feb34f6b 31import { VideoModel } from './video'
86521a67 32import { VideoBlacklistModel } from './video-blacklist'
e0a92917 33import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
3fd3ab2d 34
844db39e
RK
35export enum ScopeNames {
36 FOR_API = 'FOR_API'
37}
38
39@Scopes(() => ({
40 [ScopeNames.FOR_API]: (options: {
0d3a2982 41 // search
844db39e
RK
42 search?: string
43 searchReporter?: string
0d3a2982 44 searchReportee?: string
844db39e
RK
45 searchVideo?: string
46 searchVideoChannel?: string
fc8aabd0 47
0d3a2982
RK
48 // filters
49 id?: number
feb34f6b 50
0d3a2982 51 state?: VideoAbuseState
feb34f6b 52 videoIs?: VideoAbuseVideoIs
fc8aabd0 53
0d3a2982 54 // accountIds
844db39e 55 serverAccountId: number
0251197e 56 userAccountId: number
844db39e 57 }) => {
feb34f6b 58 const where = {
844db39e
RK
59 reporterAccountId: {
60 [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')')
61 }
62 }
63
64 if (options.search) {
feb34f6b 65 Object.assign(where, {
844db39e
RK
66 [Op.or]: [
67 {
68 [Op.and]: [
69 { videoId: { [Op.not]: null } },
0251197e 70 searchAttribute(options.search, '$Video.name$')
844db39e
RK
71 ]
72 },
73 {
74 [Op.and]: [
75 { videoId: { [Op.not]: null } },
0251197e 76 searchAttribute(options.search, '$Video.VideoChannel.name$')
844db39e
RK
77 ]
78 },
79 {
80 [Op.and]: [
81 { deletedVideo: { [Op.not]: null } },
0251197e 82 { deletedVideo: searchAttribute(options.search, 'name') }
844db39e
RK
83 ]
84 },
85 {
86 [Op.and]: [
87 { deletedVideo: { [Op.not]: null } },
0251197e 88 { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
844db39e
RK
89 ]
90 },
0251197e 91 searchAttribute(options.search, '$Account.name$')
844db39e
RK
92 ]
93 })
94 }
95
feb34f6b
C
96 if (options.id) Object.assign(where, { id: options.id })
97 if (options.state) Object.assign(where, { state: options.state })
0d3a2982 98
feb34f6b
C
99 if (options.videoIs === 'deleted') {
100 Object.assign(where, {
101 deletedVideo: {
102 [Op.not]: null
103 }
0d3a2982
RK
104 })
105 }
106
feb34f6b 107 const onlyBlacklisted = options.videoIs === 'blacklisted'
0d3a2982 108
844db39e 109 return {
5fd4ca00
RK
110 attributes: {
111 include: [
112 [
efa012ed 113 // we don't care about this count for deleted videos, so there are not included
5fd4ca00
RK
114 literal(
115 '(' +
0251197e
RK
116 'SELECT count(*) ' +
117 'FROM "videoAbuse" ' +
118 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
5fd4ca00
RK
119 ')'
120 ),
121 'countReportsForVideo'
122 ],
123 [
efa012ed 124 // we don't care about this count for deleted videos, so there are not included
5fd4ca00
RK
125 literal(
126 '(' +
127 'SELECT t.nth ' +
128 'FROM ( ' +
129 'SELECT id, ' +
130 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
131 'FROM "videoAbuse" ' +
132 ') t ' +
133 'WHERE t.id = "VideoAbuseModel".id ' +
134 ')'
135 ),
136 'nthReportForVideo'
137 ],
138 [
139 literal(
140 '(' +
141 'SELECT count("videoAbuse"."id") ' +
142 'FROM "videoAbuse" ' +
143 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
144 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
145 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
146 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
147 ')'
148 ),
efa012ed
RK
149 'countReportsForReporter__video'
150 ],
151 [
152 literal(
153 '(' +
154 'SELECT count(DISTINCT "videoAbuse"."id") ' +
155 'FROM "videoAbuse" ' +
156 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
157 ')'
158 ),
159 'countReportsForReporter__deletedVideo'
5fd4ca00
RK
160 ],
161 [
162 literal(
163 '(' +
0251197e 164 'SELECT count(DISTINCT "videoAbuse"."id") ' +
5fd4ca00
RK
165 'FROM "videoAbuse" ' +
166 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
167 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
efa012ed
RK
168 'INNER JOIN "account" ON ' +
169 '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' +
170 `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
171 ')'
172 ),
173 'countReportsForReportee__video'
174 ],
175 [
176 literal(
177 '(' +
178 'SELECT count(DISTINCT "videoAbuse"."id") ' +
179 'FROM "videoAbuse" ' +
180 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` +
197876ea
RK
181 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
182 `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
5fd4ca00
RK
183 ')'
184 ),
efa012ed 185 'countReportsForReportee__deletedVideo'
5fd4ca00
RK
186 ]
187 ]
188 },
86521a67
RK
189 include: [
190 {
844db39e
RK
191 model: AccountModel,
192 required: true,
0251197e 193 where: searchAttribute(options.searchReporter, 'name')
86521a67
RK
194 },
195 {
844db39e 196 model: VideoModel,
feb34f6b 197 required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
0251197e 198 where: searchAttribute(options.searchVideo, 'name'),
86521a67
RK
199 include: [
200 {
844db39e
RK
201 model: ThumbnailModel
202 },
203 {
e0a92917 204 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
0d3a2982
RK
205 where: searchAttribute(options.searchVideoChannel, 'name'),
206 include: [
207 {
208 model: AccountModel,
209 where: searchAttribute(options.searchReportee, 'name')
210 }
211 ]
844db39e
RK
212 },
213 {
214 attributes: [ 'id', 'reason', 'unfederated' ],
9b1fa49b
RK
215 model: VideoBlacklistModel,
216 required: onlyBlacklisted
86521a67
RK
217 }
218 ]
86521a67 219 }
844db39e
RK
220 ],
221 where
86521a67 222 }
844db39e 223 }
86521a67 224}))
3fd3ab2d
C
225@Table({
226 tableName: 'videoAbuse',
227 indexes: [
55fa55a9 228 {
3fd3ab2d 229 fields: [ 'videoId' ]
55fa55a9
C
230 },
231 {
3fd3ab2d 232 fields: [ 'reporterAccountId' ]
55fa55a9 233 }
e02643f3 234 ]
3fd3ab2d
C
235})
236export class VideoAbuseModel extends Model<VideoAbuseModel> {
e02643f3 237
3fd3ab2d 238 @AllowNull(false)
1506307f 239 @Default(null)
3fd3ab2d 240 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
1506307f 241 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
3fd3ab2d 242 reason: string
21e0727a 243
268eebed
C
244 @AllowNull(false)
245 @Default(null)
246 @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
247 @Column
248 state: VideoAbuseState
249
250 @AllowNull(true)
251 @Default(null)
1735c825 252 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
268eebed
C
253 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
254 moderationComment: string
255
68d19a0a
RK
256 @AllowNull(true)
257 @Default(null)
258 @Column(DataType.JSONB)
5fd4ca00 259 deletedVideo: VideoDetails
68d19a0a 260
3fd3ab2d
C
261 @CreatedAt
262 createdAt: Date
21e0727a 263
3fd3ab2d
C
264 @UpdatedAt
265 updatedAt: Date
e02643f3 266
3fd3ab2d
C
267 @ForeignKey(() => AccountModel)
268 @Column
269 reporterAccountId: number
55fa55a9 270
3fd3ab2d 271 @BelongsTo(() => AccountModel, {
55fa55a9 272 foreignKey: {
68d19a0a 273 allowNull: true
55fa55a9 274 },
68d19a0a 275 onDelete: 'set null'
55fa55a9 276 })
3fd3ab2d
C
277 Account: AccountModel
278
279 @ForeignKey(() => VideoModel)
280 @Column
281 videoId: number
55fa55a9 282
3fd3ab2d 283 @BelongsTo(() => VideoModel, {
55fa55a9 284 foreignKey: {
68d19a0a 285 allowNull: true
55fa55a9 286 },
68d19a0a 287 onDelete: 'set null'
55fa55a9 288 })
3fd3ab2d
C
289 Video: VideoModel
290
68d19a0a
RK
291 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
292 const videoAttributes = {}
293 if (videoId) videoAttributes['videoId'] = videoId
294 if (uuid) videoAttributes['deletedVideo'] = { uuid }
295
268eebed
C
296 const query = {
297 where: {
298 id,
68d19a0a 299 ...videoAttributes
268eebed
C
300 }
301 }
302 return VideoAbuseModel.findOne(query)
303 }
304
f0a47bc9 305 static listForApi (parameters: {
a1587156
C
306 start: number
307 count: number
308 sort: string
feb34f6b 309
f0a47bc9
C
310 serverAccountId: number
311 user?: MUserAccountId
feb34f6b
C
312
313 id?: number
314 state?: VideoAbuseState
315 videoIs?: VideoAbuseVideoIs
316
317 search?: string
318 searchReporter?: string
319 searchReportee?: string
320 searchVideo?: string
321 searchVideoChannel?: string
f0a47bc9 322 }) {
feb34f6b
C
323 const {
324 start,
325 count,
326 sort,
327 search,
328 user,
329 serverAccountId,
330 state,
331 videoIs,
332 searchReportee,
333 searchVideo,
334 searchVideoChannel,
335 searchReporter,
336 id
337 } = parameters
338
f0a47bc9
C
339 const userAccountId = user ? user.Account.id : undefined
340
3fd3ab2d
C
341 const query = {
342 offset: start,
343 limit: count,
3bb6c526 344 order: getSort(sort),
86521a67
RK
345 col: 'VideoAbuseModel.id',
346 distinct: true
3fd3ab2d 347 }
55fa55a9 348
844db39e 349 const filters = {
feb34f6b
C
350 id,
351 search,
352 state,
353 videoIs,
354 searchReportee,
355 searchVideo,
356 searchVideoChannel,
357 searchReporter,
844db39e
RK
358 serverAccountId,
359 userAccountId
360 }
361
362 return VideoAbuseModel
363 .scope({ method: [ ScopeNames.FOR_API, filters ] })
364 .findAndCountAll(query)
3fd3ab2d
C
365 .then(({ rows, count }) => {
366 return { total: count, data: rows }
367 })
55fa55a9
C
368 }
369
1ca9f7c3 370 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
5fd4ca00
RK
371 const countReportsForVideo = this.get('countReportsForVideo') as number
372 const nthReportForVideo = this.get('nthReportForVideo') as number
efa012ed
RK
373 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
374 const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number
375 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
376 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
5fd4ca00 377
68d19a0a
RK
378 const video = this.Video
379 ? this.Video
380 : this.deletedVideo
381
3fd3ab2d
C
382 return {
383 id: this.id,
384 reason: this.reason,
19a3b914 385 reporterAccount: this.Account.toFormattedJSON(),
268eebed
C
386 state: {
387 id: this.state,
388 label: VideoAbuseModel.getStateLabel(this.state)
389 },
390 moderationComment: this.moderationComment,
19a3b914 391 video: {
68d19a0a
RK
392 id: video.id,
393 uuid: video.uuid,
394 name: video.name,
395 nsfw: video.nsfw,
86521a67
RK
396 deleted: !this.Video,
397 blacklisted: this.Video && this.Video.isBlacklisted(),
398 thumbnailPath: this.Video?.getMiniatureStaticPath(),
5fd4ca00 399 channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
19a3b914 400 },
5fd4ca00
RK
401 createdAt: this.createdAt,
402 updatedAt: this.updatedAt,
403 count: countReportsForVideo || 0,
404 nth: nthReportForVideo || 0,
efa012ed
RK
405 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
406 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0)
3fd3ab2d
C
407 }
408 }
409
453e83ea 410 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
3fd3ab2d
C
411 return {
412 type: 'Flag' as 'Flag',
413 content: this.reason,
414 object: this.Video.url
415 }
416 }
268eebed
C
417
418 private static getStateLabel (id: number) {
419 return VIDEO_ABUSE_STATES[id] || 'Unknown'
420 }
55fa55a9 421}