diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/models/abuse | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/models/abuse')
-rw-r--r-- | server/models/abuse/abuse-message.ts | 114 | ||||
-rw-r--r-- | server/models/abuse/abuse.ts | 624 | ||||
-rw-r--r-- | server/models/abuse/sql/abuse-query-builder.ts | 167 | ||||
-rw-r--r-- | server/models/abuse/video-abuse.ts | 64 | ||||
-rw-r--r-- | server/models/abuse/video-comment-abuse.ts | 48 |
5 files changed, 0 insertions, 1017 deletions
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts deleted file mode 100644 index 14a5bffa2..000000000 --- a/server/models/abuse/abuse-message.ts +++ /dev/null | |||
@@ -1,114 +0,0 @@ | |||
1 | import { FindOptions } from 'sequelize' | ||
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' | ||
4 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | ||
5 | import { AbuseMessage } from '@shared/models' | ||
6 | import { AttributesOnly } from '@shared/typescript-utils' | ||
7 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | ||
8 | import { getSort, throwIfNotValid } from '../shared' | ||
9 | import { AbuseModel } from './abuse' | ||
10 | |||
11 | @Table({ | ||
12 | tableName: 'abuseMessage', | ||
13 | indexes: [ | ||
14 | { | ||
15 | fields: [ 'abuseId' ] | ||
16 | }, | ||
17 | { | ||
18 | fields: [ 'accountId' ] | ||
19 | } | ||
20 | ] | ||
21 | }) | ||
22 | export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessageModel>>> { | ||
23 | |||
24 | @AllowNull(false) | ||
25 | @Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message')) | ||
26 | @Column(DataType.TEXT) | ||
27 | message: string | ||
28 | |||
29 | @AllowNull(false) | ||
30 | @Column | ||
31 | byModerator: boolean | ||
32 | |||
33 | @CreatedAt | ||
34 | createdAt: Date | ||
35 | |||
36 | @UpdatedAt | ||
37 | updatedAt: Date | ||
38 | |||
39 | @ForeignKey(() => AccountModel) | ||
40 | @Column | ||
41 | accountId: number | ||
42 | |||
43 | @BelongsTo(() => AccountModel, { | ||
44 | foreignKey: { | ||
45 | name: 'accountId', | ||
46 | allowNull: true | ||
47 | }, | ||
48 | onDelete: 'set null' | ||
49 | }) | ||
50 | Account: AccountModel | ||
51 | |||
52 | @ForeignKey(() => AbuseModel) | ||
53 | @Column | ||
54 | abuseId: number | ||
55 | |||
56 | @BelongsTo(() => AbuseModel, { | ||
57 | foreignKey: { | ||
58 | name: 'abuseId', | ||
59 | allowNull: false | ||
60 | }, | ||
61 | onDelete: 'cascade' | ||
62 | }) | ||
63 | Abuse: AbuseModel | ||
64 | |||
65 | static listForApi (abuseId: number) { | ||
66 | const getQuery = (forCount: boolean) => { | ||
67 | const query: FindOptions = { | ||
68 | where: { abuseId }, | ||
69 | order: getSort('createdAt') | ||
70 | } | ||
71 | |||
72 | if (forCount !== true) { | ||
73 | query.include = [ | ||
74 | { | ||
75 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
76 | required: false | ||
77 | } | ||
78 | ] | ||
79 | } | ||
80 | |||
81 | return query | ||
82 | } | ||
83 | |||
84 | return Promise.all([ | ||
85 | AbuseMessageModel.count(getQuery(true)), | ||
86 | AbuseMessageModel.findAll(getQuery(false)) | ||
87 | ]).then(([ total, data ]) => ({ total, data })) | ||
88 | } | ||
89 | |||
90 | static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> { | ||
91 | return AbuseMessageModel.findOne({ | ||
92 | where: { | ||
93 | id: messageId, | ||
94 | abuseId | ||
95 | } | ||
96 | }) | ||
97 | } | ||
98 | |||
99 | toFormattedJSON (this: MAbuseMessageFormattable): AbuseMessage { | ||
100 | const account = this.Account | ||
101 | ? this.Account.toFormattedSummaryJSON() | ||
102 | : null | ||
103 | |||
104 | return { | ||
105 | id: this.id, | ||
106 | createdAt: this.createdAt, | ||
107 | |||
108 | byModerator: this.byModerator, | ||
109 | message: this.message, | ||
110 | |||
111 | account | ||
112 | } | ||
113 | } | ||
114 | } | ||
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts deleted file mode 100644 index 4ce40bf2f..000000000 --- a/server/models/abuse/abuse.ts +++ /dev/null | |||
@@ -1,624 +0,0 @@ | |||
1 | import { invert } from 'lodash' | ||
2 | import { literal, Op, QueryTypes } from 'sequelize' | ||
3 | import { | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | Default, | ||
10 | ForeignKey, | ||
11 | HasOne, | ||
12 | Is, | ||
13 | Model, | ||
14 | Scopes, | ||
15 | Table, | ||
16 | UpdatedAt | ||
17 | } from 'sequelize-typescript' | ||
18 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' | ||
19 | import { abusePredefinedReasonsMap } from '@shared/core-utils' | ||
20 | import { | ||
21 | AbuseFilter, | ||
22 | AbuseObject, | ||
23 | AbusePredefinedReasons, | ||
24 | AbusePredefinedReasonsString, | ||
25 | AbuseState, | ||
26 | AbuseVideoIs, | ||
27 | AdminAbuse, | ||
28 | AdminVideoAbuse, | ||
29 | AdminVideoCommentAbuse, | ||
30 | UserAbuse, | ||
31 | UserVideoAbuse | ||
32 | } from '@shared/models' | ||
33 | import { AttributesOnly } from '@shared/typescript-utils' | ||
34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
35 | import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' | ||
36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | ||
37 | import { getSort, throwIfNotValid } from '../shared' | ||
38 | import { ThumbnailModel } from '../video/thumbnail' | ||
39 | import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' | ||
40 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' | ||
42 | import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment' | ||
43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './sql/abuse-query-builder' | ||
44 | import { VideoAbuseModel } from './video-abuse' | ||
45 | import { VideoCommentAbuseModel } from './video-comment-abuse' | ||
46 | |||
47 | export enum ScopeNames { | ||
48 | FOR_API = 'FOR_API' | ||
49 | } | ||
50 | |||
51 | @Scopes(() => ({ | ||
52 | [ScopeNames.FOR_API]: () => { | ||
53 | return { | ||
54 | attributes: { | ||
55 | include: [ | ||
56 | [ | ||
57 | literal( | ||
58 | '(' + | ||
59 | 'SELECT count(*) ' + | ||
60 | 'FROM "abuseMessage" ' + | ||
61 | 'WHERE "abuseId" = "AbuseModel"."id"' + | ||
62 | ')' | ||
63 | ), | ||
64 | 'countMessages' | ||
65 | ], | ||
66 | [ | ||
67 | // we don't care about this count for deleted videos, so there are not included | ||
68 | literal( | ||
69 | '(' + | ||
70 | 'SELECT count(*) ' + | ||
71 | 'FROM "videoAbuse" ' + | ||
72 | 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' + | ||
73 | ')' | ||
74 | ), | ||
75 | 'countReportsForVideo' | ||
76 | ], | ||
77 | [ | ||
78 | // we don't care about this count for deleted videos, so there are not included | ||
79 | literal( | ||
80 | '(' + | ||
81 | 'SELECT t.nth ' + | ||
82 | 'FROM ( ' + | ||
83 | 'SELECT id, ' + | ||
84 | 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + | ||
85 | 'FROM "videoAbuse" ' + | ||
86 | ') t ' + | ||
87 | 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' + | ||
88 | ')' | ||
89 | ), | ||
90 | 'nthReportForVideo' | ||
91 | ], | ||
92 | [ | ||
93 | literal( | ||
94 | '(' + | ||
95 | 'SELECT count("abuse"."id") ' + | ||
96 | 'FROM "abuse" ' + | ||
97 | 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' + | ||
98 | ')' | ||
99 | ), | ||
100 | 'countReportsForReporter' | ||
101 | ], | ||
102 | [ | ||
103 | literal( | ||
104 | '(' + | ||
105 | 'SELECT count("abuse"."id") ' + | ||
106 | 'FROM "abuse" ' + | ||
107 | 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' + | ||
108 | ')' | ||
109 | ), | ||
110 | 'countReportsForReportee' | ||
111 | ] | ||
112 | ] | ||
113 | }, | ||
114 | include: [ | ||
115 | { | ||
116 | model: AccountModel.scope({ | ||
117 | method: [ | ||
118 | AccountScopeNames.SUMMARY, | ||
119 | { actorRequired: false } as AccountSummaryOptions | ||
120 | ] | ||
121 | }), | ||
122 | as: 'ReporterAccount' | ||
123 | }, | ||
124 | { | ||
125 | model: AccountModel.scope({ | ||
126 | method: [ | ||
127 | AccountScopeNames.SUMMARY, | ||
128 | { actorRequired: false } as AccountSummaryOptions | ||
129 | ] | ||
130 | }), | ||
131 | as: 'FlaggedAccount' | ||
132 | }, | ||
133 | { | ||
134 | model: VideoCommentAbuseModel.unscoped(), | ||
135 | include: [ | ||
136 | { | ||
137 | model: VideoCommentModel.unscoped(), | ||
138 | include: [ | ||
139 | { | ||
140 | model: VideoModel.unscoped(), | ||
141 | attributes: [ 'name', 'id', 'uuid' ] | ||
142 | } | ||
143 | ] | ||
144 | } | ||
145 | ] | ||
146 | }, | ||
147 | { | ||
148 | model: VideoAbuseModel.unscoped(), | ||
149 | include: [ | ||
150 | { | ||
151 | attributes: [ 'id', 'uuid', 'name', 'nsfw' ], | ||
152 | model: VideoModel.unscoped(), | ||
153 | include: [ | ||
154 | { | ||
155 | attributes: [ 'filename', 'fileUrl', 'type' ], | ||
156 | model: ThumbnailModel | ||
157 | }, | ||
158 | { | ||
159 | model: VideoChannelModel.scope({ | ||
160 | method: [ | ||
161 | VideoChannelScopeNames.SUMMARY, | ||
162 | { withAccount: false, actorRequired: false } as ChannelSummaryOptions | ||
163 | ] | ||
164 | }), | ||
165 | required: false | ||
166 | }, | ||
167 | { | ||
168 | attributes: [ 'id', 'reason', 'unfederated' ], | ||
169 | required: false, | ||
170 | model: VideoBlacklistModel | ||
171 | } | ||
172 | ] | ||
173 | } | ||
174 | ] | ||
175 | } | ||
176 | ] | ||
177 | } | ||
178 | } | ||
179 | })) | ||
180 | @Table({ | ||
181 | tableName: 'abuse', | ||
182 | indexes: [ | ||
183 | { | ||
184 | fields: [ 'reporterAccountId' ] | ||
185 | }, | ||
186 | { | ||
187 | fields: [ 'flaggedAccountId' ] | ||
188 | } | ||
189 | ] | ||
190 | }) | ||
191 | export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> { | ||
192 | |||
193 | @AllowNull(false) | ||
194 | @Default(null) | ||
195 | @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) | ||
196 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max)) | ||
197 | reason: string | ||
198 | |||
199 | @AllowNull(false) | ||
200 | @Default(null) | ||
201 | @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) | ||
202 | @Column | ||
203 | state: AbuseState | ||
204 | |||
205 | @AllowNull(true) | ||
206 | @Default(null) | ||
207 | @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true)) | ||
208 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max)) | ||
209 | moderationComment: string | ||
210 | |||
211 | @AllowNull(true) | ||
212 | @Default(null) | ||
213 | @Column(DataType.ARRAY(DataType.INTEGER)) | ||
214 | predefinedReasons: AbusePredefinedReasons[] | ||
215 | |||
216 | @CreatedAt | ||
217 | createdAt: Date | ||
218 | |||
219 | @UpdatedAt | ||
220 | updatedAt: Date | ||
221 | |||
222 | @ForeignKey(() => AccountModel) | ||
223 | @Column | ||
224 | reporterAccountId: number | ||
225 | |||
226 | @BelongsTo(() => AccountModel, { | ||
227 | foreignKey: { | ||
228 | name: 'reporterAccountId', | ||
229 | allowNull: true | ||
230 | }, | ||
231 | as: 'ReporterAccount', | ||
232 | onDelete: 'set null' | ||
233 | }) | ||
234 | ReporterAccount: AccountModel | ||
235 | |||
236 | @ForeignKey(() => AccountModel) | ||
237 | @Column | ||
238 | flaggedAccountId: number | ||
239 | |||
240 | @BelongsTo(() => AccountModel, { | ||
241 | foreignKey: { | ||
242 | name: 'flaggedAccountId', | ||
243 | allowNull: true | ||
244 | }, | ||
245 | as: 'FlaggedAccount', | ||
246 | onDelete: 'set null' | ||
247 | }) | ||
248 | FlaggedAccount: AccountModel | ||
249 | |||
250 | @HasOne(() => VideoCommentAbuseModel, { | ||
251 | foreignKey: { | ||
252 | name: 'abuseId', | ||
253 | allowNull: false | ||
254 | }, | ||
255 | onDelete: 'cascade' | ||
256 | }) | ||
257 | VideoCommentAbuse: VideoCommentAbuseModel | ||
258 | |||
259 | @HasOne(() => VideoAbuseModel, { | ||
260 | foreignKey: { | ||
261 | name: 'abuseId', | ||
262 | allowNull: false | ||
263 | }, | ||
264 | onDelete: 'cascade' | ||
265 | }) | ||
266 | VideoAbuse: VideoAbuseModel | ||
267 | |||
268 | static loadByIdWithReporter (id: number): Promise<MAbuseReporter> { | ||
269 | const query = { | ||
270 | where: { | ||
271 | id | ||
272 | }, | ||
273 | include: [ | ||
274 | { | ||
275 | model: AccountModel, | ||
276 | as: 'ReporterAccount' | ||
277 | } | ||
278 | ] | ||
279 | } | ||
280 | |||
281 | return AbuseModel.findOne(query) | ||
282 | } | ||
283 | |||
284 | static loadFull (id: number): Promise<MAbuseFull> { | ||
285 | const query = { | ||
286 | where: { | ||
287 | id | ||
288 | }, | ||
289 | include: [ | ||
290 | { | ||
291 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
292 | required: false, | ||
293 | as: 'ReporterAccount' | ||
294 | }, | ||
295 | { | ||
296 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
297 | as: 'FlaggedAccount' | ||
298 | }, | ||
299 | { | ||
300 | model: VideoAbuseModel, | ||
301 | required: false, | ||
302 | include: [ | ||
303 | { | ||
304 | model: VideoModel.scope([ VideoScopeNames.WITH_ACCOUNT_DETAILS ]) | ||
305 | } | ||
306 | ] | ||
307 | }, | ||
308 | { | ||
309 | model: VideoCommentAbuseModel, | ||
310 | required: false, | ||
311 | include: [ | ||
312 | { | ||
313 | model: VideoCommentModel.scope([ | ||
314 | CommentScopeNames.WITH_ACCOUNT | ||
315 | ]), | ||
316 | include: [ | ||
317 | { | ||
318 | model: VideoModel | ||
319 | } | ||
320 | ] | ||
321 | } | ||
322 | ] | ||
323 | } | ||
324 | ] | ||
325 | } | ||
326 | |||
327 | return AbuseModel.findOne(query) | ||
328 | } | ||
329 | |||
330 | static async listForAdminApi (parameters: { | ||
331 | start: number | ||
332 | count: number | ||
333 | sort: string | ||
334 | |||
335 | filter?: AbuseFilter | ||
336 | |||
337 | serverAccountId: number | ||
338 | user?: MUserAccountId | ||
339 | |||
340 | id?: number | ||
341 | predefinedReason?: AbusePredefinedReasonsString | ||
342 | state?: AbuseState | ||
343 | videoIs?: AbuseVideoIs | ||
344 | |||
345 | search?: string | ||
346 | searchReporter?: string | ||
347 | searchReportee?: string | ||
348 | searchVideo?: string | ||
349 | searchVideoChannel?: string | ||
350 | }) { | ||
351 | const { | ||
352 | start, | ||
353 | count, | ||
354 | sort, | ||
355 | search, | ||
356 | user, | ||
357 | serverAccountId, | ||
358 | state, | ||
359 | videoIs, | ||
360 | predefinedReason, | ||
361 | searchReportee, | ||
362 | searchVideo, | ||
363 | filter, | ||
364 | searchVideoChannel, | ||
365 | searchReporter, | ||
366 | id | ||
367 | } = parameters | ||
368 | |||
369 | const userAccountId = user ? user.Account.id : undefined | ||
370 | const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined | ||
371 | |||
372 | const queryOptions: BuildAbusesQueryOptions = { | ||
373 | start, | ||
374 | count, | ||
375 | sort, | ||
376 | id, | ||
377 | filter, | ||
378 | predefinedReasonId, | ||
379 | search, | ||
380 | state, | ||
381 | videoIs, | ||
382 | searchReportee, | ||
383 | searchVideo, | ||
384 | searchVideoChannel, | ||
385 | searchReporter, | ||
386 | serverAccountId, | ||
387 | userAccountId | ||
388 | } | ||
389 | |||
390 | const [ total, data ] = await Promise.all([ | ||
391 | AbuseModel.internalCountForApi(queryOptions), | ||
392 | AbuseModel.internalListForApi(queryOptions) | ||
393 | ]) | ||
394 | |||
395 | return { total, data } | ||
396 | } | ||
397 | |||
398 | static async listForUserApi (parameters: { | ||
399 | user: MUserAccountId | ||
400 | |||
401 | start: number | ||
402 | count: number | ||
403 | sort: string | ||
404 | |||
405 | id?: number | ||
406 | search?: string | ||
407 | state?: AbuseState | ||
408 | }) { | ||
409 | const { | ||
410 | start, | ||
411 | count, | ||
412 | sort, | ||
413 | search, | ||
414 | user, | ||
415 | state, | ||
416 | id | ||
417 | } = parameters | ||
418 | |||
419 | const queryOptions: BuildAbusesQueryOptions = { | ||
420 | start, | ||
421 | count, | ||
422 | sort, | ||
423 | id, | ||
424 | search, | ||
425 | state, | ||
426 | reporterAccountId: user.Account.id | ||
427 | } | ||
428 | |||
429 | const [ total, data ] = await Promise.all([ | ||
430 | AbuseModel.internalCountForApi(queryOptions), | ||
431 | AbuseModel.internalListForApi(queryOptions) | ||
432 | ]) | ||
433 | |||
434 | return { total, data } | ||
435 | } | ||
436 | |||
437 | buildBaseVideoCommentAbuse (this: MAbuseUserFormattable) { | ||
438 | // Associated video comment could have been destroyed if the video has been deleted | ||
439 | if (!this.VideoCommentAbuse?.VideoComment) return null | ||
440 | |||
441 | const entity = this.VideoCommentAbuse.VideoComment | ||
442 | |||
443 | return { | ||
444 | id: entity.id, | ||
445 | threadId: entity.getThreadId(), | ||
446 | |||
447 | text: entity.text ?? '', | ||
448 | |||
449 | deleted: entity.isDeleted(), | ||
450 | |||
451 | video: { | ||
452 | id: entity.Video.id, | ||
453 | name: entity.Video.name, | ||
454 | uuid: entity.Video.uuid | ||
455 | } | ||
456 | } | ||
457 | } | ||
458 | |||
459 | buildBaseVideoAbuse (this: MAbuseUserFormattable): UserVideoAbuse { | ||
460 | if (!this.VideoAbuse) return null | ||
461 | |||
462 | const abuseModel = this.VideoAbuse | ||
463 | const entity = abuseModel.Video || abuseModel.deletedVideo | ||
464 | |||
465 | return { | ||
466 | id: entity.id, | ||
467 | uuid: entity.uuid, | ||
468 | name: entity.name, | ||
469 | nsfw: entity.nsfw, | ||
470 | |||
471 | startAt: abuseModel.startAt, | ||
472 | endAt: abuseModel.endAt, | ||
473 | |||
474 | deleted: !abuseModel.Video, | ||
475 | blacklisted: abuseModel.Video?.isBlacklisted() || false, | ||
476 | thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), | ||
477 | |||
478 | channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel | ||
479 | } | ||
480 | } | ||
481 | |||
482 | buildBaseAbuse (this: MAbuseUserFormattable, countMessages: number): UserAbuse { | ||
483 | const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
484 | |||
485 | return { | ||
486 | id: this.id, | ||
487 | reason: this.reason, | ||
488 | predefinedReasons, | ||
489 | |||
490 | flaggedAccount: this.FlaggedAccount | ||
491 | ? this.FlaggedAccount.toFormattedJSON() | ||
492 | : null, | ||
493 | |||
494 | state: { | ||
495 | id: this.state, | ||
496 | label: AbuseModel.getStateLabel(this.state) | ||
497 | }, | ||
498 | |||
499 | countMessages, | ||
500 | |||
501 | createdAt: this.createdAt, | ||
502 | updatedAt: this.updatedAt | ||
503 | } | ||
504 | } | ||
505 | |||
506 | toFormattedAdminJSON (this: MAbuseAdminFormattable): AdminAbuse { | ||
507 | const countReportsForVideo = this.get('countReportsForVideo') as number | ||
508 | const nthReportForVideo = this.get('nthReportForVideo') as number | ||
509 | |||
510 | const countReportsForReporter = this.get('countReportsForReporter') as number | ||
511 | const countReportsForReportee = this.get('countReportsForReportee') as number | ||
512 | |||
513 | const countMessages = this.get('countMessages') as number | ||
514 | |||
515 | const baseVideo = this.buildBaseVideoAbuse() | ||
516 | const video: AdminVideoAbuse = baseVideo | ||
517 | ? Object.assign(baseVideo, { | ||
518 | countReports: countReportsForVideo, | ||
519 | nthReport: nthReportForVideo | ||
520 | }) | ||
521 | : null | ||
522 | |||
523 | const comment: AdminVideoCommentAbuse = this.buildBaseVideoCommentAbuse() | ||
524 | |||
525 | const abuse = this.buildBaseAbuse(countMessages || 0) | ||
526 | |||
527 | return Object.assign(abuse, { | ||
528 | video, | ||
529 | comment, | ||
530 | |||
531 | moderationComment: this.moderationComment, | ||
532 | |||
533 | reporterAccount: this.ReporterAccount | ||
534 | ? this.ReporterAccount.toFormattedJSON() | ||
535 | : null, | ||
536 | |||
537 | countReportsForReporter: (countReportsForReporter || 0), | ||
538 | countReportsForReportee: (countReportsForReportee || 0) | ||
539 | }) | ||
540 | } | ||
541 | |||
542 | toFormattedUserJSON (this: MAbuseUserFormattable): UserAbuse { | ||
543 | const countMessages = this.get('countMessages') as number | ||
544 | |||
545 | const video = this.buildBaseVideoAbuse() | ||
546 | const comment = this.buildBaseVideoCommentAbuse() | ||
547 | const abuse = this.buildBaseAbuse(countMessages || 0) | ||
548 | |||
549 | return Object.assign(abuse, { | ||
550 | video, | ||
551 | comment | ||
552 | }) | ||
553 | } | ||
554 | |||
555 | toActivityPubObject (this: MAbuseAP): AbuseObject { | ||
556 | const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
557 | |||
558 | const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url | ||
559 | |||
560 | const startAt = this.VideoAbuse?.startAt | ||
561 | const endAt = this.VideoAbuse?.endAt | ||
562 | |||
563 | return { | ||
564 | type: 'Flag' as 'Flag', | ||
565 | content: this.reason, | ||
566 | mediaType: 'text/markdown', | ||
567 | object, | ||
568 | tag: predefinedReasons.map(r => ({ | ||
569 | type: 'Hashtag' as 'Hashtag', | ||
570 | name: r | ||
571 | })), | ||
572 | startAt, | ||
573 | endAt | ||
574 | } | ||
575 | } | ||
576 | |||
577 | private static async internalCountForApi (parameters: BuildAbusesQueryOptions) { | ||
578 | const { query, replacements } = buildAbuseListQuery(parameters, 'count') | ||
579 | const options = { | ||
580 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
581 | replacements | ||
582 | } | ||
583 | |||
584 | const [ { total } ] = await AbuseModel.sequelize.query<{ total: string }>(query, options) | ||
585 | if (total === null) return 0 | ||
586 | |||
587 | return parseInt(total, 10) | ||
588 | } | ||
589 | |||
590 | private static async internalListForApi (parameters: BuildAbusesQueryOptions) { | ||
591 | const { query, replacements } = buildAbuseListQuery(parameters, 'id') | ||
592 | const options = { | ||
593 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
594 | replacements | ||
595 | } | ||
596 | |||
597 | const rows = await AbuseModel.sequelize.query<{ id: string }>(query, options) | ||
598 | const ids = rows.map(r => r.id) | ||
599 | |||
600 | if (ids.length === 0) return [] | ||
601 | |||
602 | return AbuseModel.scope(ScopeNames.FOR_API) | ||
603 | .findAll({ | ||
604 | order: getSort(parameters.sort), | ||
605 | where: { | ||
606 | id: { | ||
607 | [Op.in]: ids | ||
608 | } | ||
609 | } | ||
610 | }) | ||
611 | } | ||
612 | |||
613 | private static getStateLabel (id: number) { | ||
614 | return ABUSE_STATES[id] || 'Unknown' | ||
615 | } | ||
616 | |||
617 | private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] { | ||
618 | const invertedPredefinedReasons = invert(abusePredefinedReasonsMap) | ||
619 | |||
620 | return (predefinedReasons || []) | ||
621 | .map(r => invertedPredefinedReasons[r] as AbusePredefinedReasonsString) | ||
622 | .filter(v => !!v) | ||
623 | } | ||
624 | } | ||
diff --git a/server/models/abuse/sql/abuse-query-builder.ts b/server/models/abuse/sql/abuse-query-builder.ts deleted file mode 100644 index 282d4541a..000000000 --- a/server/models/abuse/sql/abuse-query-builder.ts +++ /dev/null | |||
@@ -1,167 +0,0 @@ | |||
1 | |||
2 | import { exists } from '@server/helpers/custom-validators/misc' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' | ||
5 | import { buildBlockedAccountSQL, buildSortDirectionAndField } from '../../shared' | ||
6 | |||
7 | export type BuildAbusesQueryOptions = { | ||
8 | start: number | ||
9 | count: number | ||
10 | sort: string | ||
11 | |||
12 | // search | ||
13 | search?: string | ||
14 | searchReporter?: string | ||
15 | searchReportee?: string | ||
16 | |||
17 | // video related | ||
18 | searchVideo?: string | ||
19 | searchVideoChannel?: string | ||
20 | videoIs?: AbuseVideoIs | ||
21 | |||
22 | // filters | ||
23 | id?: number | ||
24 | predefinedReasonId?: number | ||
25 | filter?: AbuseFilter | ||
26 | |||
27 | state?: AbuseState | ||
28 | |||
29 | // accountIds | ||
30 | serverAccountId?: number | ||
31 | userAccountId?: number | ||
32 | |||
33 | reporterAccountId?: number | ||
34 | } | ||
35 | |||
36 | function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | 'id') { | ||
37 | const whereAnd: string[] = [] | ||
38 | const replacements: any = {} | ||
39 | |||
40 | const joins = [ | ||
41 | 'LEFT JOIN "videoAbuse" ON "videoAbuse"."abuseId" = "abuse"."id"', | ||
42 | 'LEFT JOIN "video" ON "videoAbuse"."videoId" = "video"."id"', | ||
43 | 'LEFT JOIN "videoBlacklist" ON "videoBlacklist"."videoId" = "video"."id"', | ||
44 | 'LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"', | ||
45 | 'LEFT JOIN "account" "reporterAccount" ON "reporterAccount"."id" = "abuse"."reporterAccountId"', | ||
46 | 'LEFT JOIN "account" "flaggedAccount" ON "flaggedAccount"."id" = "abuse"."flaggedAccountId"', | ||
47 | 'LEFT JOIN "commentAbuse" ON "commentAbuse"."abuseId" = "abuse"."id"', | ||
48 | 'LEFT JOIN "videoComment" ON "commentAbuse"."videoCommentId" = "videoComment"."id"' | ||
49 | ] | ||
50 | |||
51 | if (options.serverAccountId || options.userAccountId) { | ||
52 | whereAnd.push( | ||
53 | '"abuse"."reporterAccountId" IS NULL OR ' + | ||
54 | '"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')' | ||
55 | ) | ||
56 | } | ||
57 | |||
58 | if (options.reporterAccountId) { | ||
59 | whereAnd.push('"abuse"."reporterAccountId" = :reporterAccountId') | ||
60 | replacements.reporterAccountId = options.reporterAccountId | ||
61 | } | ||
62 | |||
63 | if (options.search) { | ||
64 | const searchWhereOr = [ | ||
65 | '"video"."name" ILIKE :search', | ||
66 | '"videoChannel"."name" ILIKE :search', | ||
67 | `"videoAbuse"."deletedVideo"->>'name' ILIKE :search`, | ||
68 | `"videoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE :search`, | ||
69 | '"reporterAccount"."name" ILIKE :search', | ||
70 | '"flaggedAccount"."name" ILIKE :search' | ||
71 | ] | ||
72 | |||
73 | replacements.search = `%${options.search}%` | ||
74 | whereAnd.push('(' + searchWhereOr.join(' OR ') + ')') | ||
75 | } | ||
76 | |||
77 | if (options.searchVideo) { | ||
78 | whereAnd.push('"video"."name" ILIKE :searchVideo') | ||
79 | replacements.searchVideo = `%${options.searchVideo}%` | ||
80 | } | ||
81 | |||
82 | if (options.searchVideoChannel) { | ||
83 | whereAnd.push('"videoChannel"."name" ILIKE :searchVideoChannel') | ||
84 | replacements.searchVideoChannel = `%${options.searchVideoChannel}%` | ||
85 | } | ||
86 | |||
87 | if (options.id) { | ||
88 | whereAnd.push('"abuse"."id" = :id') | ||
89 | replacements.id = options.id | ||
90 | } | ||
91 | |||
92 | if (options.state) { | ||
93 | whereAnd.push('"abuse"."state" = :state') | ||
94 | replacements.state = options.state | ||
95 | } | ||
96 | |||
97 | if (options.videoIs === 'deleted') { | ||
98 | whereAnd.push('"videoAbuse"."deletedVideo" IS NOT NULL') | ||
99 | } else if (options.videoIs === 'blacklisted') { | ||
100 | whereAnd.push('"videoBlacklist"."id" IS NOT NULL') | ||
101 | } | ||
102 | |||
103 | if (options.predefinedReasonId) { | ||
104 | whereAnd.push(':predefinedReasonId = ANY("abuse"."predefinedReasons")') | ||
105 | replacements.predefinedReasonId = options.predefinedReasonId | ||
106 | } | ||
107 | |||
108 | if (options.filter === 'video') { | ||
109 | whereAnd.push('"videoAbuse"."id" IS NOT NULL') | ||
110 | } else if (options.filter === 'comment') { | ||
111 | whereAnd.push('"commentAbuse"."id" IS NOT NULL') | ||
112 | } else if (options.filter === 'account') { | ||
113 | whereAnd.push('"videoAbuse"."id" IS NULL AND "commentAbuse"."id" IS NULL') | ||
114 | } | ||
115 | |||
116 | if (options.searchReporter) { | ||
117 | whereAnd.push('"reporterAccount"."name" ILIKE :searchReporter') | ||
118 | replacements.searchReporter = `%${options.searchReporter}%` | ||
119 | } | ||
120 | |||
121 | if (options.searchReportee) { | ||
122 | whereAnd.push('"flaggedAccount"."name" ILIKE :searchReportee') | ||
123 | replacements.searchReportee = `%${options.searchReportee}%` | ||
124 | } | ||
125 | |||
126 | const prefix = type === 'count' | ||
127 | ? 'SELECT COUNT("abuse"."id") AS "total"' | ||
128 | : 'SELECT "abuse"."id" ' | ||
129 | |||
130 | let suffix = '' | ||
131 | if (type !== 'count') { | ||
132 | |||
133 | if (options.sort) { | ||
134 | const order = buildAbuseOrder(options.sort) | ||
135 | suffix += `${order} ` | ||
136 | } | ||
137 | |||
138 | if (exists(options.count)) { | ||
139 | const count = forceNumber(options.count) | ||
140 | suffix += `LIMIT ${count} ` | ||
141 | } | ||
142 | |||
143 | if (exists(options.start)) { | ||
144 | const start = forceNumber(options.start) | ||
145 | suffix += `OFFSET ${start} ` | ||
146 | } | ||
147 | } | ||
148 | |||
149 | const where = whereAnd.length !== 0 | ||
150 | ? `WHERE ${whereAnd.join(' AND ')}` | ||
151 | : '' | ||
152 | |||
153 | return { | ||
154 | query: `${prefix} FROM "abuse" ${joins.join(' ')} ${where} ${suffix}`, | ||
155 | replacements | ||
156 | } | ||
157 | } | ||
158 | |||
159 | function buildAbuseOrder (value: string) { | ||
160 | const { direction, field } = buildSortDirectionAndField(value) | ||
161 | |||
162 | return `ORDER BY "abuse"."${field}" ${direction}` | ||
163 | } | ||
164 | |||
165 | export { | ||
166 | buildAbuseListQuery | ||
167 | } | ||
diff --git a/server/models/abuse/video-abuse.ts b/server/models/abuse/video-abuse.ts deleted file mode 100644 index 773a9ebba..000000000 --- a/server/models/abuse/video-abuse.ts +++ /dev/null | |||
@@ -1,64 +0,0 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AttributesOnly } from '@shared/typescript-utils' | ||
3 | import { VideoDetails } from '@shared/models' | ||
4 | import { VideoModel } from '../video/video' | ||
5 | import { AbuseModel } from './abuse' | ||
6 | |||
7 | @Table({ | ||
8 | tableName: 'videoAbuse', | ||
9 | indexes: [ | ||
10 | { | ||
11 | fields: [ 'abuseId' ] | ||
12 | }, | ||
13 | { | ||
14 | fields: [ 'videoId' ] | ||
15 | } | ||
16 | ] | ||
17 | }) | ||
18 | export class VideoAbuseModel extends Model<Partial<AttributesOnly<VideoAbuseModel>>> { | ||
19 | |||
20 | @CreatedAt | ||
21 | createdAt: Date | ||
22 | |||
23 | @UpdatedAt | ||
24 | updatedAt: Date | ||
25 | |||
26 | @AllowNull(true) | ||
27 | @Default(null) | ||
28 | @Column | ||
29 | startAt: number | ||
30 | |||
31 | @AllowNull(true) | ||
32 | @Default(null) | ||
33 | @Column | ||
34 | endAt: number | ||
35 | |||
36 | @AllowNull(true) | ||
37 | @Default(null) | ||
38 | @Column(DataType.JSONB) | ||
39 | deletedVideo: VideoDetails | ||
40 | |||
41 | @ForeignKey(() => AbuseModel) | ||
42 | @Column | ||
43 | abuseId: number | ||
44 | |||
45 | @BelongsTo(() => AbuseModel, { | ||
46 | foreignKey: { | ||
47 | allowNull: false | ||
48 | }, | ||
49 | onDelete: 'cascade' | ||
50 | }) | ||
51 | Abuse: AbuseModel | ||
52 | |||
53 | @ForeignKey(() => VideoModel) | ||
54 | @Column | ||
55 | videoId: number | ||
56 | |||
57 | @BelongsTo(() => VideoModel, { | ||
58 | foreignKey: { | ||
59 | allowNull: true | ||
60 | }, | ||
61 | onDelete: 'set null' | ||
62 | }) | ||
63 | Video: VideoModel | ||
64 | } | ||
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts deleted file mode 100644 index 337aaaa58..000000000 --- a/server/models/abuse/video-comment-abuse.ts +++ /dev/null | |||
@@ -1,48 +0,0 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AttributesOnly } from '@shared/typescript-utils' | ||
3 | import { VideoCommentModel } from '../video/video-comment' | ||
4 | import { AbuseModel } from './abuse' | ||
5 | |||
6 | @Table({ | ||
7 | tableName: 'commentAbuse', | ||
8 | indexes: [ | ||
9 | { | ||
10 | fields: [ 'abuseId' ] | ||
11 | }, | ||
12 | { | ||
13 | fields: [ 'videoCommentId' ] | ||
14 | } | ||
15 | ] | ||
16 | }) | ||
17 | export class VideoCommentAbuseModel extends Model<Partial<AttributesOnly<VideoCommentAbuseModel>>> { | ||
18 | |||
19 | @CreatedAt | ||
20 | createdAt: Date | ||
21 | |||
22 | @UpdatedAt | ||
23 | updatedAt: Date | ||
24 | |||
25 | @ForeignKey(() => AbuseModel) | ||
26 | @Column | ||
27 | abuseId: number | ||
28 | |||
29 | @BelongsTo(() => AbuseModel, { | ||
30 | foreignKey: { | ||
31 | allowNull: false | ||
32 | }, | ||
33 | onDelete: 'cascade' | ||
34 | }) | ||
35 | Abuse: AbuseModel | ||
36 | |||
37 | @ForeignKey(() => VideoCommentModel) | ||
38 | @Column | ||
39 | videoCommentId: number | ||
40 | |||
41 | @BelongsTo(() => VideoCommentModel, { | ||
42 | foreignKey: { | ||
43 | allowNull: true | ||
44 | }, | ||
45 | onDelete: 'set null' | ||
46 | }) | ||
47 | VideoComment: VideoCommentModel | ||
48 | } | ||