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