aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-06-22 13:00:39 +0200
committerGitHub <noreply@github.com>2020-06-22 13:00:39 +0200
commit1ebddadd0704812a4600c39cabe2268321e88331 (patch)
tree1cc8560e5b63e9976aa5411ba800a62cfe7b8ea9 /server
parent07aea1a2642fc9868cb01e30c322514029d5b95a (diff)
downloadPeerTube-1ebddadd0704812a4600c39cabe2268321e88331.tar.gz
PeerTube-1ebddadd0704812a4600c39cabe2268321e88331.tar.zst
PeerTube-1ebddadd0704812a4600c39cabe2268321e88331.zip
predefined report reasons & improved reporter UI (#2842)
- added `startAt` and `endAt` optional timestamps to help pin down reported sections of a video - added predefined report reasons - added video player with report modal
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/abuse.ts11
-rw-r--r--server/helpers/custom-validators/video-abuses.ts29
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0515-video-abuse-reason-timestamps.ts31
-rw-r--r--server/lib/activitypub/process/process-flag.ts17
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts39
-rw-r--r--server/models/video/video-abuse.ts64
-rw-r--r--server/tests/api/check-params/video-abuses.ts30
-rw-r--r--server/tests/api/videos/video-abuse.ts43
9 files changed, 239 insertions, 27 deletions
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index 77843f149..ab2074459 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, VideoAbuseState, VideoAbuse } from '../../../../shared' 2import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } 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/database' 5import { sequelizeTypescript } from '../../../initializers/database'
@@ -74,6 +74,7 @@ async function listVideoAbuses (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 predefinedReason: req.query.predefinedReason,
77 search: req.query.search, 78 search: req.query.search,
78 state: req.query.state, 79 state: req.query.state,
79 videoIs: req.query.videoIs, 80 videoIs: req.query.videoIs,
@@ -123,12 +124,16 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
123 124
124 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { 125 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
125 reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 126 reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
127 const predefinedReasons = body.predefinedReasons?.map(r => videoAbusePredefinedReasonsMap[r])
126 128
127 const abuseToCreate = { 129 const abuseToCreate = {
128 reporterAccountId: reporterAccount.id, 130 reporterAccountId: reporterAccount.id,
129 reason: body.reason, 131 reason: body.reason,
130 videoId: videoInstance.id, 132 videoId: videoInstance.id,
131 state: VideoAbuseState.PENDING 133 state: VideoAbuseState.PENDING,
134 predefinedReasons,
135 startAt: body.startAt,
136 endAt: body.endAt
132 } 137 }
133 138
134 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) 139 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
@@ -152,7 +157,7 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
152 reporter: reporterAccount.Actor.getIdentifier() 157 reporter: reporterAccount.Actor.getIdentifier()
153 }) 158 })
154 159
155 logger.info('Abuse report for video %s created.', videoInstance.name) 160 logger.info('Abuse report for video "%s" created.', videoInstance.name)
156 161
157 return res.json({ videoAbuse: videoAbuseJSON }).end() 162 return res.json({ videoAbuse: videoAbuseJSON }).end()
158} 163}
diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts
index 05e11b1c6..0c2c34268 100644
--- a/server/helpers/custom-validators/video-abuses.ts
+++ b/server/helpers/custom-validators/video-abuses.ts
@@ -1,8 +1,9 @@
1import validator from 'validator' 1import validator from 'validator'
2 2
3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
4import { exists } from './misc' 4import { exists, isArray } from './misc'
5import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' 5import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
6import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model'
6 7
7const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 8const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
8 9
@@ -10,6 +11,22 @@ function isVideoAbuseReasonValid (value: string) {
10 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) 11 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
11} 12}
12 13
14function isVideoAbusePredefinedReasonValid (value: VideoAbusePredefinedReasonsString) {
15 return exists(value) && value in videoAbusePredefinedReasonsMap
16}
17
18function isVideoAbusePredefinedReasonsValid (value: VideoAbusePredefinedReasonsString[]) {
19 return exists(value) && isArray(value) && value.every(v => v in videoAbusePredefinedReasonsMap)
20}
21
22function isVideoAbuseTimestampValid (value: number) {
23 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
24}
25
26function isVideoAbuseTimestampCoherent (endAt: number, { req }) {
27 return exists(req.body.startAt) && endAt > req.body.startAt
28}
29
13function isVideoAbuseModerationCommentValid (value: string) { 30function isVideoAbuseModerationCommentValid (value: string) {
14 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) 31 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
15} 32}
@@ -28,8 +45,12 @@ function isAbuseVideoIsValid (value: VideoAbuseVideoIs) {
28// --------------------------------------------------------------------------- 45// ---------------------------------------------------------------------------
29 46
30export { 47export {
31 isVideoAbuseStateValid,
32 isVideoAbuseReasonValid, 48 isVideoAbuseReasonValid,
33 isAbuseVideoIsValid, 49 isVideoAbusePredefinedReasonValid,
34 isVideoAbuseModerationCommentValid 50 isVideoAbusePredefinedReasonsValid,
51 isVideoAbuseTimestampValid,
52 isVideoAbuseTimestampCoherent,
53 isVideoAbuseModerationCommentValid,
54 isVideoAbuseStateValid,
55 isAbuseVideoIsValid
35} 56}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 314f094b3..dd79c0e16 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
14 14
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17const LAST_MIGRATION_VERSION = 510 17const LAST_MIGRATION_VERSION = 515
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
diff --git a/server/initializers/migrations/0515-video-abuse-reason-timestamps.ts b/server/initializers/migrations/0515-video-abuse-reason-timestamps.ts
new file mode 100644
index 000000000..c58335617
--- /dev/null
+++ b/server/initializers/migrations/0515-video-abuse-reason-timestamps.ts
@@ -0,0 +1,31 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 await utils.queryInterface.addColumn('videoAbuse', 'predefinedReasons', {
9 type: Sequelize.ARRAY(Sequelize.INTEGER),
10 allowNull: true
11 })
12
13 await utils.queryInterface.addColumn('videoAbuse', 'startAt', {
14 type: Sequelize.INTEGER,
15 allowNull: true
16 })
17
18 await utils.queryInterface.addColumn('videoAbuse', 'endAt', {
19 type: Sequelize.INTEGER,
20 allowNull: true
21 })
22}
23
24function down (options) {
25 throw new Error('Not implemented.')
26}
27
28export {
29 up,
30 down
31}
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index 8d1c9c869..1d7132a3a 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -1,4 +1,9 @@
1import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared' 1import {
2 ActivityCreate,
3 ActivityFlag,
4 VideoAbuseState,
5 videoAbusePredefinedReasonsMap
6} from '../../../../shared'
2import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' 7import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 8import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
@@ -38,13 +43,21 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
38 43
39 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) 44 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
40 const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t)) 45 const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
46 const tags = Array.isArray(flag.tag) ? flag.tag : []
47 const predefinedReasons = tags.map(tag => videoAbusePredefinedReasonsMap[tag.name])
48 .filter(v => !isNaN(v))
49 const startAt = flag.startAt
50 const endAt = flag.endAt
41 51
42 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { 52 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
43 const videoAbuseData = { 53 const videoAbuseData = {
44 reporterAccountId: account.id, 54 reporterAccountId: account.id,
45 reason: flag.content, 55 reason: flag.content,
46 videoId: video.id, 56 videoId: video.id,
47 state: VideoAbuseState.PENDING 57 state: VideoAbuseState.PENDING,
58 predefinedReasons,
59 startAt,
60 endAt
48 } 61 }
49 62
50 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) 63 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
index 901997bcb..5bbd1e3c6 100644
--- a/server/middlewares/validators/videos/video-abuses.ts
+++ b/server/middlewares/validators/videos/video-abuses.ts
@@ -1,19 +1,46 @@
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 { exists, isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 3import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
4import { 4import {
5 isAbuseVideoIsValid, 5 isAbuseVideoIsValid,
6 isVideoAbuseModerationCommentValid, 6 isVideoAbuseModerationCommentValid,
7 isVideoAbuseReasonValid, 7 isVideoAbuseReasonValid,
8 isVideoAbuseStateValid 8 isVideoAbuseStateValid,
9 isVideoAbusePredefinedReasonsValid,
10 isVideoAbusePredefinedReasonValid,
11 isVideoAbuseTimestampValid,
12 isVideoAbuseTimestampCoherent
9} from '../../../helpers/custom-validators/video-abuses' 13} from '../../../helpers/custom-validators/video-abuses'
10import { logger } from '../../../helpers/logger' 14import { logger } from '../../../helpers/logger'
11import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares' 15import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares'
12import { areValidationErrors } from '../utils' 16import { areValidationErrors } from '../utils'
13 17
14const videoAbuseReportValidator = [ 18const videoAbuseReportValidator = [
15 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 19 param('videoId')
16 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'), 20 .custom(isIdOrUUIDValid)
21 .not()
22 .isEmpty()
23 .withMessage('Should have a valid videoId'),
24 body('reason')
25 .custom(isVideoAbuseReasonValid)
26 .withMessage('Should have a valid reason'),
27 body('predefinedReasons')
28 .optional()
29 .custom(isVideoAbusePredefinedReasonsValid)
30 .withMessage('Should have a valid list of predefined reasons'),
31 body('startAt')
32 .optional()
33 .customSanitizer(toIntOrNull)
34 .custom(isVideoAbuseTimestampValid)
35 .withMessage('Should have valid starting time value'),
36 body('endAt')
37 .optional()
38 .customSanitizer(toIntOrNull)
39 .custom(isVideoAbuseTimestampValid)
40 .withMessage('Should have valid ending time value')
41 .bail()
42 .custom(isVideoAbuseTimestampCoherent)
43 .withMessage('Should have a startAt timestamp beginning before endAt'),
17 44
18 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 45 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) 46 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
@@ -63,6 +90,10 @@ const videoAbuseListValidator = [
63 query('id') 90 query('id')
64 .optional() 91 .optional()
65 .custom(isIdValid).withMessage('Should have a valid id'), 92 .custom(isIdValid).withMessage('Should have a valid id'),
93 query('predefinedReason')
94 .optional()
95 .custom(isVideoAbusePredefinedReasonValid)
96 .withMessage('Should have a valid predefinedReason'),
66 query('search') 97 query('search')
67 .optional() 98 .optional()
68 .custom(exists).withMessage('Should have a valid search'), 99 .custom(exists).withMessage('Should have a valid search'),
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index b2f111337..1319332f0 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -15,7 +15,13 @@ import {
15 UpdatedAt 15 UpdatedAt
16} from 'sequelize-typescript' 16} from 'sequelize-typescript'
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' 17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
18import { VideoAbuseState, VideoDetails } from '../../../shared' 18import {
19 VideoAbuseState,
20 VideoDetails,
21 VideoAbusePredefinedReasons,
22 VideoAbusePredefinedReasonsString,
23 videoAbusePredefinedReasonsMap
24} from '../../../shared'
19import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 25import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
20import { VideoAbuse } from '../../../shared/models/videos' 26import { VideoAbuse } from '../../../shared/models/videos'
21import { 27import {
@@ -31,6 +37,7 @@ import { ThumbnailModel } from './thumbnail'
31import { VideoModel } from './video' 37import { VideoModel } from './video'
32import { VideoBlacklistModel } from './video-blacklist' 38import { VideoBlacklistModel } from './video-blacklist'
33import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' 39import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
40import { invert } from 'lodash'
34 41
35export enum ScopeNames { 42export enum ScopeNames {
36 FOR_API = 'FOR_API' 43 FOR_API = 'FOR_API'
@@ -47,6 +54,7 @@ export enum ScopeNames {
47 54
48 // filters 55 // filters
49 id?: number 56 id?: number
57 predefinedReasonId?: number
50 58
51 state?: VideoAbuseState 59 state?: VideoAbuseState
52 videoIs?: VideoAbuseVideoIs 60 videoIs?: VideoAbuseVideoIs
@@ -104,6 +112,14 @@ export enum ScopeNames {
104 }) 112 })
105 } 113 }
106 114
115 if (options.predefinedReasonId) {
116 Object.assign(where, {
117 predefinedReasons: {
118 [Op.contains]: [ options.predefinedReasonId ]
119 }
120 })
121 }
122
107 const onlyBlacklisted = options.videoIs === 'blacklisted' 123 const onlyBlacklisted = options.videoIs === 'blacklisted'
108 124
109 return { 125 return {
@@ -258,6 +274,21 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
258 @Column(DataType.JSONB) 274 @Column(DataType.JSONB)
259 deletedVideo: VideoDetails 275 deletedVideo: VideoDetails
260 276
277 @AllowNull(true)
278 @Default(null)
279 @Column(DataType.ARRAY(DataType.INTEGER))
280 predefinedReasons: VideoAbusePredefinedReasons[]
281
282 @AllowNull(true)
283 @Default(null)
284 @Column
285 startAt: number
286
287 @AllowNull(true)
288 @Default(null)
289 @Column
290 endAt: number
291
261 @CreatedAt 292 @CreatedAt
262 createdAt: Date 293 createdAt: Date
263 294
@@ -311,6 +342,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
311 user?: MUserAccountId 342 user?: MUserAccountId
312 343
313 id?: number 344 id?: number
345 predefinedReason?: VideoAbusePredefinedReasonsString
314 state?: VideoAbuseState 346 state?: VideoAbuseState
315 videoIs?: VideoAbuseVideoIs 347 videoIs?: VideoAbuseVideoIs
316 348
@@ -329,6 +361,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
329 serverAccountId, 361 serverAccountId,
330 state, 362 state,
331 videoIs, 363 videoIs,
364 predefinedReason,
332 searchReportee, 365 searchReportee,
333 searchVideo, 366 searchVideo,
334 searchVideoChannel, 367 searchVideoChannel,
@@ -337,6 +370,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
337 } = parameters 370 } = parameters
338 371
339 const userAccountId = user ? user.Account.id : undefined 372 const userAccountId = user ? user.Account.id : undefined
373 const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
340 374
341 const query = { 375 const query = {
342 offset: start, 376 offset: start,
@@ -348,6 +382,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
348 382
349 const filters = { 383 const filters = {
350 id, 384 id,
385 predefinedReasonId,
351 search, 386 search,
352 state, 387 state,
353 videoIs, 388 videoIs,
@@ -360,7 +395,9 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
360 } 395 }
361 396
362 return VideoAbuseModel 397 return VideoAbuseModel
363 .scope({ method: [ ScopeNames.FOR_API, filters ] }) 398 .scope([
399 { method: [ ScopeNames.FOR_API, filters ] }
400 ])
364 .findAndCountAll(query) 401 .findAndCountAll(query)
365 .then(({ rows, count }) => { 402 .then(({ rows, count }) => {
366 return { total: count, data: rows } 403 return { total: count, data: rows }
@@ -368,6 +405,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
368 } 405 }
369 406
370 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { 407 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
408 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
371 const countReportsForVideo = this.get('countReportsForVideo') as number 409 const countReportsForVideo = this.get('countReportsForVideo') as number
372 const nthReportForVideo = this.get('nthReportForVideo') as number 410 const nthReportForVideo = this.get('nthReportForVideo') as number
373 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number 411 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
@@ -382,6 +420,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
382 return { 420 return {
383 id: this.id, 421 id: this.id,
384 reason: this.reason, 422 reason: this.reason,
423 predefinedReasons,
385 reporterAccount: this.Account.toFormattedJSON(), 424 reporterAccount: this.Account.toFormattedJSON(),
386 state: { 425 state: {
387 id: this.state, 426 id: this.state,
@@ -400,6 +439,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
400 }, 439 },
401 createdAt: this.createdAt, 440 createdAt: this.createdAt,
402 updatedAt: this.updatedAt, 441 updatedAt: this.updatedAt,
442 startAt: this.startAt,
443 endAt: this.endAt,
403 count: countReportsForVideo || 0, 444 count: countReportsForVideo || 0,
404 nth: nthReportForVideo || 0, 445 nth: nthReportForVideo || 0,
405 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), 446 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
@@ -408,14 +449,31 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
408 } 449 }
409 450
410 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject { 451 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
452 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
453
454 const startAt = this.startAt
455 const endAt = this.endAt
456
411 return { 457 return {
412 type: 'Flag' as 'Flag', 458 type: 'Flag' as 'Flag',
413 content: this.reason, 459 content: this.reason,
414 object: this.Video.url 460 object: this.Video.url,
461 tag: predefinedReasons.map(r => ({
462 type: 'Hashtag' as 'Hashtag',
463 name: r
464 })),
465 startAt,
466 endAt
415 } 467 }
416 } 468 }
417 469
418 private static getStateLabel (id: number) { 470 private static getStateLabel (id: number) {
419 return VIDEO_ABUSE_STATES[id] || 'Unknown' 471 return VIDEO_ABUSE_STATES[id] || 'Unknown'
420 } 472 }
473
474 private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] {
475 return (predefinedReasons || [])
476 .filter(r => r in VideoAbusePredefinedReasons)
477 .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
478 }
421} 479}
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index a3fe00ffb..557bf20eb 100644
--- a/server/tests/api/check-params/video-abuses.ts
+++ b/server/tests/api/check-params/video-abuses.ts
@@ -20,7 +20,7 @@ import {
20 checkBadSortPagination, 20 checkBadSortPagination,
21 checkBadStartPagination 21 checkBadStartPagination
22} from '../../../../shared/extra-utils/requests/check-api-params' 22} from '../../../../shared/extra-utils/requests/check-api-params'
23import { VideoAbuseState } from '../../../../shared/models/videos' 23import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos'
24 24
25describe('Test video abuses API validators', function () { 25describe('Test video abuses API validators', function () {
26 let server: ServerInfo 26 let server: ServerInfo
@@ -132,12 +132,36 @@ describe('Test video abuses API validators', function () {
132 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 132 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
133 }) 133 })
134 134
135 it('Should succeed with the correct parameters', async function () { 135 it('Should succeed with the correct parameters (basic)', async function () {
136 const fields = { reason: 'super reason' } 136 const fields = { reason: 'my super reason' }
137 137
138 const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) 138 const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
139 videoAbuseId = res.body.videoAbuse.id 139 videoAbuseId = res.body.videoAbuse.id
140 }) 140 })
141
142 it('Should fail with a wrong predefined reason', async function () {
143 const fields = { reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
144
145 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
146 })
147
148 it('Should fail with negative timestamps', async function () {
149 const fields = { reason: 'my super reason', startAt: -1 }
150
151 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
152 })
153
154 it('Should fail mith misordered startAt/endAt', async function () {
155 const fields = { reason: 'my super reason', startAt: 5, endAt: 1 }
156
157 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
158 })
159
160 it('Should succeed with the corret parameters (advanced)', async function () {
161 const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 }
162
163 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
164 })
141 }) 165 })
142 166
143 describe('When updating a video abuse', function () { 167 describe('When updating a video abuse', function () {
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index a96be97f6..7383bd991 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -2,7 +2,7 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoAbuse, VideoAbuseState } from '../../../../shared/models/videos' 5import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 deleteVideoAbuse, 8 deleteVideoAbuse,
@@ -291,6 +291,32 @@ describe('Test video abuses', function () {
291 } 291 }
292 }) 292 })
293 293
294 it('Should list predefined reasons as well as timestamps for the reported video', async function () {
295 this.timeout(10000)
296
297 const reason5 = 'my super bad reason 5'
298 const predefinedReasons5: VideoAbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
299 const createdAbuse = (await reportVideoAbuse(
300 servers[0].url,
301 servers[0].accessToken,
302 servers[0].video.id,
303 reason5,
304 predefinedReasons5,
305 1,
306 5
307 )).body.videoAbuse as VideoAbuse
308
309 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
310
311 {
312 const abuse = (res.body.data as VideoAbuse[]).find(a => a.id === createdAbuse.id)
313 expect(abuse.reason).to.equals(reason5)
314 expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
315 expect(abuse.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
316 expect(abuse.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
317 }
318 })
319
294 it('Should delete the video abuse', async function () { 320 it('Should delete the video abuse', async function () {
295 this.timeout(10000) 321 this.timeout(10000)
296 322
@@ -307,7 +333,7 @@ describe('Test video abuses', function () {
307 333
308 { 334 {
309 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 335 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
310 expect(res.body.total).to.equal(5) 336 expect(res.body.total).to.equal(6)
311 } 337 }
312 }) 338 })
313 339
@@ -328,25 +354,28 @@ describe('Test video abuses', function () {
328 expect(await list({ id: 56 })).to.have.lengthOf(0) 354 expect(await list({ id: 56 })).to.have.lengthOf(0)
329 expect(await list({ id: 1 })).to.have.lengthOf(1) 355 expect(await list({ id: 1 })).to.have.lengthOf(1)
330 356
331 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(3) 357 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4)
332 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0) 358 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
333 359
334 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1) 360 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
335 361
336 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(3) 362 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4)
337 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0) 363 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
338 364
339 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) 365 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
340 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(4) 366 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
341 367
342 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(3) 368 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(4)
343 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) 369 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
344 370
345 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) 371 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
346 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) 372 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
347 373
348 expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0) 374 expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0)
349 expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(5) 375 expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(6)
376
377 expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
378 expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
350 }) 379 })
351 380
352 after(async function () { 381 after(async function () {