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