aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-05-06 17:39:07 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-05-07 08:33:34 +0200
commitfeb34f6b6b991046aab6a10df747b48fa4da07a7 (patch)
tree02bb7277d45be166ba48caef2ee73bf89dbe1258
parentd170c5c580abf6f90d7bf144e2417e248ce2ecf4 (diff)
downloadPeerTube-feb34f6b6b991046aab6a10df747b48fa4da07a7.tar.gz
PeerTube-feb34f6b6b991046aab6a10df747b48fa4da07a7.tar.zst
PeerTube-feb34f6b6b991046aab6a10df747b48fa4da07a7.zip
Use video abuse filters on client side
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html4
-rw-r--r--client/src/app/shared/rest/rest.service.ts63
-rw-r--r--client/src/app/shared/video-abuse/video-abuse.service.ts31
-rw-r--r--server/controllers/api/videos/abuse.ts11
-rw-r--r--server/helpers/custom-validators/video-abuses.ts10
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts45
-rw-r--r--server/models/utils.ts60
-rw-r--r--server/models/video/video-abuse.ts127
-rw-r--r--server/tests/api/check-params/video-abuses.ts16
-rw-r--r--server/tests/api/users/users.ts2
-rw-r--r--server/tests/api/videos/video-abuse.ts70
-rw-r--r--shared/extra-utils/videos/video-abuses.ts55
-rw-r--r--shared/models/videos/abuse/video-abuse-video-is.type.ts1
13 files changed, 342 insertions, 153 deletions
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
index ba05073cf..cffa7a40e 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
@@ -19,8 +19,8 @@
19 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a> 19 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
20 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a> 20 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
21 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a> 21 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
22 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'is:blacklisted' }" class="dropdown-item" i18n>Reports with blacklisted videos</a> 22 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blacklisted videos</a>
23 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'is:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a> 23 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
24 </div> 24 </div>
25 </div> 25 </div>
26 <input 26 <input
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts
index 5bd2b5e43..cd6db1f3c 100644
--- a/client/src/app/shared/rest/rest.service.ts
+++ b/client/src/app/shared/rest/rest.service.ts
@@ -1,10 +1,21 @@
1import { Injectable } from '@angular/core'
2import { HttpParams } from '@angular/common/http'
3import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
4import { ComponentPagination, ComponentPaginationLight } from './component-pagination.model' 2import { HttpParams } from '@angular/common/http'
5 3import { Injectable } from '@angular/core'
4import { ComponentPaginationLight } from './component-pagination.model'
6import { RestPagination } from './rest-pagination' 5import { RestPagination } from './rest-pagination'
7 6
7interface QueryStringFilterPrefixes {
8 [key: string]: {
9 prefix: string
10 handler?: (v: string) => string | number
11 multiple?: boolean
12 }
13}
14
15type ParseQueryStringFilterResult = {
16 [key: string]: string | number | (string | number)[]
17}
18
8@Injectable() 19@Injectable()
9export class RestService { 20export class RestService {
10 21
@@ -53,4 +64,48 @@ export class RestService {
53 64
54 return { start, count } 65 return { start, count }
55 } 66 }
67
68 parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes): ParseQueryStringFilterResult {
69 if (!q) return {}
70
71 // Tokenize the strings using spaces
72 const tokens = q.split(' ').filter(token => !!token)
73
74 // Build prefix array
75 const prefixeStrings = Object.values(prefixes)
76 .map(p => p.prefix)
77
78 // Search is the querystring minus defined filters
79 const searchTokens = tokens.filter(t => {
80 return prefixeStrings.every(prefixString => t.startsWith(prefixString) === false)
81 })
82
83 const additionalFilters: ParseQueryStringFilterResult = {}
84
85 for (const prefixKey of Object.keys(prefixes)) {
86 const prefixObj = prefixes[prefixKey]
87 const prefix = prefixObj.prefix
88
89 const matchedTokens = tokens.filter(t => t.startsWith(prefix))
90 .map(t => t.slice(prefix.length)) // Keep the value filter
91 .map(t => {
92 if (prefixObj.handler) return prefixObj.handler(t)
93
94 return t
95 })
96 .filter(t => !!t)
97
98 if (matchedTokens.length === 0) continue
99
100 additionalFilters[prefixKey] = prefixObj.multiple === true
101 ? matchedTokens
102 : matchedTokens[0]
103 }
104
105 return {
106 search: searchTokens.join(' '),
107
108 ...additionalFilters
109 }
110 }
56} 111}
diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts
index 1ab6b5376..700a30239 100644
--- a/client/src/app/shared/video-abuse/video-abuse.service.ts
+++ b/client/src/app/shared/video-abuse/video-abuse.service.ts
@@ -3,7 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { SortMeta } from 'primeng/api' 4import { SortMeta } from 'primeng/api'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { ResultList, VideoAbuse, VideoAbuseUpdate } from '../../../../../shared' 6import { ResultList, VideoAbuse, VideoAbuseUpdate, VideoAbuseState } from '../../../../../shared'
7import { environment } from '../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { RestExtractor, RestPagination, RestService } from '../rest' 8import { RestExtractor, RestPagination, RestService } from '../rest'
9 9
@@ -28,7 +28,34 @@ export class VideoAbuseService {
28 let params = new HttpParams() 28 let params = new HttpParams()
29 params = this.restService.addRestGetParams(params, pagination, sort) 29 params = this.restService.addRestGetParams(params, pagination, sort)
30 30
31 if (search) params = params.append('search', search) 31 if (search) {
32 const filters = this.restService.parseQueryStringFilter(search, {
33 id: { prefix: '#' },
34 state: {
35 prefix: 'state:',
36 handler: v => {
37 if (v === 'accepted') return VideoAbuseState.ACCEPTED
38 if (v === 'pending') return VideoAbuseState.PENDING
39 if (v === 'rejected') return VideoAbuseState.REJECTED
40
41 return undefined
42 }
43 },
44 videoIs: {
45 prefix: 'videoIs:',
46 handler: v => {
47 if (v === 'deleted') return v
48 if (v === 'blacklisted') return v
49
50 return undefined
51 }
52 },
53 searchReporter: { prefix: 'reporter:' },
54 searchReportee: { prefix: 'reportee:' }
55 })
56
57 params = this.restService.addObjectParams(params, filters)
58 }
32 59
33 return this.authHttp.get<ResultList<VideoAbuse>>(url, { params }) 60 return this.authHttp.get<ResultList<VideoAbuse>>(url, { params })
34 .pipe( 61 .pipe(
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index bc7df48c8..3fe7f7e51 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -14,7 +14,8 @@ import {
14 videoAbuseGetValidator, 14 videoAbuseGetValidator,
15 videoAbuseReportValidator, 15 videoAbuseReportValidator,
16 videoAbusesSortValidator, 16 videoAbusesSortValidator,
17 videoAbuseUpdateValidator 17 videoAbuseUpdateValidator,
18 videoAbuseListValidator
18} from '../../../middlewares' 19} from '../../../middlewares'
19import { AccountModel } from '../../../models/account/account' 20import { AccountModel } from '../../../models/account/account'
20import { VideoAbuseModel } from '../../../models/video/video-abuse' 21import { VideoAbuseModel } from '../../../models/video/video-abuse'
@@ -34,6 +35,7 @@ abuseVideoRouter.get('/abuse',
34 videoAbusesSortValidator, 35 videoAbusesSortValidator,
35 setDefaultSort, 36 setDefaultSort,
36 setDefaultPagination, 37 setDefaultPagination,
38 videoAbuseListValidator,
37 asyncMiddleware(listVideoAbuses) 39 asyncMiddleware(listVideoAbuses)
38) 40)
39abuseVideoRouter.put('/:videoId/abuse/:id', 41abuseVideoRouter.put('/:videoId/abuse/:id',
@@ -70,7 +72,14 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
70 start: req.query.start, 72 start: req.query.start,
71 count: req.query.count, 73 count: req.query.count,
72 sort: req.query.sort, 74 sort: req.query.sort,
75 id: req.query.id,
73 search: req.query.search, 76 search: req.query.search,
77 state: req.query.state,
78 videoIs: req.query.videoIs,
79 searchReporter: req.query.searchReporter,
80 searchReportee: req.query.searchReportee,
81 searchVideo: req.query.searchVideo,
82 searchVideoChannel: req.query.searchVideoChannel,
74 serverAccountId: serverActor.Account.id, 83 serverAccountId: serverActor.Account.id,
75 user 84 user
76 }) 85 })
diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts
index 5c7bc6fd9..05e11b1c6 100644
--- a/server/helpers/custom-validators/video-abuses.ts
+++ b/server/helpers/custom-validators/video-abuses.ts
@@ -1,6 +1,8 @@
1import validator from 'validator' 1import validator from 'validator'
2
2import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
3import { exists } from './misc' 4import { exists } from './misc'
5import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
4 6
5const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 7const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
6 8
@@ -16,10 +18,18 @@ function isVideoAbuseStateValid (value: string) {
16 return exists(value) && VIDEO_ABUSE_STATES[value] !== undefined 18 return exists(value) && VIDEO_ABUSE_STATES[value] !== undefined
17} 19}
18 20
21function isAbuseVideoIsValid (value: VideoAbuseVideoIs) {
22 return exists(value) && (
23 value === 'deleted' ||
24 value === 'blacklisted'
25 )
26}
27
19// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
20 29
21export { 30export {
22 isVideoAbuseStateValid, 31 isVideoAbuseStateValid,
23 isVideoAbuseReasonValid, 32 isVideoAbuseReasonValid,
33 isAbuseVideoIsValid,
24 isVideoAbuseModerationCommentValid 34 isVideoAbuseModerationCommentValid
25} 35}
diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
index 7c316fe13..901997bcb 100644
--- a/server/middlewares/validators/videos/video-abuses.ts
+++ b/server/middlewares/validators/videos/video-abuses.ts
@@ -1,14 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 3import { exists, isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
4import { logger } from '../../../helpers/logger'
5import { areValidationErrors } from '../utils'
6import { 4import {
5 isAbuseVideoIsValid,
7 isVideoAbuseModerationCommentValid, 6 isVideoAbuseModerationCommentValid,
8 isVideoAbuseReasonValid, 7 isVideoAbuseReasonValid,
9 isVideoAbuseStateValid 8 isVideoAbuseStateValid
10} from '../../../helpers/custom-validators/video-abuses' 9} from '../../../helpers/custom-validators/video-abuses'
10import { logger } from '../../../helpers/logger'
11import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares' 11import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares'
12import { areValidationErrors } from '../utils'
12 13
13const videoAbuseReportValidator = [ 14const videoAbuseReportValidator = [
14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 15 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -58,9 +59,45 @@ const videoAbuseUpdateValidator = [
58 } 59 }
59] 60]
60 61
62const videoAbuseListValidator = [
63 query('id')
64 .optional()
65 .custom(isIdValid).withMessage('Should have a valid id'),
66 query('search')
67 .optional()
68 .custom(exists).withMessage('Should have a valid search'),
69 query('state')
70 .optional()
71 .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'),
72 query('videoIs')
73 .optional()
74 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
75 query('searchReporter')
76 .optional()
77 .custom(exists).withMessage('Should have a valid reporter search'),
78 query('searchReportee')
79 .optional()
80 .custom(exists).withMessage('Should have a valid reportee search'),
81 query('searchVideo')
82 .optional()
83 .custom(exists).withMessage('Should have a valid video search'),
84 query('searchVideoChannel')
85 .optional()
86 .custom(exists).withMessage('Should have a valid video channel search'),
87
88 (req: express.Request, res: express.Response, next: express.NextFunction) => {
89 logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body })
90
91 if (areValidationErrors(req, res)) return
92
93 return next()
94 }
95]
96
61// --------------------------------------------------------------------------- 97// ---------------------------------------------------------------------------
62 98
63export { 99export {
100 videoAbuseListValidator,
64 videoAbuseReportValidator, 101 videoAbuseReportValidator,
65 videoAbuseGetValidator, 102 videoAbuseGetValidator,
66 videoAbuseUpdateValidator 103 videoAbuseUpdateValidator
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
210function searchAttribute (sourceField, targetField) { 210function 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
222interface QueryStringFilterPrefixes {
223 [key: string]: string | { prefix: string, handler: Function, multiple?: boolean }
224}
225
226function 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 @@
1import * as Bluebird from 'bluebird'
2import { literal, Op } from 'sequelize'
1import { 3import {
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'
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
18import { VideoAbuseState, VideoDetails } from '../../../shared'
4import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 19import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
5import { VideoAbuse } from '../../../shared/models/videos' 20import { VideoAbuse } from '../../../shared/models/videos'
6import { 21import {
@@ -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'
11import { AccountModel } from '../account/account'
12import { buildBlockedAccountSQL, getSort, throwIfNotValid, searchAttribute, parseQueryStringFilter } from '../utils'
13import { VideoModel } from './video'
14import { VideoAbuseState, VideoDetails } from '../../../shared'
15import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 26import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
16import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models' 27import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
17import * as Bluebird from 'bluebird' 28import { AccountModel } from '../account/account'
18import { literal, Op } from 'sequelize' 29import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
19import { ThumbnailModel } from './thumbnail' 30import { ThumbnailModel } from './thumbnail'
31import { VideoModel } from './video'
20import { VideoBlacklistModel } from './video-blacklist' 32import { VideoBlacklistModel } from './video-blacklist'
21import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' 33import { 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 }
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index bea2177f3..e643cb95e 100644
--- a/server/tests/api/check-params/video-abuses.ts
+++ b/server/tests/api/check-params/video-abuses.ts
@@ -76,6 +76,22 @@ describe('Test video abuses API validators', function () {
76 statusCodeExpected: 403 76 statusCodeExpected: 403
77 }) 77 })
78 }) 78 })
79
80 it('Should fail with a bad id filter', async function () {
81 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } })
82 })
83
84 it('Should fail with a bad state filter', async function () {
85 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } })
86 })
87
88 it('Should fail with a bad videoIs filter', async function () {
89 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } })
90 })
91
92 it('Should succeed with the correct params', async function () {
93 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 13 }, statusCodeExpected: 200 })
94 })
79 }) 95 })
80 96
81 describe('When reporting a video abuse', function () { 97 describe('When reporting a video abuse', function () {
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 60fbd2a20..f3b732632 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -901,7 +901,7 @@ describe('Test users', function () {
901 const reason = 'my super bad reason' 901 const reason = 'my super bad reason'
902 await reportVideoAbuse(server.url, user17AccessToken, videoId, reason) 902 await reportVideoAbuse(server.url, user17AccessToken, videoId, reason)
903 903
904 const res1 = await getVideoAbusesList(server.url, server.accessToken) 904 const res1 = await getVideoAbusesList({ url: server.url, token: server.accessToken })
905 const abuseId = res1.body.data[0].id 905 const abuseId = res1.body.data[0].id
906 906
907 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true) 907 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index 26bc3783b..a96be97f6 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -71,7 +71,7 @@ describe('Test video abuses', function () {
71 }) 71 })
72 72
73 it('Should not have video abuses', async function () { 73 it('Should not have video abuses', async function () {
74 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 74 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
75 75
76 expect(res.body.total).to.equal(0) 76 expect(res.body.total).to.equal(0)
77 expect(res.body.data).to.be.an('array') 77 expect(res.body.data).to.be.an('array')
@@ -89,7 +89,7 @@ describe('Test video abuses', function () {
89 }) 89 })
90 90
91 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () { 91 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
92 const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 92 const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
93 93
94 expect(res1.body.total).to.equal(1) 94 expect(res1.body.total).to.equal(1)
95 expect(res1.body.data).to.be.an('array') 95 expect(res1.body.data).to.be.an('array')
@@ -106,7 +106,7 @@ describe('Test video abuses', function () {
106 expect(abuse.countReportsForReporter).to.equal(1) 106 expect(abuse.countReportsForReporter).to.equal(1)
107 expect(abuse.countReportsForReportee).to.equal(1) 107 expect(abuse.countReportsForReportee).to.equal(1)
108 108
109 const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 109 const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
110 expect(res2.body.total).to.equal(0) 110 expect(res2.body.total).to.equal(0)
111 expect(res2.body.data).to.be.an('array') 111 expect(res2.body.data).to.be.an('array')
112 expect(res2.body.data.length).to.equal(0) 112 expect(res2.body.data.length).to.equal(0)
@@ -123,7 +123,7 @@ describe('Test video abuses', function () {
123 }) 123 })
124 124
125 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () { 125 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
126 const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 126 const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
127 expect(res1.body.total).to.equal(2) 127 expect(res1.body.total).to.equal(2)
128 expect(res1.body.data).to.be.an('array') 128 expect(res1.body.data).to.be.an('array')
129 expect(res1.body.data.length).to.equal(2) 129 expect(res1.body.data.length).to.equal(2)
@@ -148,7 +148,7 @@ describe('Test video abuses', function () {
148 expect(abuse2.state.label).to.equal('Pending') 148 expect(abuse2.state.label).to.equal('Pending')
149 expect(abuse2.moderationComment).to.be.null 149 expect(abuse2.moderationComment).to.be.null
150 150
151 const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 151 const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
152 expect(res2.body.total).to.equal(1) 152 expect(res2.body.total).to.equal(1)
153 expect(res2.body.data).to.be.an('array') 153 expect(res2.body.data).to.be.an('array')
154 expect(res2.body.data.length).to.equal(1) 154 expect(res2.body.data.length).to.equal(1)
@@ -166,7 +166,7 @@ describe('Test video abuses', function () {
166 const body = { state: VideoAbuseState.REJECTED } 166 const body = { state: VideoAbuseState.REJECTED }
167 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) 167 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
168 168
169 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 169 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
170 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED) 170 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED)
171 }) 171 })
172 172
@@ -174,7 +174,7 @@ describe('Test video abuses', function () {
174 const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' } 174 const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' }
175 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) 175 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
176 176
177 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 177 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
178 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED) 178 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED)
179 expect(res.body.data[0].moderationComment).to.equal('It is valid') 179 expect(res.body.data[0].moderationComment).to.equal('It is valid')
180 }) 180 })
@@ -186,7 +186,7 @@ describe('Test video abuses', function () {
186 await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this') 186 await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this')
187 await waitJobs(servers) 187 await waitJobs(servers)
188 188
189 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 189 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
190 expect(res.body.total).to.equal(3) 190 expect(res.body.total).to.equal(3)
191 } 191 }
192 192
@@ -195,7 +195,7 @@ describe('Test video abuses', function () {
195 { 195 {
196 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) 196 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
197 197
198 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 198 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
199 expect(res.body.total).to.equal(2) 199 expect(res.body.total).to.equal(2)
200 200
201 const abuse = res.body.data.find(a => a.reason === 'will mute this') 201 const abuse = res.body.data.find(a => a.reason === 'will mute this')
@@ -205,7 +205,7 @@ describe('Test video abuses', function () {
205 { 205 {
206 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) 206 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
207 207
208 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 208 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
209 expect(res.body.total).to.equal(3) 209 expect(res.body.total).to.equal(3)
210 } 210 }
211 }) 211 })
@@ -216,7 +216,7 @@ describe('Test video abuses', function () {
216 { 216 {
217 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host) 217 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
218 218
219 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 219 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
220 expect(res.body.total).to.equal(2) 220 expect(res.body.total).to.equal(2)
221 221
222 const abuse = res.body.data.find(a => a.reason === 'will mute this') 222 const abuse = res.body.data.find(a => a.reason === 'will mute this')
@@ -226,7 +226,7 @@ describe('Test video abuses', function () {
226 { 226 {
227 await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock) 227 await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock)
228 228
229 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 229 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
230 expect(res.body.total).to.equal(3) 230 expect(res.body.total).to.equal(3)
231 } 231 }
232 }) 232 })
@@ -238,7 +238,7 @@ describe('Test video abuses', function () {
238 238
239 await waitJobs(servers) 239 await waitJobs(servers)
240 240
241 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 241 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
242 expect(res.body.total).to.equal(2, "wrong number of videos returned") 242 expect(res.body.total).to.equal(2, "wrong number of videos returned")
243 expect(res.body.data.length).to.equal(2, "wrong number of videos returned") 243 expect(res.body.data.length).to.equal(2, "wrong number of videos returned")
244 expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video") 244 expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video")
@@ -274,7 +274,7 @@ describe('Test video abuses', function () {
274 const reason4 = 'my super bad reason 4' 274 const reason4 = 'my super bad reason 4'
275 await reportVideoAbuse(servers[0].url, userAccessToken, servers[0].video.id, reason4) 275 await reportVideoAbuse(servers[0].url, userAccessToken, servers[0].video.id, reason4)
276 276
277 const res2 = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 277 const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
278 278
279 { 279 {
280 for (const abuse of res2.body.data as VideoAbuse[]) { 280 for (const abuse of res2.body.data as VideoAbuse[]) {
@@ -299,18 +299,56 @@ describe('Test video abuses', function () {
299 await waitJobs(servers) 299 await waitJobs(servers)
300 300
301 { 301 {
302 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 302 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
303 expect(res.body.total).to.equal(1) 303 expect(res.body.total).to.equal(1)
304 expect(res.body.data.length).to.equal(1) 304 expect(res.body.data.length).to.equal(1)
305 expect(res.body.data[0].id).to.not.equal(abuseServer2.id) 305 expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
306 } 306 }
307 307
308 { 308 {
309 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 309 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
310 expect(res.body.total).to.equal(5) 310 expect(res.body.total).to.equal(5)
311 } 311 }
312 }) 312 })
313 313
314 it('Should list and filter video abuses', async function () {
315 async function list (query: Omit<Parameters<typeof getVideoAbusesList>[0], 'url' | 'token'>) {
316 const options = {
317 url: servers[0].url,
318 token: servers[0].accessToken
319 }
320
321 Object.assign(options, query)
322
323 const res = await getVideoAbusesList(options)
324
325 return res.body.data as VideoAbuse[]
326 }
327
328 expect(await list({ id: 56 })).to.have.lengthOf(0)
329 expect(await list({ id: 1 })).to.have.lengthOf(1)
330
331 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(3)
332 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
333
334 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
335
336 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(3)
337 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
338
339 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
340 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(4)
341
342 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(3)
343 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
344
345 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
346 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
347
348 expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0)
349 expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(5)
350 })
351
314 after(async function () { 352 after(async function () {
315 await cleanupTests(servers) 353 await cleanupTests(servers)
316 }) 354 })
diff --git a/shared/extra-utils/videos/video-abuses.ts b/shared/extra-utils/videos/video-abuses.ts
index 7f011ec0f..81582bfc7 100644
--- a/shared/extra-utils/videos/video-abuses.ts
+++ b/shared/extra-utils/videos/video-abuses.ts
@@ -1,6 +1,8 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model' 2import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
3import { makeDeleteRequest, makePutBodyRequest } from '../requests/requests' 3import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests'
4import { VideoAbuseState } from '@shared/models'
5import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
4 6
5function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) { 7function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) {
6 const path = '/api/v1/videos/' + videoId + '/abuse' 8 const path = '/api/v1/videos/' + videoId + '/abuse'
@@ -13,16 +15,51 @@ function reportVideoAbuse (url: string, token: string, videoId: number | string,
13 .expect(specialStatus) 15 .expect(specialStatus)
14} 16}
15 17
16function getVideoAbusesList (url: string, token: string) { 18function getVideoAbusesList (options: {
19 url: string
20 token: string
21 id?: number
22 search?: string
23 state?: VideoAbuseState
24 videoIs?: VideoAbuseVideoIs
25 searchReporter?: string
26 searchReportee?: string
27 searchVideo?: string
28 searchVideoChannel?: string
29}) {
30 const {
31 url,
32 token,
33 id,
34 search,
35 state,
36 videoIs,
37 searchReporter,
38 searchReportee,
39 searchVideo,
40 searchVideoChannel
41 } = options
17 const path = '/api/v1/videos/abuse' 42 const path = '/api/v1/videos/abuse'
18 43
19 return request(url) 44 const query = {
20 .get(path) 45 sort: 'createdAt',
21 .query({ sort: 'createdAt' }) 46 id,
22 .set('Accept', 'application/json') 47 search,
23 .set('Authorization', 'Bearer ' + token) 48 state,
24 .expect(200) 49 videoIs,
25 .expect('Content-Type', /json/) 50 searchReporter,
51 searchReportee,
52 searchVideo,
53 searchVideoChannel
54 }
55
56 return makeGetRequest({
57 url,
58 path,
59 token,
60 query,
61 statusCodeExpected: 200
62 })
26} 63}
27 64
28function updateVideoAbuse ( 65function updateVideoAbuse (
diff --git a/shared/models/videos/abuse/video-abuse-video-is.type.ts b/shared/models/videos/abuse/video-abuse-video-is.type.ts
new file mode 100644
index 000000000..e86018993
--- /dev/null
+++ b/shared/models/videos/abuse/video-abuse-video-is.type.ts
@@ -0,0 +1 @@
export type VideoAbuseVideoIs = 'deleted' | 'blacklisted'