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