aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/abuse.ts10
-rw-r--r--server/helpers/custom-validators/abuses.ts17
-rw-r--r--server/helpers/custom-validators/video-comments.ts81
-rw-r--r--server/helpers/middlewares/abuses.ts13
-rw-r--r--server/helpers/middlewares/accounts.ts4
-rw-r--r--server/middlewares/validators/abuse.ts74
-rw-r--r--server/middlewares/validators/videos/video-comments.ts70
-rw-r--r--server/models/abuse/abuse.ts91
-rw-r--r--server/models/abuse/video-comment-abuse.ts2
-rw-r--r--server/models/video/video-comment.ts65
-rw-r--r--server/models/video/video.ts4
-rw-r--r--server/tests/api/check-params/abuses.ts271
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/video-abuses.ts6
-rw-r--r--server/types/models/moderation/abuse.ts9
-rw-r--r--server/typings/express/index.d.ts1
-rw-r--r--shared/extra-utils/moderation/abuses.ts88
-rw-r--r--shared/models/moderation/abuse/abuse-create.model.ts7
-rw-r--r--shared/models/moderation/abuse/abuse-filter.ts1
-rw-r--r--shared/models/moderation/abuse/abuse-filter.type.ts1
-rw-r--r--shared/models/moderation/abuse/abuse.model.ts10
-rw-r--r--shared/models/moderation/abuse/index.ts1
22 files changed, 665 insertions, 162 deletions
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
index ee046cb3a..38808021d 100644
--- a/server/controllers/api/abuse.ts
+++ b/server/controllers/api/abuse.ts
@@ -23,7 +23,7 @@ import { AccountModel } from '../../models/account/account'
23 23
24const abuseRouter = express.Router() 24const abuseRouter = express.Router()
25 25
26abuseRouter.get('/abuse', 26abuseRouter.get('/',
27 authenticate, 27 authenticate,
28 ensureUserHasRight(UserRight.MANAGE_ABUSES), 28 ensureUserHasRight(UserRight.MANAGE_ABUSES),
29 paginationValidator, 29 paginationValidator,
@@ -33,18 +33,18 @@ abuseRouter.get('/abuse',
33 abuseListValidator, 33 abuseListValidator,
34 asyncMiddleware(listAbuses) 34 asyncMiddleware(listAbuses)
35) 35)
36abuseRouter.put('/:videoId/abuse/:id', 36abuseRouter.put('/:id',
37 authenticate, 37 authenticate,
38 ensureUserHasRight(UserRight.MANAGE_ABUSES), 38 ensureUserHasRight(UserRight.MANAGE_ABUSES),
39 asyncMiddleware(abuseUpdateValidator), 39 asyncMiddleware(abuseUpdateValidator),
40 asyncRetryTransactionMiddleware(updateAbuse) 40 asyncRetryTransactionMiddleware(updateAbuse)
41) 41)
42abuseRouter.post('/:videoId/abuse', 42abuseRouter.post('/',
43 authenticate, 43 authenticate,
44 asyncMiddleware(abuseReportValidator), 44 asyncMiddleware(abuseReportValidator),
45 asyncRetryTransactionMiddleware(reportAbuse) 45 asyncRetryTransactionMiddleware(reportAbuse)
46) 46)
47abuseRouter.delete('/:videoId/abuse/:id', 47abuseRouter.delete('/:id',
48 authenticate, 48 authenticate,
49 ensureUserHasRight(UserRight.MANAGE_ABUSES), 49 ensureUserHasRight(UserRight.MANAGE_ABUSES),
50 asyncMiddleware(abuseGetValidator), 50 asyncMiddleware(abuseGetValidator),
@@ -74,7 +74,7 @@ async function listAbuses (req: express.Request, res: express.Response) {
74 count: req.query.count, 74 count: req.query.count,
75 sort: req.query.sort, 75 sort: req.query.sort,
76 id: req.query.id, 76 id: req.query.id,
77 filter: 'video', 77 filter: req.query.filter,
78 predefinedReason: req.query.predefinedReason, 78 predefinedReason: req.query.predefinedReason,
79 search: req.query.search, 79 search: req.query.search,
80 state: req.query.state, 80 state: req.query.state,
diff --git a/server/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts
index a6a895c65..c21468caa 100644
--- a/server/helpers/custom-validators/abuses.ts
+++ b/server/helpers/custom-validators/abuses.ts
@@ -1,6 +1,6 @@
1import validator from 'validator' 1import validator from 'validator'
2import { abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs } from '@shared/models' 2import { AbuseFilter, abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs, AbuseCreate } from '@shared/models'
3import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants' 3import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
4import { exists, isArray } from './misc' 4import { exists, isArray } from './misc'
5 5
6const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES 6const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
@@ -13,7 +13,11 @@ function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) {
13 return exists(value) && value in abusePredefinedReasonsMap 13 return exists(value) && value in abusePredefinedReasonsMap
14} 14}
15 15
16function isAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) { 16function isAbuseFilterValid (value: AbuseFilter) {
17 return value === 'video' || value === 'comment' || value === 'account'
18}
19
20function areAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) {
17 return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap) 21 return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap)
18} 22}
19 23
@@ -22,7 +26,9 @@ function isAbuseTimestampValid (value: number) {
22} 26}
23 27
24function isAbuseTimestampCoherent (endAt: number, { req }) { 28function isAbuseTimestampCoherent (endAt: number, { req }) {
25 return exists(req.body.startAt) && endAt > req.body.startAt 29 const startAt = (req.body as AbuseCreate).video.startAt
30
31 return exists(startAt) && endAt > startAt
26} 32}
27 33
28function isAbuseModerationCommentValid (value: string) { 34function isAbuseModerationCommentValid (value: string) {
@@ -44,8 +50,9 @@ function isAbuseVideoIsValid (value: AbuseVideoIs) {
44 50
45export { 51export {
46 isAbuseReasonValid, 52 isAbuseReasonValid,
53 isAbuseFilterValid,
47 isAbusePredefinedReasonValid, 54 isAbusePredefinedReasonValid,
48 isAbusePredefinedReasonsValid, 55 areAbusePredefinedReasonsValid as isAbusePredefinedReasonsValid,
49 isAbuseTimestampValid, 56 isAbuseTimestampValid,
50 isAbuseTimestampCoherent, 57 isAbuseTimestampCoherent,
51 isAbuseModerationCommentValid, 58 isAbuseModerationCommentValid,
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts
index 846f28b17..a01680cbe 100644
--- a/server/helpers/custom-validators/video-comments.ts
+++ b/server/helpers/custom-validators/video-comments.ts
@@ -1,6 +1,8 @@
1import 'multer' 1import * as express from 'express'
2import validator from 'validator' 2import validator from 'validator'
3import { VideoCommentModel } from '@server/models/video/video-comment'
3import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 4import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
5import { MVideoId } from '@server/types/models'
4 6
5const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS 7const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS
6 8
@@ -8,8 +10,83 @@ function isValidVideoCommentText (value: string) {
8 return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) 10 return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT)
9} 11}
10 12
13async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
14 const id = parseInt(idArg + '', 10)
15 const videoComment = await VideoCommentModel.loadById(id)
16
17 if (!videoComment) {
18 res.status(404)
19 .json({ error: 'Video comment thread not found' })
20 .end()
21
22 return false
23 }
24
25 if (videoComment.videoId !== video.id) {
26 res.status(400)
27 .json({ error: 'Video comment is not associated to this video.' })
28 .end()
29
30 return false
31 }
32
33 if (videoComment.inReplyToCommentId !== null) {
34 res.status(400)
35 .json({ error: 'Video comment is not a thread.' })
36 .end()
37
38 return false
39 }
40
41 res.locals.videoCommentThread = videoComment
42 return true
43}
44
45async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
46 const id = parseInt(idArg + '', 10)
47 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
48
49 if (!videoComment) {
50 res.status(404)
51 .json({ error: 'Video comment thread not found' })
52 .end()
53
54 return false
55 }
56
57 if (videoComment.videoId !== video.id) {
58 res.status(400)
59 .json({ error: 'Video comment is not associated to this video.' })
60 .end()
61
62 return false
63 }
64
65 res.locals.videoCommentFull = videoComment
66 return true
67}
68
69async function doesCommentIdExist (idArg: number | string, res: express.Response) {
70 const id = parseInt(idArg + '', 10)
71 const videoComment = await VideoCommentModel.loadById(id)
72
73 if (!videoComment) {
74 res.status(404)
75 .json({ error: 'Video comment thread not found' })
76
77 return false
78 }
79
80 res.locals.videoComment = videoComment
81
82 return true
83}
84
11// --------------------------------------------------------------------------- 85// ---------------------------------------------------------------------------
12 86
13export { 87export {
14 isValidVideoCommentText 88 isValidVideoCommentText,
89 doesVideoCommentThreadExist,
90 doesVideoCommentExist,
91 doesCommentIdExist
15} 92}
diff --git a/server/helpers/middlewares/abuses.ts b/server/helpers/middlewares/abuses.ts
index 3906f6760..b102273a2 100644
--- a/server/helpers/middlewares/abuses.ts
+++ b/server/helpers/middlewares/abuses.ts
@@ -17,7 +17,6 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
17 if (abuse === null) { 17 if (abuse === null) {
18 res.status(404) 18 res.status(404)
19 .json({ error: 'Video abuse not found' }) 19 .json({ error: 'Video abuse not found' })
20 .end()
21 20
22 return false 21 return false
23 } 22 }
@@ -26,8 +25,18 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
26 return true 25 return true
27} 26}
28 27
29async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) { 28async function doesAbuseExist (abuseId: number | string, res: Response) {
29 const abuse = await AbuseModel.loadById(parseInt(abuseId + '', 10))
30 30
31 if (!abuse) {
32 res.status(404)
33 .json({ error: 'Video abuse not found' })
34
35 return false
36 }
37
38 res.locals.abuse = abuse
39 return true
31} 40}
32 41
33// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts
index bddea7eaa..29b4ed1a6 100644
--- a/server/helpers/middlewares/accounts.ts
+++ b/server/helpers/middlewares/accounts.ts
@@ -3,8 +3,8 @@ import { AccountModel } from '../../models/account/account'
3import * as Bluebird from 'bluebird' 3import * as Bluebird from 'bluebird'
4import { MAccountDefault } from '../../types/models' 4import { MAccountDefault } from '../../types/models'
5 5
6function doesAccountIdExist (id: number, res: Response, sendNotFound = true) { 6function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
7 const promise = AccountModel.load(id) 7 const promise = AccountModel.load(parseInt(id + '', 10))
8 8
9 return doesAccountExist(promise, res, sendNotFound) 9 return doesAccountExist(promise, res, sendNotFound)
10} 10}
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts
index f098e2ff9..048dbead0 100644
--- a/server/middlewares/validators/abuse.ts
+++ b/server/middlewares/validators/abuse.ts
@@ -1,6 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { 3import {
4 isAbuseFilterValid,
4 isAbuseModerationCommentValid, 5 isAbuseModerationCommentValid,
5 isAbusePredefinedReasonsValid, 6 isAbusePredefinedReasonsValid,
6 isAbusePredefinedReasonValid, 7 isAbusePredefinedReasonValid,
@@ -11,29 +12,28 @@ import {
11 isAbuseVideoIsValid 12 isAbuseVideoIsValid
12} from '@server/helpers/custom-validators/abuses' 13} from '@server/helpers/custom-validators/abuses'
13import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc' 14import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc'
15import { doesCommentIdExist } from '@server/helpers/custom-validators/video-comments'
14import { logger } from '@server/helpers/logger' 16import { logger } from '@server/helpers/logger'
15import { doesAbuseExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares' 17import { doesAbuseExist, doesAccountIdExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares'
18import { AbuseCreate } from '@shared/models'
16import { areValidationErrors } from './utils' 19import { areValidationErrors } from './utils'
17 20
18const abuseReportValidator = [ 21const abuseReportValidator = [
19 param('videoId') 22 body('account.id')
23 .optional()
24 .custom(isIdValid)
25 .withMessage('Should have a valid accountId'),
26
27 body('video.id')
28 .optional()
20 .custom(isIdOrUUIDValid) 29 .custom(isIdOrUUIDValid)
21 .not()
22 .isEmpty()
23 .withMessage('Should have a valid videoId'), 30 .withMessage('Should have a valid videoId'),
24 body('reason') 31 body('video.startAt')
25 .custom(isAbuseReasonValid)
26 .withMessage('Should have a valid reason'),
27 body('predefinedReasons')
28 .optional()
29 .custom(isAbusePredefinedReasonsValid)
30 .withMessage('Should have a valid list of predefined reasons'),
31 body('startAt')
32 .optional() 32 .optional()
33 .customSanitizer(toIntOrNull) 33 .customSanitizer(toIntOrNull)
34 .custom(isAbuseTimestampValid) 34 .custom(isAbuseTimestampValid)
35 .withMessage('Should have valid starting time value'), 35 .withMessage('Should have valid starting time value'),
36 body('endAt') 36 body('video.endAt')
37 .optional() 37 .optional()
38 .customSanitizer(toIntOrNull) 38 .customSanitizer(toIntOrNull)
39 .custom(isAbuseTimestampValid) 39 .custom(isAbuseTimestampValid)
@@ -42,47 +42,70 @@ const abuseReportValidator = [
42 .custom(isAbuseTimestampCoherent) 42 .custom(isAbuseTimestampCoherent)
43 .withMessage('Should have a startAt timestamp beginning before endAt'), 43 .withMessage('Should have a startAt timestamp beginning before endAt'),
44 44
45 body('comment.id')
46 .optional()
47 .custom(isIdValid)
48 .withMessage('Should have a valid commentId'),
49
50 body('reason')
51 .custom(isAbuseReasonValid)
52 .withMessage('Should have a valid reason'),
53
54 body('predefinedReasons')
55 .optional()
56 .custom(isAbusePredefinedReasonsValid)
57 .withMessage('Should have a valid list of predefined reasons'),
58
45 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 59 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
46 logger.debug('Checking abuseReport parameters', { parameters: req.body }) 60 logger.debug('Checking abuseReport parameters', { parameters: req.body })
47 61
48 if (areValidationErrors(req, res)) return 62 if (areValidationErrors(req, res)) return
49 if (!await doesVideoExist(req.params.videoId, res)) return
50 63
51 // TODO: check comment or video (exlusive) 64 const body: AbuseCreate = req.body
65
66 if (body.video?.id && !await doesVideoExist(body.video.id, res)) return
67 if (body.account?.id && !await doesAccountIdExist(body.account.id, res)) return
68 if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
69
70 if (!body.video?.id && !body.account?.id && !body.comment?.id) {
71 res.status(400)
72 .json({ error: 'video id or account id or comment id is required.' })
73
74 return
75 }
52 76
53 return next() 77 return next()
54 } 78 }
55] 79]
56 80
57const abuseGetValidator = [ 81const abuseGetValidator = [
58 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
59 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), 82 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
60 83
61 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 84 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
62 logger.debug('Checking abuseGetValidator parameters', { parameters: req.body }) 85 logger.debug('Checking abuseGetValidator parameters', { parameters: req.body })
63 86
64 if (areValidationErrors(req, res)) return 87 if (areValidationErrors(req, res)) return
65 // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return 88 if (!await doesAbuseExist(req.params.id, res)) return
66 89
67 return next() 90 return next()
68 } 91 }
69] 92]
70 93
71const abuseUpdateValidator = [ 94const abuseUpdateValidator = [
72 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
73 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), 95 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
96
74 body('state') 97 body('state')
75 .optional() 98 .optional()
76 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), 99 .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'),
77 body('moderationComment') 100 body('moderationComment')
78 .optional() 101 .optional()
79 .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), 102 .custom(isAbuseModerationCommentValid).withMessage('Should have a valid moderation comment'),
80 103
81 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 104 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
82 logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body }) 105 logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body })
83 106
84 if (areValidationErrors(req, res)) return 107 if (areValidationErrors(req, res)) return
85 // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return 108 if (!await doesAbuseExist(req.params.id, res)) return
86 109
87 return next() 110 return next()
88 } 111 }
@@ -92,6 +115,10 @@ const abuseListValidator = [
92 query('id') 115 query('id')
93 .optional() 116 .optional()
94 .custom(isIdValid).withMessage('Should have a valid id'), 117 .custom(isIdValid).withMessage('Should have a valid id'),
118 query('filter')
119 .optional()
120 .custom(isAbuseFilterValid)
121 .withMessage('Should have a valid filter'),
95 query('predefinedReason') 122 query('predefinedReason')
96 .optional() 123 .optional()
97 .custom(isAbusePredefinedReasonValid) 124 .custom(isAbusePredefinedReasonValid)
@@ -151,10 +178,7 @@ const videoAbuseReportValidator = [
151 .optional() 178 .optional()
152 .customSanitizer(toIntOrNull) 179 .customSanitizer(toIntOrNull)
153 .custom(isAbuseTimestampValid) 180 .custom(isAbuseTimestampValid)
154 .withMessage('Should have valid ending time value') 181 .withMessage('Should have valid ending time value'),
155 .bail()
156 .custom(isAbuseTimestampCoherent)
157 .withMessage('Should have a startAt timestamp beginning before endAt'),
158 182
159 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 183 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
160 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) 184 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index ef019fcf9..77f5c6ff3 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -3,13 +3,16 @@ import { body, param } from 'express-validator'
3import { MUserAccountUrl } from '@server/types/models' 3import { MUserAccountUrl } from '@server/types/models'
4import { UserRight } from '../../../../shared' 4import { UserRight } from '../../../../shared'
5import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 5import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
6import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' 6import {
7 doesVideoCommentExist,
8 doesVideoCommentThreadExist,
9 isValidVideoCommentText
10} from '../../../helpers/custom-validators/video-comments'
7import { logger } from '../../../helpers/logger' 11import { logger } from '../../../helpers/logger'
8import { doesVideoExist } from '../../../helpers/middlewares' 12import { doesVideoExist } from '../../../helpers/middlewares'
9import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' 13import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
10import { Hooks } from '../../../lib/plugins/hooks' 14import { Hooks } from '../../../lib/plugins/hooks'
11import { VideoCommentModel } from '../../../models/video/video-comment' 15import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
12import { MCommentOwnerVideoReply, MVideo, MVideoFullLight, MVideoId } from '../../../types/models/video'
13import { areValidationErrors } from '../utils' 16import { areValidationErrors } from '../utils'
14 17
15const listVideoCommentThreadsValidator = [ 18const listVideoCommentThreadsValidator = [
@@ -120,67 +123,10 @@ export {
120 123
121// --------------------------------------------------------------------------- 124// ---------------------------------------------------------------------------
122 125
123async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
124 const id = parseInt(idArg + '', 10)
125 const videoComment = await VideoCommentModel.loadById(id)
126
127 if (!videoComment) {
128 res.status(404)
129 .json({ error: 'Video comment thread not found' })
130 .end()
131
132 return false
133 }
134
135 if (videoComment.videoId !== video.id) {
136 res.status(400)
137 .json({ error: 'Video comment is not associated to this video.' })
138 .end()
139
140 return false
141 }
142
143 if (videoComment.inReplyToCommentId !== null) {
144 res.status(400)
145 .json({ error: 'Video comment is not a thread.' })
146 .end()
147
148 return false
149 }
150
151 res.locals.videoCommentThread = videoComment
152 return true
153}
154
155async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
156 const id = parseInt(idArg + '', 10)
157 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
158
159 if (!videoComment) {
160 res.status(404)
161 .json({ error: 'Video comment thread not found' })
162 .end()
163
164 return false
165 }
166
167 if (videoComment.videoId !== video.id) {
168 res.status(400)
169 .json({ error: 'Video comment is not associated to this video.' })
170 .end()
171
172 return false
173 }
174
175 res.locals.videoCommentFull = videoComment
176 return true
177}
178
179function isVideoCommentsEnabled (video: MVideo, res: express.Response) { 126function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
180 if (video.commentsEnabled !== true) { 127 if (video.commentsEnabled !== true) {
181 res.status(409) 128 res.status(409)
182 .json({ error: 'Video comments are disabled for this video.' }) 129 .json({ error: 'Video comments are disabled for this video.' })
183 .end()
184 130
185 return false 131 return false
186 } 132 }
@@ -192,7 +138,7 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC
192 if (videoComment.isDeleted()) { 138 if (videoComment.isDeleted()) {
193 res.status(409) 139 res.status(409)
194 .json({ error: 'This comment is already deleted' }) 140 .json({ error: 'This comment is already deleted' })
195 .end() 141
196 return false 142 return false
197 } 143 }
198 144
@@ -240,7 +186,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
240 if (!acceptedResult || acceptedResult.accepted !== true) { 186 if (!acceptedResult || acceptedResult.accepted !== true) {
241 logger.info('Refused local comment.', { acceptedResult, acceptParameters }) 187 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
242 res.status(403) 188 res.status(403)
243 .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) 189 .json({ error: acceptedResult.errorMessage || 'Refused local comment' })
244 190
245 return false 191 return false
246 } 192 }
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts
index 4f99f9c9b..087c77bd3 100644
--- a/server/models/abuse/abuse.ts
+++ b/server/models/abuse/abuse.ts
@@ -19,16 +19,17 @@ import {
19import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' 19import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
20import { 20import {
21 Abuse, 21 Abuse,
22 AbuseFilter,
22 AbuseObject, 23 AbuseObject,
23 AbusePredefinedReasons, 24 AbusePredefinedReasons,
24 abusePredefinedReasonsMap, 25 abusePredefinedReasonsMap,
25 AbusePredefinedReasonsString, 26 AbusePredefinedReasonsString,
26 AbuseState, 27 AbuseState,
27 AbuseVideoIs, 28 AbuseVideoIs,
28 VideoAbuse 29 VideoAbuse,
30 VideoCommentAbuse
29} from '@shared/models' 31} from '@shared/models'
30import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter' 32import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
31import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
32import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' 33import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
33import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' 34import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
34import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' 35import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
@@ -38,6 +39,7 @@ import { VideoBlacklistModel } from '../video/video-blacklist'
38import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' 39import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
39import { VideoAbuseModel } from './video-abuse' 40import { VideoAbuseModel } from './video-abuse'
40import { VideoCommentAbuseModel } from './video-comment-abuse' 41import { VideoCommentAbuseModel } from './video-comment-abuse'
42import { VideoCommentModel } from '../video/video-comment'
41 43
42export enum ScopeNames { 44export enum ScopeNames {
43 FOR_API = 'FOR_API' 45 FOR_API = 'FOR_API'
@@ -66,19 +68,18 @@ export enum ScopeNames {
66 serverAccountId: number 68 serverAccountId: number
67 userAccountId: number 69 userAccountId: number
68 }) => { 70 }) => {
69 const onlyBlacklisted = options.videoIs === 'blacklisted' 71 const whereAnd: WhereOptions[] = []
70 const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel)
71 72
72 const where = { 73 whereAnd.push({
73 reporterAccountId: { 74 reporterAccountId: {
74 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') 75 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
75 } 76 }
76 } 77 })
77 78
78 if (options.search) { 79 if (options.search) {
79 const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%') 80 const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%')
80 81
81 Object.assign(where, { 82 whereAnd.push({
82 [Op.or]: [ 83 [Op.or]: [
83 { 84 {
84 [Op.and]: [ 85 [Op.and]: [
@@ -110,11 +111,11 @@ export enum ScopeNames {
110 }) 111 })
111 } 112 }
112 113
113 if (options.id) Object.assign(where, { id: options.id }) 114 if (options.id) whereAnd.push({ id: options.id })
114 if (options.state) Object.assign(where, { state: options.state }) 115 if (options.state) whereAnd.push({ state: options.state })
115 116
116 if (options.videoIs === 'deleted') { 117 if (options.videoIs === 'deleted') {
117 Object.assign(where, { 118 whereAnd.push({
118 '$VideoAbuse.deletedVideo$': { 119 '$VideoAbuse.deletedVideo$': {
119 [Op.not]: null 120 [Op.not]: null
120 } 121 }
@@ -122,13 +123,23 @@ export enum ScopeNames {
122 } 123 }
123 124
124 if (options.predefinedReasonId) { 125 if (options.predefinedReasonId) {
125 Object.assign(where, { 126 whereAnd.push({
126 predefinedReasons: { 127 predefinedReasons: {
127 [Op.contains]: [ options.predefinedReasonId ] 128 [Op.contains]: [ options.predefinedReasonId ]
128 } 129 }
129 }) 130 })
130 } 131 }
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
132 return { 143 return {
133 attributes: { 144 attributes: {
134 include: [ 145 include: [
@@ -223,6 +234,23 @@ export enum ScopeNames {
223 where: searchAttribute(options.searchReportee, 'name') 234 where: searchAttribute(options.searchReportee, 'name')
224 }, 235 },
225 { 236 {
237 model: VideoCommentAbuseModel.unscoped(),
238 required: options.filter === 'comment',
239 include: [
240 {
241 model: VideoCommentModel.unscoped(),
242 required: false,
243 include: [
244 {
245 model: VideoModel.unscoped(),
246 attributes: [ 'name', 'id', 'uuid' ],
247 required: true
248 }
249 ]
250 }
251 ]
252 },
253 {
226 model: VideoAbuseModel, 254 model: VideoAbuseModel,
227 required: options.filter === 'video' || !!options.videoIs || videoRequired, 255 required: options.filter === 'video' || !!options.videoIs || videoRequired,
228 include: [ 256 include: [
@@ -241,8 +269,7 @@ export enum ScopeNames {
241 include: [ 269 include: [
242 { 270 {
243 model: AccountModel.scope(AccountScopeNames.SUMMARY), 271 model: AccountModel.scope(AccountScopeNames.SUMMARY),
244 required: true, 272 required: true
245 where: searchAttribute(options.searchReportee, 'name')
246 } 273 }
247 ] 274 ]
248 }, 275 },
@@ -256,7 +283,9 @@ export enum ScopeNames {
256 ] 283 ]
257 } 284 }
258 ], 285 ],
259 where 286 where: {
287 [Op.and]: whereAnd
288 }
260 } 289 }
261 } 290 }
262})) 291}))
@@ -348,6 +377,7 @@ export class AbuseModel extends Model<AbuseModel> {
348 }) 377 })
349 VideoAbuse: VideoAbuseModel 378 VideoAbuse: VideoAbuseModel
350 379
380 // FIXME: deprecated in 2.3. Remove these validators
351 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> { 381 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
352 const videoWhere: WhereOptions = {} 382 const videoWhere: WhereOptions = {}
353 383
@@ -369,6 +399,16 @@ export class AbuseModel extends Model<AbuseModel> {
369 return AbuseModel.findOne(query) 399 return AbuseModel.findOne(query)
370 } 400 }
371 401
402 static loadById (id: number): Bluebird<MAbuse> {
403 const query = {
404 where: {
405 id
406 }
407 }
408
409 return AbuseModel.findOne(query)
410 }
411
372 static listForApi (parameters: { 412 static listForApi (parameters: {
373 start: number 413 start: number
374 count: number 414 count: number
@@ -454,6 +494,7 @@ export class AbuseModel extends Model<AbuseModel> {
454 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number 494 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
455 495
456 let video: VideoAbuse 496 let video: VideoAbuse
497 let comment: VideoCommentAbuse
457 498
458 if (this.VideoAbuse) { 499 if (this.VideoAbuse) {
459 const abuseModel = this.VideoAbuse 500 const abuseModel = this.VideoAbuse
@@ -475,6 +516,24 @@ export class AbuseModel extends Model<AbuseModel> {
475 } 516 }
476 } 517 }
477 518
519 if (this.VideoCommentAbuse) {
520 const abuseModel = this.VideoCommentAbuse
521 const entity = abuseModel.VideoComment || abuseModel.deletedComment
522
523 comment = {
524 id: entity.id,
525 text: entity.text,
526
527 deleted: !abuseModel.VideoComment,
528
529 video: {
530 id: entity.Video.id,
531 name: entity.Video.name,
532 uuid: entity.Video.uuid
533 }
534 }
535 }
536
478 return { 537 return {
479 id: this.id, 538 id: this.id,
480 reason: this.reason, 539 reason: this.reason,
@@ -490,7 +549,7 @@ export class AbuseModel extends Model<AbuseModel> {
490 moderationComment: this.moderationComment, 549 moderationComment: this.moderationComment,
491 550
492 video, 551 video,
493 comment: null, 552 comment,
494 553
495 createdAt: this.createdAt, 554 createdAt: this.createdAt,
496 updatedAt: this.updatedAt, 555 updatedAt: this.updatedAt,
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts
index b4cc2762e..de9f4d5fd 100644
--- a/server/models/abuse/video-comment-abuse.ts
+++ b/server/models/abuse/video-comment-abuse.ts
@@ -25,7 +25,7 @@ export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> {
25 @AllowNull(true) 25 @AllowNull(true)
26 @Default(null) 26 @Default(null)
27 @Column(DataType.JSONB) 27 @Column(DataType.JSONB)
28 deletedComment: VideoComment 28 deletedComment: VideoComment & { Video: { name: string, id: number, uuid: string }}
29 29
30 @ForeignKey(() => AbuseModel) 30 @ForeignKey(() => AbuseModel)
31 @Column 31 @Column
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 90625d987..fb6078ed8 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,7 +1,22 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { uniq } from 'lodash' 2import { uniq } from 'lodash'
3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' 3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 4import {
5 AllowNull,
6 BeforeDestroy,
7 BelongsTo,
8 Column,
9 CreatedAt,
10 DataType,
11 ForeignKey,
12 HasMany,
13 Is,
14 Model,
15 Scopes,
16 Table,
17 UpdatedAt
18} from 'sequelize-typescript'
19import { logger } from '@server/helpers/logger'
5import { getServerActor } from '@server/models/application/application' 20import { getServerActor } from '@server/models/application/application'
6import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' 21import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
7import { VideoPrivacy } from '@shared/models' 22import { VideoPrivacy } from '@shared/models'
@@ -24,6 +39,7 @@ import {
24 MCommentOwnerVideoReply, 39 MCommentOwnerVideoReply,
25 MVideoImmutable 40 MVideoImmutable
26} from '../../types/models/video' 41} from '../../types/models/video'
42import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
27import { AccountModel } from '../account/account' 43import { AccountModel } from '../account/account'
28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 44import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
29import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' 45import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
@@ -224,6 +240,53 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
224 }) 240 })
225 Account: AccountModel 241 Account: AccountModel
226 242
243 @HasMany(() => VideoCommentAbuseModel, {
244 foreignKey: {
245 name: 'commentId',
246 allowNull: true
247 },
248 onDelete: 'set null'
249 })
250 CommentAbuses: VideoCommentAbuseModel[]
251
252 @BeforeDestroy
253 static async saveEssentialDataToAbuses (instance: VideoCommentModel, options) {
254 const tasks: Promise<any>[] = []
255
256 if (!Array.isArray(instance.CommentAbuses)) {
257 instance.CommentAbuses = await instance.$get('CommentAbuses')
258
259 if (instance.CommentAbuses.length === 0) return undefined
260 }
261
262 if (!instance.Video) {
263 instance.Video = await instance.$get('Video')
264 }
265
266 logger.info('Saving video comment %s for abuse.', instance.url)
267
268 const details = Object.assign(instance.toFormattedJSON(), {
269 Video: {
270 id: instance.Video.id,
271 name: instance.Video.name,
272 uuid: instance.Video.uuid
273 }
274 })
275
276 for (const abuse of instance.CommentAbuses) {
277 abuse.deletedComment = details
278
279 tasks.push(abuse.save({ transaction: options.transaction }))
280 }
281
282 Promise.all(tasks)
283 .catch(err => {
284 logger.error('Some errors when saving details of comment %s in its abuses before destroy hook.', instance.url, { err })
285 })
286
287 return undefined
288 }
289
227 static loadById (id: number, t?: Transaction): Bluebird<MComment> { 290 static loadById (id: number, t?: Transaction): Bluebird<MComment> {
228 const query: FindOptions = { 291 const query: FindOptions = {
229 where: { 292 where: {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 272bba0e1..43609587c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -803,14 +803,14 @@ export class VideoModel extends Model<VideoModel> {
803 static async saveEssentialDataToAbuses (instance: VideoModel, options) { 803 static async saveEssentialDataToAbuses (instance: VideoModel, options) {
804 const tasks: Promise<any>[] = [] 804 const tasks: Promise<any>[] = []
805 805
806 logger.info('Saving video abuses details of video %s.', instance.url)
807
808 if (!Array.isArray(instance.VideoAbuses)) { 806 if (!Array.isArray(instance.VideoAbuses)) {
809 instance.VideoAbuses = await instance.$get('VideoAbuses') 807 instance.VideoAbuses = await instance.$get('VideoAbuses')
810 808
811 if (instance.VideoAbuses.length === 0) return undefined 809 if (instance.VideoAbuses.length === 0) return undefined
812 } 810 }
813 811
812 logger.info('Saving video abuses details of video %s.', instance.url)
813
814 const details = instance.toFormattedDetailsJSON() 814 const details = instance.toFormattedDetailsJSON()
815 815
816 for (const abuse of instance.VideoAbuses) { 816 for (const abuse of instance.VideoAbuses) {
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts
new file mode 100644
index 000000000..ba7c0833f
--- /dev/null
+++ b/server/tests/api/check-params/abuses.ts
@@ -0,0 +1,271 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { AbuseCreate, AbuseState } from '@shared/models'
5import {
6 cleanupTests,
7 createUser,
8 deleteAbuse,
9 flushAndRunServer,
10 makeGetRequest,
11 makePostBodyRequest,
12 ServerInfo,
13 setAccessTokensToServers,
14 updateAbuse,
15 uploadVideo,
16 userLogin
17} from '../../../../shared/extra-utils'
18import {
19 checkBadCountPagination,
20 checkBadSortPagination,
21 checkBadStartPagination
22} from '../../../../shared/extra-utils/requests/check-api-params'
23
24// FIXME: deprecated in 2.3. Remove this controller
25
26describe('Test video abuses API validators', function () {
27 const basePath = '/api/v1/abuses/'
28
29 let server: ServerInfo
30 let userAccessToken = ''
31 let abuseId: number
32
33 // ---------------------------------------------------------------
34
35 before(async function () {
36 this.timeout(30000)
37
38 server = await flushAndRunServer(1)
39
40 await setAccessTokensToServers([ server ])
41
42 const username = 'user1'
43 const password = 'my super password'
44 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
45 userAccessToken = await userLogin(server, { username, password })
46
47 const res = await uploadVideo(server.url, server.accessToken, {})
48 server.video = res.body.video
49 })
50
51 describe('When listing abuses', function () {
52 const path = basePath
53
54 it('Should fail with a bad start pagination', async function () {
55 await checkBadStartPagination(server.url, path, server.accessToken)
56 })
57
58 it('Should fail with a bad count pagination', async function () {
59 await checkBadCountPagination(server.url, path, server.accessToken)
60 })
61
62 it('Should fail with an incorrect sort', async function () {
63 await checkBadSortPagination(server.url, path, server.accessToken)
64 })
65
66 it('Should fail with a non authenticated user', async function () {
67 await makeGetRequest({
68 url: server.url,
69 path,
70 statusCodeExpected: 401
71 })
72 })
73
74 it('Should fail with a non admin user', async function () {
75 await makeGetRequest({
76 url: server.url,
77 path,
78 token: userAccessToken,
79 statusCodeExpected: 403
80 })
81 })
82
83 it('Should fail with a bad id filter', async function () {
84 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } })
85 })
86
87 it('Should fail with a bad filter', async function () {
88 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } })
89 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } })
90 })
91
92 it('Should fail with bad predefined reason', async function () {
93 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } })
94 })
95
96 it('Should fail with a bad state filter', async function () {
97 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } })
98 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } })
99 })
100
101 it('Should fail with a bad videoIs filter', async function () {
102 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } })
103 })
104
105 it('Should succeed with the correct params', async function () {
106 const query = {
107 id: 13,
108 predefinedReason: 'violentOrRepulsive',
109 filter: 'comment',
110 state: 2,
111 videoIs: 'deleted'
112 }
113
114 await makeGetRequest({ url: server.url, path, token: server.accessToken, query, statusCodeExpected: 200 })
115 })
116 })
117
118 describe('When reporting an abuse', function () {
119 const path = basePath
120
121 it('Should fail with nothing', async function () {
122 const fields = {}
123 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
124 })
125
126 it('Should fail with a wrong video', async function () {
127 const fields = { video: { id: 'blabla' }, reason: 'my super reason' }
128 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
129 })
130
131 it('Should fail with an unknown video', async function () {
132 const fields = { video: { id: 42 }, reason: 'my super reason' }
133 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
134 })
135
136 it('Should fail with a wrong comment', async function () {
137 const fields = { comment: { id: 'blabla' }, reason: 'my super reason' }
138 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
139 })
140
141 it('Should fail with an unknown comment', async function () {
142 const fields = { comment: { id: 42 }, reason: 'my super reason' }
143 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
144 })
145
146 it('Should fail with a wrong account', async function () {
147 const fields = { account: { id: 'blabla' }, reason: 'my super reason' }
148 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
149 })
150
151 it('Should fail with an unknown account', async function () {
152 const fields = { account: { id: 42 }, reason: 'my super reason' }
153 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
154 })
155
156 it('Should fail with not account, comment or video', async function () {
157 const fields = { reason: 'my super reason' }
158 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 400 })
159 })
160
161 it('Should fail with a non authenticated user', async function () {
162 const fields = { video: { id: server.video.id }, reason: 'my super reason' }
163
164 await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 })
165 })
166
167 it('Should fail with a reason too short', async function () {
168 const fields = { video: { id: server.video.id }, reason: 'h' }
169
170 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
171 })
172
173 it('Should fail with a too big reason', async function () {
174 const fields = { video: { id: server.video.id }, reason: 'super'.repeat(605) }
175
176 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
177 })
178
179 it('Should succeed with the correct parameters (basic)', async function () {
180 const fields: AbuseCreate = { video: { id: server.video.id }, reason: 'my super reason' }
181
182 const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
183 abuseId = res.body.abuse.id
184 })
185
186 it('Should fail with a wrong predefined reason', async function () {
187 const fields = { video: { id: server.video.id }, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
188
189 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
190 })
191
192 it('Should fail with negative timestamps', async function () {
193 const fields = { video: { id: server.video.id, startAt: -1 }, reason: 'my super reason' }
194
195 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
196 })
197
198 it('Should fail mith misordered startAt/endAt', async function () {
199 const fields = { video: { id: server.video.id, startAt: 5, endAt: 1 }, reason: 'my super reason' }
200
201 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
202 })
203
204 it('Should succeed with the corret parameters (advanced)', async function () {
205 const fields: AbuseCreate = {
206 video: {
207 id: server.video.id,
208 startAt: 1,
209 endAt: 5
210 },
211 reason: 'my super reason',
212 predefinedReasons: [ 'serverRules' ]
213 }
214
215 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
216 })
217 })
218
219 describe('When updating an abuse', function () {
220
221 it('Should fail with a non authenticated user', async function () {
222 await updateAbuse(server.url, 'blabla', abuseId, {}, 401)
223 })
224
225 it('Should fail with a non admin user', async function () {
226 await updateAbuse(server.url, userAccessToken, abuseId, {}, 403)
227 })
228
229 it('Should fail with a bad abuse id', async function () {
230 await updateAbuse(server.url, server.accessToken, 45, {}, 404)
231 })
232
233 it('Should fail with a bad state', async function () {
234 const body = { state: 5 }
235 await updateAbuse(server.url, server.accessToken, abuseId, body, 400)
236 })
237
238 it('Should fail with a bad moderation comment', async function () {
239 const body = { moderationComment: 'b'.repeat(3001) }
240 await updateAbuse(server.url, server.accessToken, abuseId, body, 400)
241 })
242
243 it('Should succeed with the correct params', async function () {
244 const body = { state: AbuseState.ACCEPTED }
245 await updateAbuse(server.url, server.accessToken, abuseId, body)
246 })
247 })
248
249 describe('When deleting a video abuse', function () {
250
251 it('Should fail with a non authenticated user', async function () {
252 await deleteAbuse(server.url, 'blabla', abuseId, 401)
253 })
254
255 it('Should fail with a non admin user', async function () {
256 await deleteAbuse(server.url, userAccessToken, abuseId, 403)
257 })
258
259 it('Should fail with a bad abuse id', async function () {
260 await deleteAbuse(server.url, server.accessToken, 45, 404)
261 })
262
263 it('Should succeed with the correct params', async function () {
264 await deleteAbuse(server.url, server.accessToken, abuseId)
265 })
266 })
267
268 after(async function () {
269 await cleanupTests([ server ])
270 })
271})
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 93ffd98b1..0ee1f27aa 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -1,3 +1,4 @@
1import './abuses'
1import './accounts' 2import './accounts'
2import './blocklist' 3import './blocklist'
3import './bulk' 4import './bulk'
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index f122baef4..3b361ca79 100644
--- a/server/tests/api/check-params/video-abuses.ts
+++ b/server/tests/api/check-params/video-abuses.ts
@@ -152,12 +152,6 @@ describe('Test video abuses API validators', function () {
152 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 152 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
153 }) 153 })
154 154
155 it('Should fail mith misordered startAt/endAt', async function () {
156 const fields = { reason: 'my super reason', startAt: 5, endAt: 1 }
157
158 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
159 })
160
161 it('Should succeed with the corret parameters (advanced)', async function () { 155 it('Should succeed with the corret parameters (advanced)', async function () {
162 const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 } 156 const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 }
163 157
diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts
index abbc93d6f..8e12be874 100644
--- a/server/types/models/moderation/abuse.ts
+++ b/server/types/models/moderation/abuse.ts
@@ -3,7 +3,7 @@ import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse
3import { PickWith } from '@shared/core-utils' 3import { PickWith } from '@shared/core-utils'
4import { AbuseModel } from '../../../models/abuse/abuse' 4import { AbuseModel } from '../../../models/abuse/abuse'
5import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account' 5import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account'
6import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo } from '../video' 6import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video'
7import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' 7import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
8 8
9type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M> 9type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M>
@@ -51,6 +51,10 @@ export type MCommentAbuseUrl =
51 MCommentAbuse & 51 MCommentAbuse &
52 UseCommentAbuse<'VideoComment', MCommentUrl> 52 UseCommentAbuse<'VideoComment', MCommentUrl>
53 53
54export type MCommentAbuseFormattable =
55 MCommentAbuse &
56 UseCommentAbuse<'VideoComment', MComment & PickWith<MCommentVideo, 'Video', Pick<MVideo, 'id' | 'uuid' | 'name'>>>
57
54// ############################################################################ 58// ############################################################################
55 59
56export type MAbuseId = Pick<AbuseModel, 'id'> 60export type MAbuseId = Pick<AbuseModel, 'id'>
@@ -94,4 +98,5 @@ export type MAbuseFull =
94export type MAbuseFormattable = 98export type MAbuseFormattable =
95 MAbuse & 99 MAbuse &
96 Use<'ReporterAccount', MAccountFormattable> & 100 Use<'ReporterAccount', MAccountFormattable> &
97 Use<'VideoAbuse', MVideoAbuseFormattable> 101 Use<'VideoAbuse', MVideoAbuseFormattable> &
102 Use<'VideoCommentAbuse', MCommentAbuseFormattable>
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts
index 7595e6d86..b1afffcd4 100644
--- a/server/typings/express/index.d.ts
+++ b/server/typings/express/index.d.ts
@@ -91,6 +91,7 @@ declare module 'express' {
91 91
92 accountVideoRate?: MAccountVideoRateAccountVideo 92 accountVideoRate?: MAccountVideoRateAccountVideo
93 93
94 videoComment?: MComment
94 videoCommentFull?: MCommentOwnerVideoReply 95 videoCommentFull?: MCommentOwnerVideoReply
95 videoCommentThread?: MComment 96 videoCommentThread?: MComment
96 97
diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts
index 48a51e2b8..1af703f92 100644
--- a/shared/extra-utils/moderation/abuses.ts
+++ b/shared/extra-utils/moderation/abuses.ts
@@ -1,25 +1,57 @@
1import * as request from 'supertest'
2import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
3import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
4 1
5function reportAbuse ( 2import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
6 url: string, 3import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
7 token: string, 4
8 videoId: number | string, 5function reportAbuse (options: {
9 reason: string, 6 url: string
10 predefinedReasons?: AbusePredefinedReasonsString[], 7 token: string
11 startAt?: number, 8
12 endAt?: number, 9 reason: string
13 specialStatus = 200 10
14) { 11 accountId?: number
15 const path = '/api/v1/videos/' + videoId + '/abuse' 12 videoId?: number
16 13 commentId?: number
17 return request(url) 14
18 .post(path) 15 predefinedReasons?: AbusePredefinedReasonsString[]
19 .set('Accept', 'application/json') 16
20 .set('Authorization', 'Bearer ' + token) 17 startAt?: number
21 .send({ reason, predefinedReasons, startAt, endAt }) 18 endAt?: number
22 .expect(specialStatus) 19
20 statusCodeExpected?: number
21}) {
22 const path = '/api/v1/abuses'
23
24 const video = options.videoId ? {
25 id: options.videoId,
26 startAt: options.startAt,
27 endAt: options.endAt
28 } : undefined
29
30 const comment = options.commentId ? {
31 id: options.commentId
32 } : undefined
33
34 const account = options.accountId ? {
35 id: options.accountId
36 } : undefined
37
38 const body = {
39 account,
40 video,
41 comment,
42
43 reason: options.reason,
44 predefinedReasons: options.predefinedReasons
45 }
46
47 return makePostBodyRequest({
48 url: options.url,
49 path,
50 token: options.token,
51
52 fields: body,
53 statusCodeExpected: options.statusCodeExpected || 200
54 })
23} 55}
24 56
25function getAbusesList (options: { 57function getAbusesList (options: {
@@ -28,6 +60,7 @@ function getAbusesList (options: {
28 id?: number 60 id?: number
29 predefinedReason?: AbusePredefinedReasonsString 61 predefinedReason?: AbusePredefinedReasonsString
30 search?: string 62 search?: string
63 filter?: AbuseFilter,
31 state?: AbuseState 64 state?: AbuseState
32 videoIs?: AbuseVideoIs 65 videoIs?: AbuseVideoIs
33 searchReporter?: string 66 searchReporter?: string
@@ -41,6 +74,7 @@ function getAbusesList (options: {
41 id, 74 id,
42 predefinedReason, 75 predefinedReason,
43 search, 76 search,
77 filter,
44 state, 78 state,
45 videoIs, 79 videoIs,
46 searchReporter, 80 searchReporter,
@@ -48,7 +82,7 @@ function getAbusesList (options: {
48 searchVideo, 82 searchVideo,
49 searchVideoChannel 83 searchVideoChannel
50 } = options 84 } = options
51 const path = '/api/v1/videos/abuse' 85 const path = '/api/v1/abuses'
52 86
53 const query = { 87 const query = {
54 sort: 'createdAt', 88 sort: 'createdAt',
@@ -56,6 +90,7 @@ function getAbusesList (options: {
56 predefinedReason, 90 predefinedReason,
57 search, 91 search,
58 state, 92 state,
93 filter,
59 videoIs, 94 videoIs,
60 searchReporter, 95 searchReporter,
61 searchReportee, 96 searchReportee,
@@ -75,12 +110,11 @@ function getAbusesList (options: {
75function updateAbuse ( 110function updateAbuse (
76 url: string, 111 url: string,
77 token: string, 112 token: string,
78 videoId: string | number, 113 abuseId: number,
79 videoAbuseId: number,
80 body: AbuseUpdate, 114 body: AbuseUpdate,
81 statusCodeExpected = 204 115 statusCodeExpected = 204
82) { 116) {
83 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId 117 const path = '/api/v1/abuses/' + abuseId
84 118
85 return makePutBodyRequest({ 119 return makePutBodyRequest({
86 url, 120 url,
@@ -91,8 +125,8 @@ function updateAbuse (
91 }) 125 })
92} 126}
93 127
94function deleteAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) { 128function deleteAbuse (url: string, token: string, abuseId: number, statusCodeExpected = 204) {
95 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId 129 const path = '/api/v1/abuses/' + abuseId
96 130
97 return makeDeleteRequest({ 131 return makeDeleteRequest({
98 url, 132 url,
diff --git a/shared/models/moderation/abuse/abuse-create.model.ts b/shared/models/moderation/abuse/abuse-create.model.ts
index c0d04e46d..b0358dbb9 100644
--- a/shared/models/moderation/abuse/abuse-create.model.ts
+++ b/shared/models/moderation/abuse/abuse-create.model.ts
@@ -1,11 +1,14 @@
1import { AbusePredefinedReasonsString } from './abuse-reason.model' 1import { AbusePredefinedReasonsString } from './abuse-reason.model'
2 2
3export interface AbuseCreate { 3export interface AbuseCreate {
4 accountId: number
5
6 reason: string 4 reason: string
5
7 predefinedReasons?: AbusePredefinedReasonsString[] 6 predefinedReasons?: AbusePredefinedReasonsString[]
8 7
8 account?: {
9 id: number
10 }
11
9 video?: { 12 video?: {
10 id: number 13 id: number
11 startAt?: number 14 startAt?: number
diff --git a/shared/models/moderation/abuse/abuse-filter.ts b/shared/models/moderation/abuse/abuse-filter.ts
deleted file mode 100644
index 03303bbab..000000000
--- a/shared/models/moderation/abuse/abuse-filter.ts
+++ /dev/null
@@ -1 +0,0 @@
1export type AbuseFilter = 'video' | 'comment'
diff --git a/shared/models/moderation/abuse/abuse-filter.type.ts b/shared/models/moderation/abuse/abuse-filter.type.ts
new file mode 100644
index 000000000..7dafc6d77
--- /dev/null
+++ b/shared/models/moderation/abuse/abuse-filter.type.ts
@@ -0,0 +1 @@
export type AbuseFilter = 'video' | 'comment' | 'account'
diff --git a/shared/models/moderation/abuse/abuse.model.ts b/shared/models/moderation/abuse/abuse.model.ts
index 9ff150c4a..a120803e6 100644
--- a/shared/models/moderation/abuse/abuse.model.ts
+++ b/shared/models/moderation/abuse/abuse.model.ts
@@ -9,6 +9,7 @@ export interface VideoAbuse {
9 name: string 9 name: string
10 uuid: string 10 uuid: string
11 nsfw: boolean 11 nsfw: boolean
12
12 deleted: boolean 13 deleted: boolean
13 blacklisted: boolean 14 blacklisted: boolean
14 15
@@ -21,8 +22,15 @@ export interface VideoAbuse {
21 22
22export interface VideoCommentAbuse { 23export interface VideoCommentAbuse {
23 id: number 24 id: number
24 account?: Account 25
26 video: {
27 id: number
28 name: string
29 uuid: string
30 }
31
25 text: string 32 text: string
33
26 deleted: boolean 34 deleted: boolean
27} 35}
28 36
diff --git a/shared/models/moderation/abuse/index.ts b/shared/models/moderation/abuse/index.ts
index 32a6b4e6c..55046426a 100644
--- a/shared/models/moderation/abuse/index.ts
+++ b/shared/models/moderation/abuse/index.ts
@@ -1,4 +1,5 @@
1export * from './abuse-create.model' 1export * from './abuse-create.model'
2export * from './abuse-filter.type'
2export * from './abuse-reason.model' 3export * from './abuse-reason.model'
3export * from './abuse-state.model' 4export * from './abuse-state.model'
4export * from './abuse-update.model' 5export * from './abuse-update.model'