]>
Commit | Line | Data |
---|---|---|
feb34f6b | 1 | import * as Bluebird from 'bluebird' |
d95d1559 | 2 | import { invert } from 'lodash' |
811cef14 | 3 | import { literal, Op, QueryTypes, WhereOptions } from 'sequelize' |
86521a67 | 4 | import { |
feb34f6b C |
5 | AllowNull, |
6 | BelongsTo, | |
7 | Column, | |
8 | CreatedAt, | |
9 | DataType, | |
10 | Default, | |
11 | ForeignKey, | |
d95d1559 | 12 | HasOne, |
feb34f6b C |
13 | Is, |
14 | Model, | |
15 | Scopes, | |
16 | Table, | |
17 | UpdatedAt | |
86521a67 | 18 | } from 'sequelize-typescript' |
d95d1559 | 19 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' |
268eebed | 20 | import { |
d95d1559 | 21 | Abuse, |
57f6896f | 22 | AbuseFilter, |
d95d1559 C |
23 | AbuseObject, |
24 | AbusePredefinedReasons, | |
25 | abusePredefinedReasonsMap, | |
26 | AbusePredefinedReasonsString, | |
27 | AbuseState, | |
28 | AbuseVideoIs, | |
57f6896f C |
29 | VideoAbuse, |
30 | VideoCommentAbuse | |
d95d1559 | 31 | } from '@shared/models' |
57f6896f | 32 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
d95d1559 | 33 | import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' |
4f32032f | 34 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
811cef14 | 35 | import { getSort, throwIfNotValid } from '../utils' |
d95d1559 C |
36 | import { ThumbnailModel } from '../video/thumbnail' |
37 | import { VideoModel } from '../video/video' | |
38 | import { VideoBlacklistModel } from '../video/video-blacklist' | |
4f32032f C |
39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' |
40 | import { VideoCommentModel } from '../video/video-comment' | |
811cef14 | 41 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' |
d95d1559 C |
42 | import { VideoAbuseModel } from './video-abuse' |
43 | import { VideoCommentAbuseModel } from './video-comment-abuse' | |
3fd3ab2d | 44 | |
844db39e RK |
45 | export enum ScopeNames { |
46 | FOR_API = 'FOR_API' | |
47 | } | |
48 | ||
49 | @Scopes(() => ({ | |
811cef14 | 50 | [ScopeNames.FOR_API]: () => { |
844db39e | 51 | return { |
5fd4ca00 RK |
52 | attributes: { |
53 | include: [ | |
54 | [ | |
efa012ed | 55 | // we don't care about this count for deleted videos, so there are not included |
5fd4ca00 RK |
56 | literal( |
57 | '(' + | |
0251197e RK |
58 | 'SELECT count(*) ' + |
59 | 'FROM "videoAbuse" ' + | |
4f32032f | 60 | 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' + |
5fd4ca00 RK |
61 | ')' |
62 | ), | |
63 | 'countReportsForVideo' | |
64 | ], | |
65 | [ | |
efa012ed | 66 | // we don't care about this count for deleted videos, so there are not included |
5fd4ca00 RK |
67 | literal( |
68 | '(' + | |
69 | 'SELECT t.nth ' + | |
70 | 'FROM ( ' + | |
71 | 'SELECT id, ' + | |
72 | 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + | |
73 | 'FROM "videoAbuse" ' + | |
74 | ') t ' + | |
4f32032f | 75 | 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' + |
5fd4ca00 RK |
76 | ')' |
77 | ), | |
78 | 'nthReportForVideo' | |
79 | ], | |
80 | [ | |
81 | literal( | |
82 | '(' + | |
4f32032f C |
83 | 'SELECT count("abuse"."id") ' + |
84 | 'FROM "abuse" ' + | |
85 | 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' + | |
efa012ed RK |
86 | ')' |
87 | ), | |
4f32032f | 88 | 'countReportsForReporter' |
efa012ed RK |
89 | ], |
90 | [ | |
91 | literal( | |
92 | '(' + | |
4f32032f C |
93 | 'SELECT count("abuse"."id") ' + |
94 | 'FROM "abuse" ' + | |
95 | 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' + | |
5fd4ca00 RK |
96 | ')' |
97 | ), | |
4f32032f | 98 | 'countReportsForReportee' |
5fd4ca00 RK |
99 | ] |
100 | ] | |
101 | }, | |
86521a67 RK |
102 | include: [ |
103 | { | |
811cef14 C |
104 | model: AccountModel.scope({ |
105 | method: [ | |
106 | AccountScopeNames.SUMMARY, | |
107 | { actorRequired: false } as AccountSummaryOptions | |
108 | ] | |
109 | }), | |
110 | as: 'ReporterAccount' | |
86521a67 RK |
111 | }, |
112 | { | |
4f32032f C |
113 | model: AccountModel.scope({ |
114 | method: [ | |
115 | AccountScopeNames.SUMMARY, | |
116 | { actorRequired: false } as AccountSummaryOptions | |
117 | ] | |
118 | }), | |
811cef14 | 119 | as: 'FlaggedAccount' |
d95d1559 | 120 | }, |
57f6896f C |
121 | { |
122 | model: VideoCommentAbuseModel.unscoped(), | |
57f6896f C |
123 | include: [ |
124 | { | |
125 | model: VideoCommentModel.unscoped(), | |
57f6896f C |
126 | include: [ |
127 | { | |
128 | model: VideoModel.unscoped(), | |
4f32032f | 129 | attributes: [ 'name', 'id', 'uuid' ] |
57f6896f C |
130 | } |
131 | ] | |
132 | } | |
133 | ] | |
134 | }, | |
d95d1559 | 135 | { |
4f32032f | 136 | model: VideoAbuseModel.unscoped(), |
86521a67 RK |
137 | include: [ |
138 | { | |
4f32032f C |
139 | attributes: [ 'id', 'uuid', 'name', 'nsfw' ], |
140 | model: VideoModel.unscoped(), | |
0d3a2982 RK |
141 | include: [ |
142 | { | |
8ca56654 | 143 | attributes: [ 'filename', 'fileUrl', 'type' ], |
d95d1559 C |
144 | model: ThumbnailModel |
145 | }, | |
146 | { | |
4f32032f C |
147 | model: VideoChannelModel.scope({ |
148 | method: [ | |
149 | VideoChannelScopeNames.SUMMARY, | |
150 | { withAccount: false, actorRequired: false } as ChannelSummaryOptions | |
151 | ] | |
152 | }), | |
811cef14 | 153 | required: false |
d95d1559 C |
154 | }, |
155 | { | |
156 | attributes: [ 'id', 'reason', 'unfederated' ], | |
811cef14 C |
157 | required: false, |
158 | model: VideoBlacklistModel | |
0d3a2982 RK |
159 | } |
160 | ] | |
86521a67 RK |
161 | } |
162 | ] | |
86521a67 | 163 | } |
811cef14 | 164 | ] |
86521a67 | 165 | } |
844db39e | 166 | } |
86521a67 | 167 | })) |
3fd3ab2d | 168 | @Table({ |
d95d1559 | 169 | tableName: 'abuse', |
3fd3ab2d | 170 | indexes: [ |
55fa55a9 | 171 | { |
d95d1559 | 172 | fields: [ 'reporterAccountId' ] |
55fa55a9 C |
173 | }, |
174 | { | |
d95d1559 | 175 | fields: [ 'flaggedAccountId' ] |
55fa55a9 | 176 | } |
e02643f3 | 177 | ] |
3fd3ab2d | 178 | }) |
d95d1559 | 179 | export class AbuseModel extends Model<AbuseModel> { |
e02643f3 | 180 | |
3fd3ab2d | 181 | @AllowNull(false) |
1506307f | 182 | @Default(null) |
4f32032f | 183 | @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) |
d95d1559 | 184 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max)) |
3fd3ab2d | 185 | reason: string |
21e0727a | 186 | |
268eebed C |
187 | @AllowNull(false) |
188 | @Default(null) | |
4f32032f | 189 | @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) |
268eebed | 190 | @Column |
d95d1559 | 191 | state: AbuseState |
268eebed C |
192 | |
193 | @AllowNull(true) | |
194 | @Default(null) | |
4f32032f | 195 | @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true)) |
d95d1559 | 196 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max)) |
268eebed C |
197 | moderationComment: string |
198 | ||
1ebddadd RK |
199 | @AllowNull(true) |
200 | @Default(null) | |
201 | @Column(DataType.ARRAY(DataType.INTEGER)) | |
d95d1559 | 202 | predefinedReasons: AbusePredefinedReasons[] |
1ebddadd | 203 | |
3fd3ab2d C |
204 | @CreatedAt |
205 | createdAt: Date | |
21e0727a | 206 | |
3fd3ab2d C |
207 | @UpdatedAt |
208 | updatedAt: Date | |
e02643f3 | 209 | |
3fd3ab2d C |
210 | @ForeignKey(() => AccountModel) |
211 | @Column | |
212 | reporterAccountId: number | |
55fa55a9 | 213 | |
3fd3ab2d | 214 | @BelongsTo(() => AccountModel, { |
55fa55a9 | 215 | foreignKey: { |
d95d1559 | 216 | name: 'reporterAccountId', |
68d19a0a | 217 | allowNull: true |
55fa55a9 | 218 | }, |
d95d1559 | 219 | as: 'ReporterAccount', |
68d19a0a | 220 | onDelete: 'set null' |
55fa55a9 | 221 | }) |
d95d1559 | 222 | ReporterAccount: AccountModel |
3fd3ab2d | 223 | |
d95d1559 | 224 | @ForeignKey(() => AccountModel) |
3fd3ab2d | 225 | @Column |
d95d1559 | 226 | flaggedAccountId: number |
55fa55a9 | 227 | |
d95d1559 | 228 | @BelongsTo(() => AccountModel, { |
55fa55a9 | 229 | foreignKey: { |
d95d1559 | 230 | name: 'flaggedAccountId', |
68d19a0a | 231 | allowNull: true |
55fa55a9 | 232 | }, |
d95d1559 | 233 | as: 'FlaggedAccount', |
68d19a0a | 234 | onDelete: 'set null' |
55fa55a9 | 235 | }) |
d95d1559 C |
236 | FlaggedAccount: AccountModel |
237 | ||
238 | @HasOne(() => VideoCommentAbuseModel, { | |
239 | foreignKey: { | |
240 | name: 'abuseId', | |
241 | allowNull: false | |
242 | }, | |
243 | onDelete: 'cascade' | |
244 | }) | |
245 | VideoCommentAbuse: VideoCommentAbuseModel | |
3fd3ab2d | 246 | |
d95d1559 C |
247 | @HasOne(() => VideoAbuseModel, { |
248 | foreignKey: { | |
249 | name: 'abuseId', | |
250 | allowNull: false | |
251 | }, | |
252 | onDelete: 'cascade' | |
253 | }) | |
254 | VideoAbuse: VideoAbuseModel | |
255 | ||
57f6896f | 256 | // FIXME: deprecated in 2.3. Remove these validators |
d95d1559 C |
257 | static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> { |
258 | const videoWhere: WhereOptions = {} | |
259 | ||
260 | if (videoId) videoWhere.videoId = videoId | |
261 | if (uuid) videoWhere.deletedVideo = { uuid } | |
68d19a0a | 262 | |
268eebed | 263 | const query = { |
d95d1559 C |
264 | include: [ |
265 | { | |
266 | model: VideoAbuseModel, | |
267 | required: true, | |
268 | where: videoWhere | |
269 | } | |
270 | ], | |
268eebed | 271 | where: { |
d95d1559 | 272 | id |
268eebed C |
273 | } |
274 | } | |
d95d1559 | 275 | return AbuseModel.findOne(query) |
268eebed C |
276 | } |
277 | ||
57f6896f C |
278 | static loadById (id: number): Bluebird<MAbuse> { |
279 | const query = { | |
280 | where: { | |
281 | id | |
282 | } | |
283 | } | |
284 | ||
285 | return AbuseModel.findOne(query) | |
286 | } | |
287 | ||
811cef14 | 288 | static async listForApi (parameters: { |
a1587156 C |
289 | start: number |
290 | count: number | |
291 | sort: string | |
feb34f6b | 292 | |
d95d1559 C |
293 | filter?: AbuseFilter |
294 | ||
f0a47bc9 C |
295 | serverAccountId: number |
296 | user?: MUserAccountId | |
feb34f6b C |
297 | |
298 | id?: number | |
d95d1559 C |
299 | predefinedReason?: AbusePredefinedReasonsString |
300 | state?: AbuseState | |
301 | videoIs?: AbuseVideoIs | |
feb34f6b C |
302 | |
303 | search?: string | |
304 | searchReporter?: string | |
305 | searchReportee?: string | |
306 | searchVideo?: string | |
307 | searchVideoChannel?: string | |
f0a47bc9 | 308 | }) { |
feb34f6b C |
309 | const { |
310 | start, | |
311 | count, | |
312 | sort, | |
313 | search, | |
314 | user, | |
315 | serverAccountId, | |
316 | state, | |
317 | videoIs, | |
1ebddadd | 318 | predefinedReason, |
feb34f6b C |
319 | searchReportee, |
320 | searchVideo, | |
d95d1559 | 321 | filter, |
feb34f6b C |
322 | searchVideoChannel, |
323 | searchReporter, | |
324 | id | |
325 | } = parameters | |
326 | ||
f0a47bc9 | 327 | const userAccountId = user ? user.Account.id : undefined |
d95d1559 | 328 | const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined |
f0a47bc9 | 329 | |
811cef14 C |
330 | const queryOptions: BuildAbusesQueryOptions = { |
331 | start, | |
332 | count, | |
333 | sort, | |
feb34f6b | 334 | id, |
d95d1559 | 335 | filter, |
1ebddadd | 336 | predefinedReasonId, |
feb34f6b C |
337 | search, |
338 | state, | |
339 | videoIs, | |
340 | searchReportee, | |
341 | searchVideo, | |
342 | searchVideoChannel, | |
343 | searchReporter, | |
844db39e RK |
344 | serverAccountId, |
345 | userAccountId | |
346 | } | |
347 | ||
811cef14 C |
348 | const [ total, data ] = await Promise.all([ |
349 | AbuseModel.internalCountForApi(queryOptions), | |
350 | AbuseModel.internalListForApi(queryOptions) | |
351 | ]) | |
352 | ||
353 | return { total, data } | |
55fa55a9 C |
354 | } |
355 | ||
d95d1559 C |
356 | toFormattedJSON (this: MAbuseFormattable): Abuse { |
357 | const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | |
4f32032f | 358 | |
5fd4ca00 RK |
359 | const countReportsForVideo = this.get('countReportsForVideo') as number |
360 | const nthReportForVideo = this.get('nthReportForVideo') as number | |
4f32032f C |
361 | |
362 | const countReportsForReporter = this.get('countReportsForReporter') as number | |
363 | const countReportsForReportee = this.get('countReportsForReportee') as number | |
5fd4ca00 | 364 | |
310b5219 C |
365 | let video: VideoAbuse = null |
366 | let comment: VideoCommentAbuse = null | |
d95d1559 C |
367 | |
368 | if (this.VideoAbuse) { | |
369 | const abuseModel = this.VideoAbuse | |
370 | const entity = abuseModel.Video || abuseModel.deletedVideo | |
371 | ||
372 | video = { | |
373 | id: entity.id, | |
374 | uuid: entity.uuid, | |
375 | name: entity.name, | |
376 | nsfw: entity.nsfw, | |
377 | ||
378 | startAt: abuseModel.startAt, | |
379 | endAt: abuseModel.endAt, | |
380 | ||
381 | deleted: !abuseModel.Video, | |
382 | blacklisted: abuseModel.Video?.isBlacklisted() || false, | |
383 | thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), | |
4f32032f C |
384 | |
385 | channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel, | |
386 | ||
387 | countReports: countReportsForVideo, | |
388 | nthReport: nthReportForVideo | |
d95d1559 C |
389 | } |
390 | } | |
68d19a0a | 391 | |
57f6896f C |
392 | if (this.VideoCommentAbuse) { |
393 | const abuseModel = this.VideoCommentAbuse | |
310b5219 | 394 | const entity = abuseModel.VideoComment |
57f6896f C |
395 | |
396 | comment = { | |
397 | id: entity.id, | |
8ca56654 C |
398 | threadId: entity.getThreadId(), |
399 | ||
310b5219 | 400 | text: entity.text ?? '', |
57f6896f | 401 | |
310b5219 | 402 | deleted: entity.isDeleted(), |
57f6896f C |
403 | |
404 | video: { | |
405 | id: entity.Video.id, | |
406 | name: entity.Video.name, | |
407 | uuid: entity.Video.uuid | |
408 | } | |
409 | } | |
410 | } | |
411 | ||
3fd3ab2d C |
412 | return { |
413 | id: this.id, | |
414 | reason: this.reason, | |
1ebddadd | 415 | predefinedReasons, |
d95d1559 | 416 | |
4f32032f C |
417 | reporterAccount: this.ReporterAccount |
418 | ? this.ReporterAccount.toFormattedJSON() | |
419 | : null, | |
420 | ||
421 | flaggedAccount: this.FlaggedAccount | |
422 | ? this.FlaggedAccount.toFormattedJSON() | |
423 | : null, | |
d95d1559 | 424 | |
268eebed C |
425 | state: { |
426 | id: this.state, | |
d95d1559 | 427 | label: AbuseModel.getStateLabel(this.state) |
268eebed | 428 | }, |
d95d1559 | 429 | |
268eebed | 430 | moderationComment: this.moderationComment, |
d95d1559 C |
431 | |
432 | video, | |
57f6896f | 433 | comment, |
d95d1559 | 434 | |
5fd4ca00 RK |
435 | createdAt: this.createdAt, |
436 | updatedAt: this.updatedAt, | |
4f32032f C |
437 | |
438 | countReportsForReporter: (countReportsForReporter || 0), | |
439 | countReportsForReportee: (countReportsForReportee || 0), | |
d95d1559 C |
440 | |
441 | // FIXME: deprecated in 2.3, remove this | |
442 | startAt: null, | |
4f32032f C |
443 | endAt: null, |
444 | count: countReportsForVideo || 0, | |
445 | nth: nthReportForVideo || 0 | |
3fd3ab2d C |
446 | } |
447 | } | |
448 | ||
d95d1559 C |
449 | toActivityPubObject (this: MAbuseAP): AbuseObject { |
450 | const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | |
451 | ||
452 | const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url | |
1ebddadd | 453 | |
d95d1559 C |
454 | const startAt = this.VideoAbuse?.startAt |
455 | const endAt = this.VideoAbuse?.endAt | |
1ebddadd | 456 | |
3fd3ab2d C |
457 | return { |
458 | type: 'Flag' as 'Flag', | |
459 | content: this.reason, | |
d95d1559 | 460 | object, |
1ebddadd RK |
461 | tag: predefinedReasons.map(r => ({ |
462 | type: 'Hashtag' as 'Hashtag', | |
463 | name: r | |
464 | })), | |
465 | startAt, | |
466 | endAt | |
3fd3ab2d C |
467 | } |
468 | } | |
268eebed | 469 | |
811cef14 C |
470 | private static async internalCountForApi (parameters: BuildAbusesQueryOptions) { |
471 | const { query, replacements } = buildAbuseListQuery(parameters, 'count') | |
472 | const options = { | |
473 | type: QueryTypes.SELECT as QueryTypes.SELECT, | |
474 | replacements | |
475 | } | |
476 | ||
477 | const [ { total } ] = await AbuseModel.sequelize.query<{ total: string }>(query, options) | |
478 | if (total === null) return 0 | |
479 | ||
480 | return parseInt(total, 10) | |
481 | } | |
482 | ||
483 | private static async internalListForApi (parameters: BuildAbusesQueryOptions) { | |
484 | const { query, replacements } = buildAbuseListQuery(parameters, 'id') | |
485 | const options = { | |
486 | type: QueryTypes.SELECT as QueryTypes.SELECT, | |
487 | replacements | |
488 | } | |
489 | ||
490 | const rows = await AbuseModel.sequelize.query<{ id: string }>(query, options) | |
491 | const ids = rows.map(r => r.id) | |
492 | ||
493 | if (ids.length === 0) return [] | |
494 | ||
495 | return AbuseModel.scope(ScopeNames.FOR_API) | |
496 | .findAll({ | |
497 | order: getSort(parameters.sort), | |
498 | where: { | |
499 | id: { | |
500 | [Op.in]: ids | |
501 | } | |
502 | } | |
503 | }) | |
504 | } | |
505 | ||
268eebed | 506 | private static getStateLabel (id: number) { |
d95d1559 | 507 | return ABUSE_STATES[id] || 'Unknown' |
268eebed | 508 | } |
1ebddadd | 509 | |
d95d1559 | 510 | private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] { |
1ebddadd | 511 | return (predefinedReasons || []) |
d95d1559 C |
512 | .filter(r => r in AbusePredefinedReasons) |
513 | .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString) | |
1ebddadd | 514 | } |
55fa55a9 | 515 | } |