aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/abuse.ts62
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts2
-rw-r--r--server/helpers/custom-validators/video-abuses.ts43
-rw-r--r--server/helpers/custom-validators/videos.ts7
-rw-r--r--server/initializers/constants.ts14
-rw-r--r--server/initializers/migrations/0250-video-abuse-state.ts47
-rw-r--r--server/lib/activitypub/process/process-create.ts5
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/video-abuses.ts71
-rw-r--r--server/middlewares/validators/videos.ts17
-rw-r--r--server/models/video/video-abuse.ts54
-rw-r--r--server/models/video/video-import.ts1
-rw-r--r--server/tests/api/check-params/video-abuses.ts115
-rw-r--r--server/tests/api/videos/video-abuse.ts50
-rw-r--r--server/tests/utils/videos/video-abuses.ts38
15 files changed, 463 insertions, 64 deletions
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index 7782fc639..59bdf6257 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight, VideoAbuseCreate } from '../../../../shared' 2import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers'
@@ -12,8 +12,10 @@ import {
12 paginationValidator, 12 paginationValidator,
13 setDefaultPagination, 13 setDefaultPagination,
14 setDefaultSort, 14 setDefaultSort,
15 videoAbuseGetValidator,
15 videoAbuseReportValidator, 16 videoAbuseReportValidator,
16 videoAbusesSortValidator 17 videoAbusesSortValidator,
18 videoAbuseUpdateValidator
17} from '../../../middlewares' 19} from '../../../middlewares'
18import { AccountModel } from '../../../models/account/account' 20import { AccountModel } from '../../../models/account/account'
19import { VideoModel } from '../../../models/video/video' 21import { VideoModel } from '../../../models/video/video'
@@ -32,11 +34,23 @@ abuseVideoRouter.get('/abuse',
32 setDefaultPagination, 34 setDefaultPagination,
33 asyncMiddleware(listVideoAbuses) 35 asyncMiddleware(listVideoAbuses)
34) 36)
35abuseVideoRouter.post('/:id/abuse', 37abuseVideoRouter.put('/:videoId/abuse/:id',
38 authenticate,
39 ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
40 asyncMiddleware(videoAbuseUpdateValidator),
41 asyncRetryTransactionMiddleware(updateVideoAbuse)
42)
43abuseVideoRouter.post('/:videoId/abuse',
36 authenticate, 44 authenticate,
37 asyncMiddleware(videoAbuseReportValidator), 45 asyncMiddleware(videoAbuseReportValidator),
38 asyncRetryTransactionMiddleware(reportVideoAbuse) 46 asyncRetryTransactionMiddleware(reportVideoAbuse)
39) 47)
48abuseVideoRouter.delete('/:videoId/abuse/:id',
49 authenticate,
50 ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
51 asyncMiddleware(videoAbuseGetValidator),
52 asyncRetryTransactionMiddleware(deleteVideoAbuse)
53)
40 54
41// --------------------------------------------------------------------------- 55// ---------------------------------------------------------------------------
42 56
@@ -46,12 +60,39 @@ export {
46 60
47// --------------------------------------------------------------------------- 61// ---------------------------------------------------------------------------
48 62
49async function listVideoAbuses (req: express.Request, res: express.Response, next: express.NextFunction) { 63async function listVideoAbuses (req: express.Request, res: express.Response) {
50 const resultList = await VideoAbuseModel.listForApi(req.query.start, req.query.count, req.query.sort) 64 const resultList = await VideoAbuseModel.listForApi(req.query.start, req.query.count, req.query.sort)
51 65
52 return res.json(getFormattedObjects(resultList.data, resultList.total)) 66 return res.json(getFormattedObjects(resultList.data, resultList.total))
53} 67}
54 68
69async function updateVideoAbuse (req: express.Request, res: express.Response) {
70 const videoAbuse: VideoAbuseModel = res.locals.videoAbuse
71
72 if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment
73 if (req.body.state !== undefined) videoAbuse.state = req.body.state
74
75 await sequelizeTypescript.transaction(t => {
76 return videoAbuse.save({ transaction: t })
77 })
78
79 // Do not send the delete to other instances, we updated OUR copy of this video abuse
80
81 return res.type('json').status(204).end()
82}
83
84async function deleteVideoAbuse (req: express.Request, res: express.Response) {
85 const videoAbuse: VideoAbuseModel = res.locals.videoAbuse
86
87 await sequelizeTypescript.transaction(t => {
88 return videoAbuse.destroy({ transaction: t })
89 })
90
91 // Do not send the delete to other instances, we delete OUR copy of this video abuse
92
93 return res.type('json').status(204).end()
94}
95
55async function reportVideoAbuse (req: express.Request, res: express.Response) { 96async function reportVideoAbuse (req: express.Request, res: express.Response) {
56 const videoInstance = res.locals.video as VideoModel 97 const videoInstance = res.locals.video as VideoModel
57 const reporterAccount = res.locals.oauth.token.User.Account as AccountModel 98 const reporterAccount = res.locals.oauth.token.User.Account as AccountModel
@@ -60,10 +101,11 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
60 const abuseToCreate = { 101 const abuseToCreate = {
61 reporterAccountId: reporterAccount.id, 102 reporterAccountId: reporterAccount.id,
62 reason: body.reason, 103 reason: body.reason,
63 videoId: videoInstance.id 104 videoId: videoInstance.id,
105 state: VideoAbuseState.PENDING
64 } 106 }
65 107
66 await sequelizeTypescript.transaction(async t => { 108 const videoAbuse: VideoAbuseModel = await sequelizeTypescript.transaction(async t => {
67 const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) 109 const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
68 videoAbuseInstance.Video = videoInstance 110 videoAbuseInstance.Video = videoInstance
69 videoAbuseInstance.Account = reporterAccount 111 videoAbuseInstance.Account = reporterAccount
@@ -74,8 +116,12 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
74 } 116 }
75 117
76 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) 118 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON()))
77 logger.info('Abuse report for video %s created.', videoInstance.name) 119
120 return videoAbuseInstance
78 }) 121 })
79 122
80 return res.type('json').status(204).end() 123 logger.info('Abuse report for video %s created.', videoInstance.name)
124 return res.json({
125 videoAbuse: videoAbuse.toFormattedJSON()
126 }).end()
81} 127}
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index b8075f3c7..702c09842 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -3,7 +3,6 @@ import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers'
3import { peertubeTruncate } from '../../core-utils' 3import { peertubeTruncate } from '../../core-utils'
4import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' 4import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
5import { 5import {
6 isVideoAbuseReasonValid,
7 isVideoDurationValid, 6 isVideoDurationValid,
8 isVideoNameValid, 7 isVideoNameValid,
9 isVideoStateValid, 8 isVideoStateValid,
@@ -13,6 +12,7 @@ import {
13} from '../videos' 12} from '../videos'
14import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
15import { VideoState } from '../../../../shared/models/videos' 14import { VideoState } from '../../../../shared/models/videos'
15import { isVideoAbuseReasonValid } from '../video-abuses'
16 16
17function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { 17function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
18 return isBaseActivityValid(activity, 'Create') && 18 return isBaseActivityValid(activity, 'Create') &&
diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts
new file mode 100644
index 000000000..290efb149
--- /dev/null
+++ b/server/helpers/custom-validators/video-abuses.ts
@@ -0,0 +1,43 @@
1import { Response } from 'express'
2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers'
4import { exists } from './misc'
5import { VideoAbuseModel } from '../../models/video/video-abuse'
6
7const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
8
9function isVideoAbuseReasonValid (value: string) {
10 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
11}
12
13function isVideoAbuseModerationCommentValid (value: string) {
14 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
15}
16
17function isVideoAbuseStateValid (value: string) {
18 return exists(value) && VIDEO_ABUSE_STATES[ value ] !== undefined
19}
20
21async function isVideoAbuseExist (abuseId: number, videoId: number, res: Response) {
22 const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId)
23
24 if (videoAbuse === null) {
25 res.status(404)
26 .json({ error: 'Video abuse not found' })
27 .end()
28
29 return false
30 }
31
32 res.locals.videoAbuse = videoAbuse
33 return true
34}
35
36// ---------------------------------------------------------------------------
37
38export {
39 isVideoAbuseExist,
40 isVideoAbuseStateValid,
41 isVideoAbuseReasonValid,
42 isVideoAbuseModerationCommentValid
43}
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index f4c1c8b07..5e6cfe217 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -6,6 +6,7 @@ import * as validator from 'validator'
6import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' 6import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared'
7import { 7import {
8 CONSTRAINTS_FIELDS, 8 CONSTRAINTS_FIELDS,
9 VIDEO_ABUSE_STATES,
9 VIDEO_CATEGORIES, 10 VIDEO_CATEGORIES,
10 VIDEO_LICENCES, 11 VIDEO_LICENCES,
11 VIDEO_MIMETYPE_EXT, 12 VIDEO_MIMETYPE_EXT,
@@ -18,6 +19,7 @@ import { exists, isArray, isFileValid } from './misc'
18import { VideoChannelModel } from '../../models/video/video-channel' 19import { VideoChannelModel } from '../../models/video/video-channel'
19import { UserModel } from '../../models/account/user' 20import { UserModel } from '../../models/account/user'
20import * as magnetUtil from 'magnet-uri' 21import * as magnetUtil from 'magnet-uri'
22import { VideoAbuseModel } from '../../models/video/video-abuse'
21 23
22const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS 24const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
23const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 25const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
@@ -71,10 +73,6 @@ function isVideoTagsValid (tags: string[]) {
71 ) 73 )
72} 74}
73 75
74function isVideoAbuseReasonValid (value: string) {
75 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
76}
77
78function isVideoViewsValid (value: string) { 76function isVideoViewsValid (value: string) {
79 return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS) 77 return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
80} 78}
@@ -220,7 +218,6 @@ export {
220 isVideoTagsValid, 218 isVideoTagsValid,
221 isVideoFPSResolutionValid, 219 isVideoFPSResolutionValid,
222 isScheduleVideoUpdatePrivacyValid, 220 isScheduleVideoUpdatePrivacyValid,
223 isVideoAbuseReasonValid,
224 isVideoFile, 221 isVideoFile,
225 isVideoMagnetUriValid, 222 isVideoMagnetUriValid,
226 isVideoStateValid, 223 isVideoStateValid,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index ea561b686..a008bf4c5 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -3,7 +3,7 @@ import { dirname, join } from 'path'
3import { JobType, VideoRateType, VideoState } from '../../shared/models' 3import { JobType, VideoRateType, VideoState } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 4import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 5import { FollowState } from '../../shared/models/actors'
6import { VideoPrivacy } from '../../shared/models/videos' 6import { VideoPrivacy, VideoAbuseState } from '../../shared/models/videos'
7// Do not use barrels, remain constants as independent as possible 7// Do not use barrels, remain constants as independent as possible
8import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' 8import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
@@ -15,7 +15,7 @@ let config: IConfig = require('config')
15 15
16// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
17 17
18const LAST_MIGRATION_VERSION = 245 18const LAST_MIGRATION_VERSION = 250
19 19
20// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
21 21
@@ -258,7 +258,8 @@ const CONSTRAINTS_FIELDS = {
258 BLOCKED_REASON: { min: 3, max: 250 } // Length 258 BLOCKED_REASON: { min: 3, max: 250 } // Length
259 }, 259 },
260 VIDEO_ABUSES: { 260 VIDEO_ABUSES: {
261 REASON: { min: 2, max: 300 } // Length 261 REASON: { min: 2, max: 300 }, // Length
262 MODERATION_COMMENT: { min: 2, max: 300 } // Length
262 }, 263 },
263 VIDEO_CHANNELS: { 264 VIDEO_CHANNELS: {
264 NAME: { min: 3, max: 120 }, // Length 265 NAME: { min: 3, max: 120 }, // Length
@@ -409,6 +410,12 @@ const VIDEO_IMPORT_STATES = {
409 [VideoImportState.SUCCESS]: 'Success' 410 [VideoImportState.SUCCESS]: 'Success'
410} 411}
411 412
413const VIDEO_ABUSE_STATES = {
414 [VideoAbuseState.PENDING]: 'Pending',
415 [VideoAbuseState.REJECTED]: 'Rejected',
416 [VideoAbuseState.ACCEPTED]: 'Accepted'
417}
418
412const VIDEO_MIMETYPE_EXT = { 419const VIDEO_MIMETYPE_EXT = {
413 'video/webm': '.webm', 420 'video/webm': '.webm',
414 'video/ogg': '.ogv', 421 'video/ogg': '.ogv',
@@ -625,6 +632,7 @@ export {
625 VIDEO_MIMETYPE_EXT, 632 VIDEO_MIMETYPE_EXT,
626 VIDEO_TRANSCODING_FPS, 633 VIDEO_TRANSCODING_FPS,
627 FFMPEG_NICE, 634 FFMPEG_NICE,
635 VIDEO_ABUSE_STATES,
628 JOB_REQUEST_TIMEOUT, 636 JOB_REQUEST_TIMEOUT,
629 USER_PASSWORD_RESET_LIFETIME, 637 USER_PASSWORD_RESET_LIFETIME,
630 IMAGE_MIMETYPE_EXT, 638 IMAGE_MIMETYPE_EXT,
diff --git a/server/initializers/migrations/0250-video-abuse-state.ts b/server/initializers/migrations/0250-video-abuse-state.ts
new file mode 100644
index 000000000..acb668ae1
--- /dev/null
+++ b/server/initializers/migrations/0250-video-abuse-state.ts
@@ -0,0 +1,47 @@
1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3import { VideoAbuseState } from '../../../shared/models/videos'
4
5async function up (utils: {
6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize
9}): Promise<any> {
10 {
11 const data = {
12 type: Sequelize.INTEGER,
13 allowNull: true,
14 defaultValue: null
15 }
16 await utils.queryInterface.addColumn('videoAbuse', 'state', data)
17 }
18
19 {
20 const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING
21 await utils.sequelize.query(query)
22 }
23
24 {
25 const data = {
26 type: Sequelize.INTEGER,
27 allowNull: false,
28 defaultValue: null
29 }
30 await utils.queryInterface.changeColumn('videoAbuse', 'state', data)
31 }
32
33 {
34 const data = {
35 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max),
36 allowNull: true,
37 defaultValue: null
38 }
39 await utils.queryInterface.addColumn('videoAbuse', 'moderationComment', data)
40 }
41}
42
43function down (options) {
44 throw new Error('Not implemented.')
45}
46
47export { up, down }
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 6364bf135..791148919 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -1,4 +1,4 @@
1import { ActivityCreate, VideoTorrentObject } from '../../../../shared' 1import { ActivityCreate, VideoAbuseState, VideoTorrentObject } from '../../../../shared'
2import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' 2import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects'
3import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' 3import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
4import { retryTransactionWrapper } from '../../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -112,7 +112,8 @@ async function processCreateVideoAbuse (actor: ActorModel, videoAbuseToCreateDat
112 const videoAbuseData = { 112 const videoAbuseData = {
113 reporterAccountId: account.id, 113 reporterAccountId: account.id,
114 reason: videoAbuseToCreateData.content, 114 reason: videoAbuseToCreateData.content,
115 videoId: video.id 115 videoId: video.id,
116 state: VideoAbuseState.PENDING
116 } 117 }
117 118
118 await VideoAbuseModel.create(videoAbuseData) 119 await VideoAbuseModel.create(videoAbuseData)
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index c5400c8f5..ccbedd57d 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -7,6 +7,7 @@ export * from './feeds'
7export * from './sort' 7export * from './sort'
8export * from './users' 8export * from './users'
9export * from './videos' 9export * from './videos'
10export * from './video-abuses'
10export * from './video-blacklist' 11export * from './video-blacklist'
11export * from './video-channels' 12export * from './video-channels'
12export * from './webfinger' 13export * from './webfinger'
diff --git a/server/middlewares/validators/video-abuses.ts b/server/middlewares/validators/video-abuses.ts
new file mode 100644
index 000000000..f15d55a75
--- /dev/null
+++ b/server/middlewares/validators/video-abuses.ts
@@ -0,0 +1,71 @@
1import * as express from 'express'
2import 'express-validator'
3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../helpers/custom-validators/videos'
6import { logger } from '../../helpers/logger'
7import { areValidationErrors } from './utils'
8import {
9 isVideoAbuseExist,
10 isVideoAbuseModerationCommentValid,
11 isVideoAbuseReasonValid,
12 isVideoAbuseStateValid
13} from '../../helpers/custom-validators/video-abuses'
14
15const videoAbuseReportValidator = [
16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
17 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
18
19 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
20 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
21
22 if (areValidationErrors(req, res)) return
23 if (!await isVideoExist(req.params.videoId, res)) return
24
25 return next()
26 }
27]
28
29const videoAbuseGetValidator = [
30 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
31 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
32
33 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
34 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
35
36 if (areValidationErrors(req, res)) return
37 if (!await isVideoExist(req.params.videoId, res)) return
38 if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return
39
40 return next()
41 }
42]
43
44const videoAbuseUpdateValidator = [
45 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
46 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
47 body('state')
48 .optional()
49 .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'),
50 body('moderationComment')
51 .optional()
52 .custom(isVideoAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
53
54 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
55 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
56
57 if (areValidationErrors(req, res)) return
58 if (!await isVideoExist(req.params.videoId, res)) return
59 if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return
60
61 return next()
62 }
63]
64
65// ---------------------------------------------------------------------------
66
67export {
68 videoAbuseReportValidator,
69 videoAbuseGetValidator,
70 videoAbuseUpdateValidator
71}
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index c812d4677..203a00876 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -14,7 +14,6 @@ import {
14import { 14import {
15 checkUserCanManageVideo, 15 checkUserCanManageVideo,
16 isScheduleVideoUpdatePrivacyValid, 16 isScheduleVideoUpdatePrivacyValid,
17 isVideoAbuseReasonValid,
18 isVideoCategoryValid, 17 isVideoCategoryValid,
19 isVideoChannelOfAccountExist, 18 isVideoChannelOfAccountExist,
20 isVideoDescriptionValid, 19 isVideoDescriptionValid,
@@ -174,20 +173,6 @@ const videosRemoveValidator = [
174 } 173 }
175] 174]
176 175
177const videoAbuseReportValidator = [
178 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
179 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
180
181 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
182 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
183
184 if (areValidationErrors(req, res)) return
185 if (!await isVideoExist(req.params.id, res)) return
186
187 return next()
188 }
189]
190
191const videoRateValidator = [ 176const videoRateValidator = [
192 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 177 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
193 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), 178 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
@@ -299,8 +284,6 @@ export {
299 videosRemoveValidator, 284 videosRemoveValidator,
300 videosShareValidator, 285 videosShareValidator,
301 286
302 videoAbuseReportValidator,
303
304 videoRateValidator, 287 videoRateValidator,
305 288
306 getCommonVideoAttributes 289 getCommonVideoAttributes
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 39f0c2cb2..10a191372 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -1,11 +1,30 @@
1import { AfterCreate, AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import {
2 AfterCreate,
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 Default,
9 ForeignKey,
10 Is,
11 Model,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
2import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 15import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
3import { VideoAbuse } from '../../../shared/models/videos' 16import { VideoAbuse } from '../../../shared/models/videos'
4import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos' 17import {
18 isVideoAbuseModerationCommentValid,
19 isVideoAbuseReasonValid,
20 isVideoAbuseStateValid
21} from '../../helpers/custom-validators/video-abuses'
5import { Emailer } from '../../lib/emailer' 22import { Emailer } from '../../lib/emailer'
6import { AccountModel } from '../account/account' 23import { AccountModel } from '../account/account'
7import { getSort, throwIfNotValid } from '../utils' 24import { getSort, throwIfNotValid } from '../utils'
8import { VideoModel } from './video' 25import { VideoModel } from './video'
26import { VideoAbuseState } from '../../../shared'
27import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers'
9 28
10@Table({ 29@Table({
11 tableName: 'videoAbuse', 30 tableName: 'videoAbuse',
@@ -25,6 +44,18 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
25 @Column 44 @Column
26 reason: string 45 reason: string
27 46
47 @AllowNull(false)
48 @Default(null)
49 @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
50 @Column
51 state: VideoAbuseState
52
53 @AllowNull(true)
54 @Default(null)
55 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment'))
56 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
57 moderationComment: string
58
28 @CreatedAt 59 @CreatedAt
29 createdAt: Date 60 createdAt: Date
30 61
@@ -60,6 +91,16 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
60 return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) 91 return Emailer.Instance.addVideoAbuseReportJob(instance.videoId)
61 } 92 }
62 93
94 static loadByIdAndVideoId (id: number, videoId: number) {
95 const query = {
96 where: {
97 id,
98 videoId
99 }
100 }
101 return VideoAbuseModel.findOne(query)
102 }
103
63 static listForApi (start: number, count: number, sort: string) { 104 static listForApi (start: number, count: number, sort: string) {
64 const query = { 105 const query = {
65 offset: start, 106 offset: start,
@@ -88,6 +129,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
88 id: this.id, 129 id: this.id,
89 reason: this.reason, 130 reason: this.reason,
90 reporterAccount: this.Account.toFormattedJSON(), 131 reporterAccount: this.Account.toFormattedJSON(),
132 state: {
133 id: this.state,
134 label: VideoAbuseModel.getStateLabel(this.state)
135 },
136 moderationComment: this.moderationComment,
91 video: { 137 video: {
92 id: this.Video.id, 138 id: this.Video.id,
93 uuid: this.Video.uuid, 139 uuid: this.Video.uuid,
@@ -105,4 +151,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
105 object: this.Video.url 151 object: this.Video.url
106 } 152 }
107 } 153 }
154
155 private static getStateLabel (id: number) {
156 return VIDEO_ABUSE_STATES[id] || 'Unknown'
157 }
108} 158}
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index b794d8324..9d1f783c7 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -171,6 +171,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
171 video 171 video
172 } 172 }
173 } 173 }
174
174 private static getStateLabel (id: number) { 175 private static getStateLabel (id: number) {
175 return VIDEO_IMPORT_STATES[id] || 'Unknown' 176 return VIDEO_IMPORT_STATES[id] || 'Unknown'
176 } 177 }
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index 68b965bbe..d2bed6a2a 100644
--- a/server/tests/api/check-params/video-abuses.ts
+++ b/server/tests/api/check-params/video-abuses.ts
@@ -3,14 +3,26 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 createUser, flushTests, killallServers, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, 6 createUser,
7 uploadVideo, userLogin 7 deleteVideoAbuse,
8 flushTests,
9 killallServers,
10 makeGetRequest,
11 makePostBodyRequest,
12 runServer,
13 ServerInfo,
14 setAccessTokensToServers,
15 updateVideoAbuse,
16 uploadVideo,
17 userLogin
8} from '../../utils' 18} from '../../utils'
9import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 19import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
20import { VideoAbuseState } from '../../../../shared/models/videos'
10 21
11describe('Test video abuses API validators', function () { 22describe('Test video abuses API validators', function () {
12 let server: ServerInfo 23 let server: ServerInfo
13 let userAccessToken = '' 24 let userAccessToken = ''
25 let videoAbuseId: number
14 26
15 // --------------------------------------------------------------- 27 // ---------------------------------------------------------------
16 28
@@ -67,44 +79,111 @@ describe('Test video abuses API validators', function () {
67 79
68 describe('When reporting a video abuse', function () { 80 describe('When reporting a video abuse', function () {
69 const basePath = '/api/v1/videos/' 81 const basePath = '/api/v1/videos/'
82 let path: string
83
84 before(() => {
85 path = basePath + server.video.id + '/abuse'
86 })
70 87
71 it('Should fail with nothing', async function () { 88 it('Should fail with nothing', async function () {
72 const path = basePath + server.video.id + '/abuse'
73 const fields = {} 89 const fields = {}
74 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 90 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
75 }) 91 })
76 92
77 it('Should fail with a wrong video', async function () { 93 it('Should fail with a wrong video', async function () {
78 const wrongPath = '/api/v1/videos/blabla/abuse' 94 const wrongPath = '/api/v1/videos/blabla/abuse'
79 const fields = { 95 const fields = { reason: 'my super reason' }
80 reason: 'my super reason' 96
81 }
82 await makePostBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields }) 97 await makePostBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields })
83 }) 98 })
84 99
85 it('Should fail with a non authenticated user', async function () { 100 it('Should fail with a non authenticated user', async function () {
86 const path = basePath + server.video.id + '/abuse' 101 const fields = { reason: 'my super reason' }
87 const fields = { 102
88 reason: 'my super reason'
89 }
90 await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) 103 await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 })
91 }) 104 })
92 105
93 it('Should fail with a reason too short', async function () { 106 it('Should fail with a reason too short', async function () {
94 const path = basePath + server.video.id + '/abuse' 107 const fields = { reason: 'h' }
95 const fields = { 108
96 reason: 'h'
97 }
98 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 109 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
99 }) 110 })
100 111
101 it('Should fail with a reason too big', async function () { 112 it('Should fail with a reason too big', async function () {
102 const path = basePath + server.video.id + '/abuse' 113 const fields = { reason: 'super'.repeat(61) }
103 const fields = { 114
104 reason: 'super'.repeat(61)
105 }
106 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 115 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
107 }) 116 })
117
118 it('Should succeed with the correct parameters', async function () {
119 const fields = { reason: 'super reason' }
120
121 const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
122 videoAbuseId = res.body.videoAbuse.id
123 })
124 })
125
126 describe('When updating a video abuse', function () {
127 const basePath = '/api/v1/videos/'
128 let path: string
129
130 before(() => {
131 path = basePath + server.video.id + '/abuse/' + videoAbuseId
132 })
133
134 it('Should fail with a non authenticated user', async function () {
135 await updateVideoAbuse(server.url, 'blabla', server.video.uuid, videoAbuseId, {}, 401)
136 })
137
138 it('Should fail with a non admin user', async function () {
139 await updateVideoAbuse(server.url, userAccessToken, server.video.uuid, videoAbuseId, {}, 403)
140 })
141
142 it('Should fail with a bad video id or bad video abuse id', async function () {
143 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, 45, {}, 404)
144 await updateVideoAbuse(server.url, server.accessToken, 52, videoAbuseId, {}, 404)
145 })
146
147 it('Should fail with a bad state', async function () {
148 const body = { state: 5 }
149 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400)
150 })
151
152 it('Should fail with a bad moderation comment', async function () {
153 const body = { moderationComment: 'b'.repeat(305) }
154 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400)
155 })
156
157 it('Should succeed with the correct params', async function () {
158 const body = { state: VideoAbuseState.ACCEPTED }
159 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body)
160 })
161 })
162
163 describe('When deleting a video abuse', function () {
164 const basePath = '/api/v1/videos/'
165 let path: string
166
167 before(() => {
168 path = basePath + server.video.id + '/abuse/' + videoAbuseId
169 })
170
171 it('Should fail with a non authenticated user', async function () {
172 await deleteVideoAbuse(server.url, 'blabla', server.video.uuid, videoAbuseId, 401)
173 })
174
175 it('Should fail with a non admin user', async function () {
176 await deleteVideoAbuse(server.url, userAccessToken, server.video.uuid, videoAbuseId, 403)
177 })
178
179 it('Should fail with a bad video id or bad video abuse id', async function () {
180 await deleteVideoAbuse(server.url, server.accessToken, server.video.uuid, 45, 404)
181 await deleteVideoAbuse(server.url, server.accessToken, 52, videoAbuseId, 404)
182 })
183
184 it('Should succeed with the correct params', async function () {
185 await deleteVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId)
186 })
108 }) 187 })
109 188
110 after(async function () { 189 after(async function () {
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index dde309b96..a17f3c8de 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -2,8 +2,9 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoAbuse } from '../../../../shared/models/videos' 5import { VideoAbuse, VideoAbuseState } from '../../../../shared/models/videos'
6import { 6import {
7 deleteVideoAbuse,
7 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
8 getVideoAbusesList, 9 getVideoAbusesList,
9 getVideosList, 10 getVideosList,
@@ -11,6 +12,7 @@ import {
11 reportVideoAbuse, 12 reportVideoAbuse,
12 ServerInfo, 13 ServerInfo,
13 setAccessTokensToServers, 14 setAccessTokensToServers,
15 updateVideoAbuse,
14 uploadVideo 16 uploadVideo
15} from '../../utils/index' 17} from '../../utils/index'
16import { doubleFollow } from '../../utils/server/follows' 18import { doubleFollow } from '../../utils/server/follows'
@@ -20,6 +22,7 @@ const expect = chai.expect
20 22
21describe('Test video abuses', function () { 23describe('Test video abuses', function () {
22 let servers: ServerInfo[] = [] 24 let servers: ServerInfo[] = []
25 let abuseServer2: VideoAbuse
23 26
24 before(async function () { 27 before(async function () {
25 this.timeout(50000) 28 this.timeout(50000)
@@ -105,7 +108,7 @@ describe('Test video abuses', function () {
105 await waitJobs(servers) 108 await waitJobs(servers)
106 }) 109 })
107 110
108 it('Should have 2 video abuse on server 1 and 1 on server 2', async function () { 111 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
109 const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 112 const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken)
110 expect(res1.body.total).to.equal(2) 113 expect(res1.body.total).to.equal(2)
111 expect(res1.body.data).to.be.an('array') 114 expect(res1.body.data).to.be.an('array')
@@ -116,22 +119,57 @@ describe('Test video abuses', function () {
116 expect(abuse1.reporterAccount.name).to.equal('root') 119 expect(abuse1.reporterAccount.name).to.equal('root')
117 expect(abuse1.reporterAccount.host).to.equal('localhost:9001') 120 expect(abuse1.reporterAccount.host).to.equal('localhost:9001')
118 expect(abuse1.video.id).to.equal(servers[0].video.id) 121 expect(abuse1.video.id).to.equal(servers[0].video.id)
122 expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING)
123 expect(abuse1.state.label).to.equal('Pending')
124 expect(abuse1.moderationComment).to.be.null
119 125
120 const abuse2: VideoAbuse = res1.body.data[1] 126 const abuse2: VideoAbuse = res1.body.data[1]
121 expect(abuse2.reason).to.equal('my super bad reason 2') 127 expect(abuse2.reason).to.equal('my super bad reason 2')
122 expect(abuse2.reporterAccount.name).to.equal('root') 128 expect(abuse2.reporterAccount.name).to.equal('root')
123 expect(abuse2.reporterAccount.host).to.equal('localhost:9001') 129 expect(abuse2.reporterAccount.host).to.equal('localhost:9001')
124 expect(abuse2.video.id).to.equal(servers[1].video.id) 130 expect(abuse2.video.id).to.equal(servers[1].video.id)
131 expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING)
132 expect(abuse2.state.label).to.equal('Pending')
133 expect(abuse2.moderationComment).to.be.null
125 134
126 const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 135 const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
127 expect(res2.body.total).to.equal(1) 136 expect(res2.body.total).to.equal(1)
128 expect(res2.body.data).to.be.an('array') 137 expect(res2.body.data).to.be.an('array')
129 expect(res2.body.data.length).to.equal(1) 138 expect(res2.body.data.length).to.equal(1)
130 139
131 const abuse3: VideoAbuse = res2.body.data[0] 140 abuseServer2 = res2.body.data[0]
132 expect(abuse3.reason).to.equal('my super bad reason 2') 141 expect(abuseServer2.reason).to.equal('my super bad reason 2')
133 expect(abuse3.reporterAccount.name).to.equal('root') 142 expect(abuseServer2.reporterAccount.name).to.equal('root')
134 expect(abuse3.reporterAccount.host).to.equal('localhost:9001') 143 expect(abuseServer2.reporterAccount.host).to.equal('localhost:9001')
144 expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING)
145 expect(abuseServer2.state.label).to.equal('Pending')
146 expect(abuseServer2.moderationComment).to.be.null
147 })
148
149 it('Should update the state of a video abuse', async function () {
150 const body = { state: VideoAbuseState.REJECTED }
151 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
152
153 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
154 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED)
155 })
156
157 it('Should add a moderation comment', async function () {
158 const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' }
159 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
160
161 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
162 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED)
163 expect(res.body.data[0].moderationComment).to.equal('It is valid')
164 })
165
166 it('Should delete the video abuse', async function () {
167 await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id)
168
169 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
170 expect(res.body.total).to.equal(0)
171 expect(res.body.data).to.be.an('array')
172 expect(res.body.data.length).to.equal(0)
135 }) 173 })
136 174
137 after(async function () { 175 after(async function () {
diff --git a/server/tests/utils/videos/video-abuses.ts b/server/tests/utils/videos/video-abuses.ts
index 0d72bf457..5f138d6b3 100644
--- a/server/tests/utils/videos/video-abuses.ts
+++ b/server/tests/utils/videos/video-abuses.ts
@@ -1,6 +1,8 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { VideoAbuseUpdate } from '../../../../shared/models/videos/video-abuse-update.model'
3import { makeDeleteRequest, makePutBodyRequest } from '..'
2 4
3function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 204) { 5function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) {
4 const path = '/api/v1/videos/' + videoId + '/abuse' 6 const path = '/api/v1/videos/' + videoId + '/abuse'
5 7
6 return request(url) 8 return request(url)
@@ -23,9 +25,41 @@ function getVideoAbusesList (url: string, token: string) {
23 .expect('Content-Type', /json/) 25 .expect('Content-Type', /json/)
24} 26}
25 27
28function updateVideoAbuse (
29 url: string,
30 token: string,
31 videoId: string | number,
32 videoAbuseId: number,
33 body: VideoAbuseUpdate,
34 statusCodeExpected = 204
35) {
36 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
37
38 return makePutBodyRequest({
39 url,
40 token,
41 path,
42 fields: body,
43 statusCodeExpected
44 })
45}
46
47function deleteVideoAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) {
48 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
49
50 return makeDeleteRequest({
51 url,
52 token,
53 path,
54 statusCodeExpected
55 })
56}
57
26// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
27 59
28export { 60export {
29 reportVideoAbuse, 61 reportVideoAbuse,
30 getVideoAbusesList 62 getVideoAbusesList,
63 updateVideoAbuse,
64 deleteVideoAbuse
31} 65}