diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-06-22 13:00:39 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-22 13:00:39 +0200 |
commit | 1ebddadd0704812a4600c39cabe2268321e88331 (patch) | |
tree | 1cc8560e5b63e9976aa5411ba800a62cfe7b8ea9 /server | |
parent | 07aea1a2642fc9868cb01e30c322514029d5b95a (diff) | |
download | PeerTube-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.ts | 11 | ||||
-rw-r--r-- | server/helpers/custom-validators/video-abuses.ts | 29 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/initializers/migrations/0515-video-abuse-reason-timestamps.ts | 31 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-flag.ts | 17 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-abuses.ts | 39 | ||||
-rw-r--r-- | server/models/video/video-abuse.ts | 64 | ||||
-rw-r--r-- | server/tests/api/check-params/video-abuses.ts | 30 | ||||
-rw-r--r-- | server/tests/api/videos/video-abuse.ts | 43 |
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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse } from '../../../../shared' | 2 | import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { getFormattedObjects } from '../../../helpers/utils' | 4 | import { getFormattedObjects } from '../../../helpers/utils' |
5 | import { sequelizeTypescript } from '../../../initializers/database' | 5 | import { 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 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | 2 | ||
3 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' | 3 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' |
4 | import { exists } from './misc' | 4 | import { exists, isArray } from './misc' |
5 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' | 5 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' |
6 | import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model' | ||
6 | 7 | ||
7 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES | 8 | const 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 | ||
14 | function isVideoAbusePredefinedReasonValid (value: VideoAbusePredefinedReasonsString) { | ||
15 | return exists(value) && value in videoAbusePredefinedReasonsMap | ||
16 | } | ||
17 | |||
18 | function isVideoAbusePredefinedReasonsValid (value: VideoAbusePredefinedReasonsString[]) { | ||
19 | return exists(value) && isArray(value) && value.every(v => v in videoAbusePredefinedReasonsMap) | ||
20 | } | ||
21 | |||
22 | function isVideoAbuseTimestampValid (value: number) { | ||
23 | return value === null || (exists(value) && validator.isInt('' + value, { min: 0 })) | ||
24 | } | ||
25 | |||
26 | function isVideoAbuseTimestampCoherent (endAt: number, { req }) { | ||
27 | return exists(req.body.startAt) && endAt > req.body.startAt | ||
28 | } | ||
29 | |||
13 | function isVideoAbuseModerationCommentValid (value: string) { | 30 | function 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 | ||
30 | export { | 47 | export { |
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 | ||
17 | const LAST_MIGRATION_VERSION = 510 | 17 | const 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
24 | function down (options) { | ||
25 | throw new Error('Not implemented.') | ||
26 | } | ||
27 | |||
28 | export { | ||
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 @@ | |||
1 | import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared' | 1 | import { |
2 | ActivityCreate, | ||
3 | ActivityFlag, | ||
4 | VideoAbuseState, | ||
5 | videoAbusePredefinedReasonsMap | ||
6 | } from '../../../../shared' | ||
2 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' | 7 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 8 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 9 | import { 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { exists, isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | 3 | import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' |
4 | import { | 4 | import { |
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' |
10 | import { logger } from '../../../helpers/logger' | 14 | import { logger } from '../../../helpers/logger' |
11 | import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares' | 15 | import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares' |
12 | import { areValidationErrors } from '../utils' | 16 | import { areValidationErrors } from '../utils' |
13 | 17 | ||
14 | const videoAbuseReportValidator = [ | 18 | const 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' |
17 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' | 17 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' |
18 | import { VideoAbuseState, VideoDetails } from '../../../shared' | 18 | import { |
19 | VideoAbuseState, | ||
20 | VideoDetails, | ||
21 | VideoAbusePredefinedReasons, | ||
22 | VideoAbusePredefinedReasonsString, | ||
23 | videoAbusePredefinedReasonsMap | ||
24 | } from '../../../shared' | ||
19 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | 25 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' |
20 | import { VideoAbuse } from '../../../shared/models/videos' | 26 | import { VideoAbuse } from '../../../shared/models/videos' |
21 | import { | 27 | import { |
@@ -31,6 +37,7 @@ import { ThumbnailModel } from './thumbnail' | |||
31 | import { VideoModel } from './video' | 37 | import { VideoModel } from './video' |
32 | import { VideoBlacklistModel } from './video-blacklist' | 38 | import { VideoBlacklistModel } from './video-blacklist' |
33 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | 39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
40 | import { invert } from 'lodash' | ||
34 | 41 | ||
35 | export enum ScopeNames { | 42 | export 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' |
23 | import { VideoAbuseState } from '../../../../shared/models/videos' | 23 | import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos' |
24 | 24 | ||
25 | describe('Test video abuses API validators', function () { | 25 | describe('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 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { VideoAbuse, VideoAbuseState } from '../../../../shared/models/videos' | 5 | import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos' |
6 | import { | 6 | import { |
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 () { |