diff options
author | Chocobozzz <me@florianbigard.com> | 2020-05-06 17:39:07 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-05-07 08:33:34 +0200 |
commit | feb34f6b6b991046aab6a10df747b48fa4da07a7 (patch) | |
tree | 02bb7277d45be166ba48caef2ee73bf89dbe1258 /server/models | |
parent | d170c5c580abf6f90d7bf144e2417e248ce2ecf4 (diff) | |
download | PeerTube-feb34f6b6b991046aab6a10df747b48fa4da07a7.tar.gz PeerTube-feb34f6b6b991046aab6a10df747b48fa4da07a7.tar.zst PeerTube-feb34f6b6b991046aab6a10df747b48fa4da07a7.zip |
Use video abuse filters on client side
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/utils.ts | 60 | ||||
-rw-r--r-- | server/models/video/video-abuse.ts | 127 |
2 files changed, 73 insertions, 114 deletions
diff --git a/server/models/utils.ts b/server/models/utils.ts index fe4596d31..b2573cd35 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -207,60 +207,13 @@ function buildDirectionAndField (value: string) { | |||
207 | return { direction, field } | 207 | return { direction, field } |
208 | } | 208 | } |
209 | 209 | ||
210 | function searchAttribute (sourceField, targetField) { | 210 | function searchAttribute (sourceField?: string, targetField?: string) { |
211 | if (sourceField) { | 211 | if (!sourceField) return {} |
212 | return { | ||
213 | [targetField]: { | ||
214 | [Op.iLike]: `%${sourceField}%` | ||
215 | } | ||
216 | } | ||
217 | } else { | ||
218 | return {} | ||
219 | } | ||
220 | } | ||
221 | |||
222 | interface QueryStringFilterPrefixes { | ||
223 | [key: string]: string | { prefix: string, handler: Function, multiple?: boolean } | ||
224 | } | ||
225 | |||
226 | function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes): { | ||
227 | search: string | ||
228 | [key: string]: string | number | string[] | number[] | ||
229 | } { | ||
230 | const tokens = q // tokenize only if we have a querystring | ||
231 | ? [].concat.apply([], q.split('"').map((v, i) => i % 2 ? v : v.split(' '))).filter(Boolean) // split by space unless using double quotes | ||
232 | : [] | ||
233 | |||
234 | const objectMap = (obj, fn) => Object.fromEntries( | ||
235 | Object.entries(obj).map( | ||
236 | ([ k, v ], i) => [ k, fn(v, k, i) ] | ||
237 | ) | ||
238 | ) | ||
239 | 212 | ||
240 | return { | 213 | return { |
241 | // search is the querystring minus defined filters | 214 | [targetField]: { |
242 | search: tokens.filter(e => !Object.values(prefixes).some(p => { | 215 | [Op.iLike]: `%${sourceField}%` |
243 | if (typeof p === 'string') { | 216 | } |
244 | return e.startsWith(p) | ||
245 | } else { | ||
246 | return e.startsWith(p.prefix) | ||
247 | } | ||
248 | })).join(' '), | ||
249 | // filters defined in prefixes are added under their own name | ||
250 | ...objectMap(prefixes, p => { | ||
251 | if (typeof p === 'string') { | ||
252 | return tokens.filter(e => e.startsWith(p)).map(e => e.slice(p.length)) // we keep the matched item, and remove its prefix | ||
253 | } else { | ||
254 | const _tokens = tokens.filter(e => e.startsWith(p.prefix)).map(e => e.slice(p.prefix.length)).map(p.handler) | ||
255 | // multiple is false by default, meaning we usually just keep the first occurence of a given prefix | ||
256 | if (!p.multiple && _tokens.length > 0) { | ||
257 | return _tokens[0] | ||
258 | } else if (!p.multiple) { | ||
259 | return '' | ||
260 | } | ||
261 | return _tokens | ||
262 | } | ||
263 | }) | ||
264 | } | 217 | } |
265 | } | 218 | } |
266 | 219 | ||
@@ -286,8 +239,7 @@ export { | |||
286 | getFollowsSort, | 239 | getFollowsSort, |
287 | buildDirectionAndField, | 240 | buildDirectionAndField, |
288 | createSafeIn, | 241 | createSafeIn, |
289 | searchAttribute, | 242 | searchAttribute |
290 | parseQueryStringFilter | ||
291 | } | 243 | } |
292 | 244 | ||
293 | // --------------------------------------------------------------------------- | 245 | // --------------------------------------------------------------------------- |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index 6cd2c0418..0844f702d 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -1,6 +1,21 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { literal, Op } from 'sequelize' | ||
1 | import { | 3 | import { |
2 | AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, Scopes | 4 | AllowNull, |
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | Default, | ||
10 | ForeignKey, | ||
11 | Is, | ||
12 | Model, | ||
13 | Scopes, | ||
14 | Table, | ||
15 | UpdatedAt | ||
3 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
17 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' | ||
18 | import { VideoAbuseState, VideoDetails } from '../../../shared' | ||
4 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | 19 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' |
5 | import { VideoAbuse } from '../../../shared/models/videos' | 20 | import { VideoAbuse } from '../../../shared/models/videos' |
6 | import { | 21 | import { |
@@ -8,15 +23,12 @@ import { | |||
8 | isVideoAbuseReasonValid, | 23 | isVideoAbuseReasonValid, |
9 | isVideoAbuseStateValid | 24 | isVideoAbuseStateValid |
10 | } from '../../helpers/custom-validators/video-abuses' | 25 | } from '../../helpers/custom-validators/video-abuses' |
11 | import { AccountModel } from '../account/account' | ||
12 | import { buildBlockedAccountSQL, getSort, throwIfNotValid, searchAttribute, parseQueryStringFilter } from '../utils' | ||
13 | import { VideoModel } from './video' | ||
14 | import { VideoAbuseState, VideoDetails } from '../../../shared' | ||
15 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' | 26 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' |
16 | import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models' | 27 | import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models' |
17 | import * as Bluebird from 'bluebird' | 28 | import { AccountModel } from '../account/account' |
18 | import { literal, Op } from 'sequelize' | 29 | import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' |
19 | import { ThumbnailModel } from './thumbnail' | 30 | import { ThumbnailModel } from './thumbnail' |
31 | import { VideoModel } from './video' | ||
20 | import { VideoBlacklistModel } from './video-blacklist' | 32 | import { VideoBlacklistModel } from './video-blacklist' |
21 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | 33 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
22 | 34 | ||
@@ -35,21 +47,22 @@ export enum ScopeNames { | |||
35 | 47 | ||
36 | // filters | 48 | // filters |
37 | id?: number | 49 | id?: number |
50 | |||
38 | state?: VideoAbuseState | 51 | state?: VideoAbuseState |
39 | is?: 'deleted' | 'blacklisted' | 52 | videoIs?: VideoAbuseVideoIs |
40 | 53 | ||
41 | // accountIds | 54 | // accountIds |
42 | serverAccountId: number | 55 | serverAccountId: number |
43 | userAccountId: number | 56 | userAccountId: number |
44 | }) => { | 57 | }) => { |
45 | let where = { | 58 | const where = { |
46 | reporterAccountId: { | 59 | reporterAccountId: { |
47 | [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')') | 60 | [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')') |
48 | } | 61 | } |
49 | } | 62 | } |
50 | 63 | ||
51 | if (options.search) { | 64 | if (options.search) { |
52 | where = Object.assign(where, { | 65 | Object.assign(where, { |
53 | [Op.or]: [ | 66 | [Op.or]: [ |
54 | { | 67 | { |
55 | [Op.and]: [ | 68 | [Op.and]: [ |
@@ -80,26 +93,18 @@ export enum ScopeNames { | |||
80 | }) | 93 | }) |
81 | } | 94 | } |
82 | 95 | ||
83 | if (options.id) { | 96 | if (options.id) Object.assign(where, { id: options.id }) |
84 | where = Object.assign(where, { | 97 | if (options.state) Object.assign(where, { state: options.state }) |
85 | id: options.id | ||
86 | }) | ||
87 | } | ||
88 | 98 | ||
89 | if (options.state) { | 99 | if (options.videoIs === 'deleted') { |
90 | where = Object.assign(where, { | 100 | Object.assign(where, { |
91 | state: options.state | 101 | deletedVideo: { |
102 | [Op.not]: null | ||
103 | } | ||
92 | }) | 104 | }) |
93 | } | 105 | } |
94 | 106 | ||
95 | let onlyBlacklisted = false | 107 | const onlyBlacklisted = options.videoIs === 'blacklisted' |
96 | if (options.is === 'deleted') { | ||
97 | where = Object.assign(where, { | ||
98 | deletedVideo: { [Op.not]: null } | ||
99 | }) | ||
100 | } else if (options.is === 'blacklisted') { | ||
101 | onlyBlacklisted = true | ||
102 | } | ||
103 | 108 | ||
104 | return { | 109 | return { |
105 | attributes: { | 110 | attributes: { |
@@ -189,7 +194,7 @@ export enum ScopeNames { | |||
189 | }, | 194 | }, |
190 | { | 195 | { |
191 | model: VideoModel, | 196 | model: VideoModel, |
192 | required: onlyBlacklisted, | 197 | required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel), |
193 | where: searchAttribute(options.searchVideo, 'name'), | 198 | where: searchAttribute(options.searchVideo, 'name'), |
194 | include: [ | 199 | include: [ |
195 | { | 200 | { |
@@ -301,11 +306,36 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
301 | start: number | 306 | start: number |
302 | count: number | 307 | count: number |
303 | sort: string | 308 | sort: string |
304 | search?: string | 309 | |
305 | serverAccountId: number | 310 | serverAccountId: number |
306 | user?: MUserAccountId | 311 | user?: MUserAccountId |
312 | |||
313 | id?: number | ||
314 | state?: VideoAbuseState | ||
315 | videoIs?: VideoAbuseVideoIs | ||
316 | |||
317 | search?: string | ||
318 | searchReporter?: string | ||
319 | searchReportee?: string | ||
320 | searchVideo?: string | ||
321 | searchVideoChannel?: string | ||
307 | }) { | 322 | }) { |
308 | const { start, count, sort, search, user, serverAccountId } = parameters | 323 | const { |
324 | start, | ||
325 | count, | ||
326 | sort, | ||
327 | search, | ||
328 | user, | ||
329 | serverAccountId, | ||
330 | state, | ||
331 | videoIs, | ||
332 | searchReportee, | ||
333 | searchVideo, | ||
334 | searchVideoChannel, | ||
335 | searchReporter, | ||
336 | id | ||
337 | } = parameters | ||
338 | |||
309 | const userAccountId = user ? user.Account.id : undefined | 339 | const userAccountId = user ? user.Account.id : undefined |
310 | 340 | ||
311 | const query = { | 341 | const query = { |
@@ -317,37 +347,14 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
317 | } | 347 | } |
318 | 348 | ||
319 | const filters = { | 349 | const filters = { |
320 | ...parseQueryStringFilter(search, { | 350 | id, |
321 | id: { | 351 | search, |
322 | prefix: '#', | 352 | state, |
323 | handler: v => v | 353 | videoIs, |
324 | }, | 354 | searchReportee, |
325 | state: { | 355 | searchVideo, |
326 | prefix: 'state:', | 356 | searchVideoChannel, |
327 | handler: v => { | 357 | searchReporter, |
328 | if (v === 'accepted') return VideoAbuseState.ACCEPTED | ||
329 | if (v === 'pending') return VideoAbuseState.PENDING | ||
330 | if (v === 'rejected') return VideoAbuseState.REJECTED | ||
331 | return undefined | ||
332 | } | ||
333 | }, | ||
334 | is: { | ||
335 | prefix: 'is:', | ||
336 | handler: v => { | ||
337 | if (v === 'deleted') return v | ||
338 | if (v === 'blacklisted') return v | ||
339 | return undefined | ||
340 | } | ||
341 | }, | ||
342 | searchReporter: { | ||
343 | prefix: 'reporter:', | ||
344 | handler: v => v | ||
345 | }, | ||
346 | searchReportee: { | ||
347 | prefix: 'reportee:', | ||
348 | handler: v => v | ||
349 | } | ||
350 | }), | ||
351 | serverAccountId, | 358 | serverAccountId, |
352 | userAccountId | 359 | userAccountId |
353 | } | 360 | } |