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