diff options
author | Chocobozzz <me@florianbigard.com> | 2020-07-07 17:18:26 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-07-10 14:02:41 +0200 |
commit | 811cef146c841ef8530bc812c05dfee77e0f2998 (patch) | |
tree | 25643c58352f0452433e25c8bc3ed4aa752b019d /server/models/abuse/abuse.ts | |
parent | 4f32032fed8587ea97d45e235b167e8958efd81f (diff) | |
download | PeerTube-811cef146c841ef8530bc812c05dfee77e0f2998.tar.gz PeerTube-811cef146c841ef8530bc812c05dfee77e0f2998.tar.zst PeerTube-811cef146c841ef8530bc812c05dfee77e0f2998.zip |
Use raw sql for abuses
Diffstat (limited to 'server/models/abuse/abuse.ts')
-rw-r--r-- | server/models/abuse/abuse.ts | 198 |
1 files changed, 63 insertions, 135 deletions
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 9c17c4d51..28ecf8253 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { invert } from 'lodash' | 2 | import { invert } from 'lodash' |
3 | import { literal, Op, WhereOptions } from 'sequelize' | 3 | import { literal, Op, QueryTypes, WhereOptions } from 'sequelize' |
4 | import { | 4 | import { |
5 | AllowNull, | 5 | AllowNull, |
6 | BelongsTo, | 6 | BelongsTo, |
@@ -32,12 +32,13 @@ import { | |||
32 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | 32 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
33 | import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' | 33 | import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' |
34 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 34 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
35 | import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' | 35 | import { getSort, throwIfNotValid } from '../utils' |
36 | import { ThumbnailModel } from '../video/thumbnail' | 36 | import { ThumbnailModel } from '../video/thumbnail' |
37 | import { VideoModel } from '../video/video' | 37 | import { VideoModel } from '../video/video' |
38 | import { VideoBlacklistModel } from '../video/video-blacklist' | 38 | import { VideoBlacklistModel } from '../video/video-blacklist' |
39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' | 39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' |
40 | import { VideoCommentModel } from '../video/video-comment' | 40 | import { VideoCommentModel } from '../video/video-comment' |
41 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' | ||
41 | import { VideoAbuseModel } from './video-abuse' | 42 | import { VideoAbuseModel } from './video-abuse' |
42 | import { VideoCommentAbuseModel } from './video-comment-abuse' | 43 | import { VideoCommentAbuseModel } from './video-comment-abuse' |
43 | 44 | ||
@@ -46,100 +47,7 @@ export enum ScopeNames { | |||
46 | } | 47 | } |
47 | 48 | ||
48 | @Scopes(() => ({ | 49 | @Scopes(() => ({ |
49 | [ScopeNames.FOR_API]: (options: { | 50 | [ScopeNames.FOR_API]: () => { |
50 | // search | ||
51 | search?: string | ||
52 | searchReporter?: string | ||
53 | searchReportee?: string | ||
54 | |||
55 | // video releated | ||
56 | searchVideo?: string | ||
57 | searchVideoChannel?: string | ||
58 | videoIs?: AbuseVideoIs | ||
59 | |||
60 | // filters | ||
61 | id?: number | ||
62 | predefinedReasonId?: number | ||
63 | filter?: AbuseFilter | ||
64 | |||
65 | state?: AbuseState | ||
66 | |||
67 | // accountIds | ||
68 | serverAccountId: number | ||
69 | userAccountId: number | ||
70 | }) => { | ||
71 | const whereAnd: WhereOptions[] = [] | ||
72 | |||
73 | whereAnd.push({ | ||
74 | reporterAccountId: { | ||
75 | [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') | ||
76 | } | ||
77 | }) | ||
78 | |||
79 | if (options.search) { | ||
80 | const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%') | ||
81 | |||
82 | whereAnd.push({ | ||
83 | [Op.or]: [ | ||
84 | { | ||
85 | [Op.and]: [ | ||
86 | { '$VideoAbuse.videoId$': { [Op.not]: null } }, | ||
87 | searchAttribute(options.search, '$VideoAbuse.Video.name$') | ||
88 | ] | ||
89 | }, | ||
90 | { | ||
91 | [Op.and]: [ | ||
92 | { '$VideoAbuse.videoId$': { [Op.not]: null } }, | ||
93 | searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$') | ||
94 | ] | ||
95 | }, | ||
96 | { | ||
97 | [Op.and]: [ | ||
98 | { '$VideoAbuse.deletedVideo$': { [Op.not]: null } }, | ||
99 | literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`) | ||
100 | ] | ||
101 | }, | ||
102 | { | ||
103 | [Op.and]: [ | ||
104 | { '$VideoAbuse.deletedVideo$': { [Op.not]: null } }, | ||
105 | literal(`"VideoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE ${escapedSearch}`) | ||
106 | ] | ||
107 | }, | ||
108 | searchAttribute(options.search, '$ReporterAccount.name$'), | ||
109 | searchAttribute(options.search, '$FlaggedAccount.name$') | ||
110 | ] | ||
111 | }) | ||
112 | } | ||
113 | |||
114 | if (options.id) whereAnd.push({ id: options.id }) | ||
115 | if (options.state) whereAnd.push({ state: options.state }) | ||
116 | |||
117 | if (options.videoIs === 'deleted') { | ||
118 | whereAnd.push({ | ||
119 | '$VideoAbuse.deletedVideo$': { | ||
120 | [Op.not]: null | ||
121 | } | ||
122 | }) | ||
123 | } | ||
124 | |||
125 | if (options.predefinedReasonId) { | ||
126 | whereAnd.push({ | ||
127 | predefinedReasons: { | ||
128 | [Op.contains]: [ options.predefinedReasonId ] | ||
129 | } | ||
130 | }) | ||
131 | } | ||
132 | |||
133 | if (options.filter === 'account') { | ||
134 | whereAnd.push({ | ||
135 | videoId: null, | ||
136 | commentId: null | ||
137 | }) | ||
138 | } | ||
139 | |||
140 | const onlyBlacklisted = options.videoIs === 'blacklisted' | ||
141 | const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel) | ||
142 | |||
143 | return { | 51 | return { |
144 | attributes: { | 52 | attributes: { |
145 | include: [ | 53 | include: [ |
@@ -193,10 +101,13 @@ export enum ScopeNames { | |||
193 | }, | 101 | }, |
194 | include: [ | 102 | include: [ |
195 | { | 103 | { |
196 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | 104 | model: AccountModel.scope({ |
197 | as: 'ReporterAccount', | 105 | method: [ |
198 | required: !!options.searchReporter, | 106 | AccountScopeNames.SUMMARY, |
199 | where: searchAttribute(options.searchReporter, 'name') | 107 | { actorRequired: false } as AccountSummaryOptions |
108 | ] | ||
109 | }), | ||
110 | as: 'ReporterAccount' | ||
200 | }, | 111 | }, |
201 | { | 112 | { |
202 | model: AccountModel.scope({ | 113 | model: AccountModel.scope({ |
@@ -205,17 +116,13 @@ export enum ScopeNames { | |||
205 | { actorRequired: false } as AccountSummaryOptions | 116 | { actorRequired: false } as AccountSummaryOptions |
206 | ] | 117 | ] |
207 | }), | 118 | }), |
208 | as: 'FlaggedAccount', | 119 | as: 'FlaggedAccount' |
209 | required: !!options.searchReportee, | ||
210 | where: searchAttribute(options.searchReportee, 'name') | ||
211 | }, | 120 | }, |
212 | { | 121 | { |
213 | model: VideoCommentAbuseModel.unscoped(), | 122 | model: VideoCommentAbuseModel.unscoped(), |
214 | required: options.filter === 'comment', | ||
215 | include: [ | 123 | include: [ |
216 | { | 124 | { |
217 | model: VideoCommentModel.unscoped(), | 125 | model: VideoCommentModel.unscoped(), |
218 | required: false, | ||
219 | include: [ | 126 | include: [ |
220 | { | 127 | { |
221 | model: VideoModel.unscoped(), | 128 | model: VideoModel.unscoped(), |
@@ -227,13 +134,10 @@ export enum ScopeNames { | |||
227 | }, | 134 | }, |
228 | { | 135 | { |
229 | model: VideoAbuseModel.unscoped(), | 136 | model: VideoAbuseModel.unscoped(), |
230 | required: options.filter === 'video' || !!options.videoIs || videoRequired, | ||
231 | include: [ | 137 | include: [ |
232 | { | 138 | { |
233 | attributes: [ 'id', 'uuid', 'name', 'nsfw' ], | 139 | attributes: [ 'id', 'uuid', 'name', 'nsfw' ], |
234 | model: VideoModel.unscoped(), | 140 | model: VideoModel.unscoped(), |
235 | required: videoRequired, | ||
236 | where: searchAttribute(options.searchVideo, 'name'), | ||
237 | include: [ | 141 | include: [ |
238 | { | 142 | { |
239 | attributes: [ 'filename', 'fileUrl' ], | 143 | attributes: [ 'filename', 'fileUrl' ], |
@@ -246,23 +150,18 @@ export enum ScopeNames { | |||
246 | { withAccount: false, actorRequired: false } as ChannelSummaryOptions | 150 | { withAccount: false, actorRequired: false } as ChannelSummaryOptions |
247 | ] | 151 | ] |
248 | }), | 152 | }), |
249 | 153 | required: false | |
250 | where: searchAttribute(options.searchVideoChannel, 'name'), | ||
251 | required: !!options.searchVideoChannel | ||
252 | }, | 154 | }, |
253 | { | 155 | { |
254 | attributes: [ 'id', 'reason', 'unfederated' ], | 156 | attributes: [ 'id', 'reason', 'unfederated' ], |
255 | model: VideoBlacklistModel, | 157 | required: false, |
256 | required: onlyBlacklisted | 158 | model: VideoBlacklistModel |
257 | } | 159 | } |
258 | ] | 160 | ] |
259 | } | 161 | } |
260 | ] | 162 | ] |
261 | } | 163 | } |
262 | ], | 164 | ] |
263 | where: { | ||
264 | [Op.and]: whereAnd | ||
265 | } | ||
266 | } | 165 | } |
267 | } | 166 | } |
268 | })) | 167 | })) |
@@ -386,7 +285,7 @@ export class AbuseModel extends Model<AbuseModel> { | |||
386 | return AbuseModel.findOne(query) | 285 | return AbuseModel.findOne(query) |
387 | } | 286 | } |
388 | 287 | ||
389 | static listForApi (parameters: { | 288 | static async listForApi (parameters: { |
390 | start: number | 289 | start: number |
391 | count: number | 290 | count: number |
392 | sort: string | 291 | sort: string |
@@ -428,15 +327,10 @@ export class AbuseModel extends Model<AbuseModel> { | |||
428 | const userAccountId = user ? user.Account.id : undefined | 327 | const userAccountId = user ? user.Account.id : undefined |
429 | const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined | 328 | const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined |
430 | 329 | ||
431 | const query = { | 330 | const queryOptions: BuildAbusesQueryOptions = { |
432 | offset: start, | 331 | start, |
433 | limit: count, | 332 | count, |
434 | order: getSort(sort), | 333 | sort, |
435 | col: 'AbuseModel.id', | ||
436 | distinct: true | ||
437 | } | ||
438 | |||
439 | const filters = { | ||
440 | id, | 334 | id, |
441 | filter, | 335 | filter, |
442 | predefinedReasonId, | 336 | predefinedReasonId, |
@@ -451,14 +345,12 @@ export class AbuseModel extends Model<AbuseModel> { | |||
451 | userAccountId | 345 | userAccountId |
452 | } | 346 | } |
453 | 347 | ||
454 | return AbuseModel | 348 | const [ total, data ] = await Promise.all([ |
455 | .scope([ | 349 | AbuseModel.internalCountForApi(queryOptions), |
456 | { method: [ ScopeNames.FOR_API, filters ] } | 350 | AbuseModel.internalListForApi(queryOptions) |
457 | ]) | 351 | ]) |
458 | .findAndCountAll(query) | 352 | |
459 | .then(({ rows, count }) => { | 353 | return { total, data } |
460 | return { total: count, data: rows } | ||
461 | }) | ||
462 | } | 354 | } |
463 | 355 | ||
464 | toFormattedJSON (this: MAbuseFormattable): Abuse { | 356 | toFormattedJSON (this: MAbuseFormattable): Abuse { |
@@ -573,6 +465,42 @@ export class AbuseModel extends Model<AbuseModel> { | |||
573 | } | 465 | } |
574 | } | 466 | } |
575 | 467 | ||
468 | private static async internalCountForApi (parameters: BuildAbusesQueryOptions) { | ||
469 | const { query, replacements } = buildAbuseListQuery(parameters, 'count') | ||
470 | const options = { | ||
471 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
472 | replacements | ||
473 | } | ||
474 | |||
475 | const [ { total } ] = await AbuseModel.sequelize.query<{ total: string }>(query, options) | ||
476 | if (total === null) return 0 | ||
477 | |||
478 | return parseInt(total, 10) | ||
479 | } | ||
480 | |||
481 | private static async internalListForApi (parameters: BuildAbusesQueryOptions) { | ||
482 | const { query, replacements } = buildAbuseListQuery(parameters, 'id') | ||
483 | const options = { | ||
484 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
485 | replacements | ||
486 | } | ||
487 | |||
488 | const rows = await AbuseModel.sequelize.query<{ id: string }>(query, options) | ||
489 | const ids = rows.map(r => r.id) | ||
490 | |||
491 | if (ids.length === 0) return [] | ||
492 | |||
493 | return AbuseModel.scope(ScopeNames.FOR_API) | ||
494 | .findAll({ | ||
495 | order: getSort(parameters.sort), | ||
496 | where: { | ||
497 | id: { | ||
498 | [Op.in]: ids | ||
499 | } | ||
500 | } | ||
501 | }) | ||
502 | } | ||
503 | |||
576 | private static getStateLabel (id: number) { | 504 | private static getStateLabel (id: number) { |
577 | return ABUSE_STATES[id] || 'Unknown' | 505 | return ABUSE_STATES[id] || 'Unknown' |
578 | } | 506 | } |