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