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 @@ import * as express from 'express' -import { UserRight, VideoAbuseCreate } from '../../../../shared' +import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared' import { logger } from '../../../helpers/logger' import { getFormattedObjects } from '../../../helpers/utils' import { sequelizeTypescript } from '../../../initializers' @@ -12,8 +12,10 @@ import { paginationValidator, setDefaultPagination, setDefaultSort, + videoAbuseGetValidator, videoAbuseReportValidator, - videoAbusesSortValidator + videoAbusesSortValidator, + videoAbuseUpdateValidator } from '../../../middlewares' import { AccountModel } from '../../../models/account/account' import { VideoModel } from '../../../models/video/video' @@ -32,11 +34,23 @@ abuseVideoRouter.get('/abuse', setDefaultPagination, asyncMiddleware(listVideoAbuses) ) -abuseVideoRouter.post('/:id/abuse', +abuseVideoRouter.put('/:videoId/abuse/:id', + authenticate, + ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), + asyncMiddleware(videoAbuseUpdateValidator), + asyncRetryTransactionMiddleware(updateVideoAbuse) +) +abuseVideoRouter.post('/:videoId/abuse', authenticate, asyncMiddleware(videoAbuseReportValidator), asyncRetryTransactionMiddleware(reportVideoAbuse) ) +abuseVideoRouter.delete('/:videoId/abuse/:id', + authenticate, + ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), + asyncMiddleware(videoAbuseGetValidator), + asyncRetryTransactionMiddleware(deleteVideoAbuse) +) // --------------------------------------------------------------------------- @@ -46,12 +60,39 @@ export { // --------------------------------------------------------------------------- -async function listVideoAbuses (req: express.Request, res: express.Response, next: express.NextFunction) { +async function listVideoAbuses (req: express.Request, res: express.Response) { const resultList = await VideoAbuseModel.listForApi(req.query.start, req.query.count, req.query.sort) return res.json(getFormattedObjects(resultList.data, resultList.total)) } +async function updateVideoAbuse (req: express.Request, res: express.Response) { + const videoAbuse: VideoAbuseModel = res.locals.videoAbuse + + if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment + if (req.body.state !== undefined) videoAbuse.state = req.body.state + + await sequelizeTypescript.transaction(t => { + return videoAbuse.save({ transaction: t }) + }) + + // Do not send the delete to other instances, we updated OUR copy of this video abuse + + return res.type('json').status(204).end() +} + +async function deleteVideoAbuse (req: express.Request, res: express.Response) { + const videoAbuse: VideoAbuseModel = res.locals.videoAbuse + + await sequelizeTypescript.transaction(t => { + return videoAbuse.destroy({ transaction: t }) + }) + + // Do not send the delete to other instances, we delete OUR copy of this video abuse + + return res.type('json').status(204).end() +} + async function reportVideoAbuse (req: express.Request, res: express.Response) { const videoInstance = res.locals.video as VideoModel const reporterAccount = res.locals.oauth.token.User.Account as AccountModel @@ -60,10 +101,11 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) { const abuseToCreate = { reporterAccountId: reporterAccount.id, reason: body.reason, - videoId: videoInstance.id + videoId: videoInstance.id, + state: VideoAbuseState.PENDING } - await sequelizeTypescript.transaction(async t => { + const videoAbuse: VideoAbuseModel = await sequelizeTypescript.transaction(async t => { const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) videoAbuseInstance.Video = videoInstance videoAbuseInstance.Account = reporterAccount @@ -74,8 +116,12 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) { } auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) - logger.info('Abuse report for video %s created.', videoInstance.name) + + return videoAbuseInstance }) - return res.type('json').status(204).end() + logger.info('Abuse report for video %s created.', videoInstance.name) + return res.json({ + videoAbuse: videoAbuse.toFormattedJSON() + }).end() } 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' import { peertubeTruncate } from '../../core-utils' import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' import { - isVideoAbuseReasonValid, isVideoDurationValid, isVideoNameValid, isVideoStateValid, @@ -13,6 +12,7 @@ import { } from '../videos' import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' import { VideoState } from '../../../../shared/models/videos' +import { isVideoAbuseReasonValid } from '../video-abuses' function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { 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 @@ +import { Response } from 'express' +import * as validator from 'validator' +import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' +import { exists } from './misc' +import { VideoAbuseModel } from '../../models/video/video-abuse' + +const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES + +function isVideoAbuseReasonValid (value: string) { + return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) +} + +function isVideoAbuseModerationCommentValid (value: string) { + return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) +} + +function isVideoAbuseStateValid (value: string) { + return exists(value) && VIDEO_ABUSE_STATES[ value ] !== undefined +} + +async function isVideoAbuseExist (abuseId: number, videoId: number, res: Response) { + const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId) + + if (videoAbuse === null) { + res.status(404) + .json({ error: 'Video abuse not found' }) + .end() + + return false + } + + res.locals.videoAbuse = videoAbuse + return true +} + +// --------------------------------------------------------------------------- + +export { + isVideoAbuseExist, + isVideoAbuseStateValid, + isVideoAbuseReasonValid, + isVideoAbuseModerationCommentValid +} 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' import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' import { CONSTRAINTS_FIELDS, + VIDEO_ABUSE_STATES, VIDEO_CATEGORIES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, @@ -18,6 +19,7 @@ import { exists, isArray, isFileValid } from './misc' import { VideoChannelModel } from '../../models/video/video-channel' import { UserModel } from '../../models/account/user' import * as magnetUtil from 'magnet-uri' +import { VideoAbuseModel } from '../../models/video/video-abuse' const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES @@ -71,10 +73,6 @@ function isVideoTagsValid (tags: string[]) { ) } -function isVideoAbuseReasonValid (value: string) { - return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) -} - function isVideoViewsValid (value: string) { return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS) } @@ -220,7 +218,6 @@ export { isVideoTagsValid, isVideoFPSResolutionValid, isScheduleVideoUpdatePrivacyValid, - isVideoAbuseReasonValid, isVideoFile, isVideoMagnetUriValid, 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' import { JobType, VideoRateType, VideoState } from '../../shared/models' import { ActivityPubActorType } from '../../shared/models/activitypub' import { FollowState } from '../../shared/models/actors' -import { VideoPrivacy } from '../../shared/models/videos' +import { VideoPrivacy, VideoAbuseState } from '../../shared/models/videos' // Do not use barrels, remain constants as independent as possible import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' @@ -15,7 +15,7 @@ let config: IConfig = require('config') // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 245 +const LAST_MIGRATION_VERSION = 250 // --------------------------------------------------------------------------- @@ -258,7 +258,8 @@ const CONSTRAINTS_FIELDS = { BLOCKED_REASON: { min: 3, max: 250 } // Length }, VIDEO_ABUSES: { - REASON: { min: 2, max: 300 } // Length + REASON: { min: 2, max: 300 }, // Length + MODERATION_COMMENT: { min: 2, max: 300 } // Length }, VIDEO_CHANNELS: { NAME: { min: 3, max: 120 }, // Length @@ -409,6 +410,12 @@ const VIDEO_IMPORT_STATES = { [VideoImportState.SUCCESS]: 'Success' } +const VIDEO_ABUSE_STATES = { + [VideoAbuseState.PENDING]: 'Pending', + [VideoAbuseState.REJECTED]: 'Rejected', + [VideoAbuseState.ACCEPTED]: 'Accepted' +} + const VIDEO_MIMETYPE_EXT = { 'video/webm': '.webm', 'video/ogg': '.ogv', @@ -625,6 +632,7 @@ export { VIDEO_MIMETYPE_EXT, VIDEO_TRANSCODING_FPS, FFMPEG_NICE, + VIDEO_ABUSE_STATES, JOB_REQUEST_TIMEOUT, USER_PASSWORD_RESET_LIFETIME, 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 @@ +import * as Sequelize from 'sequelize' +import { CONSTRAINTS_FIELDS } from '../constants' +import { VideoAbuseState } from '../../../shared/models/videos' + +async function up (utils: { + transaction: Sequelize.Transaction + queryInterface: Sequelize.QueryInterface + sequelize: Sequelize.Sequelize +}): Promise { + { + const data = { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null + } + await utils.queryInterface.addColumn('videoAbuse', 'state', data) + } + + { + const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING + await utils.sequelize.query(query) + } + + { + const data = { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: null + } + await utils.queryInterface.changeColumn('videoAbuse', 'state', data) + } + + { + const data = { + type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max), + allowNull: true, + defaultValue: null + } + await utils.queryInterface.addColumn('videoAbuse', 'moderationComment', data) + } +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { 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 @@ -import { ActivityCreate, VideoTorrentObject } from '../../../../shared' +import { ActivityCreate, VideoAbuseState, VideoTorrentObject } from '../../../../shared' import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' import { retryTransactionWrapper } from '../../../helpers/database-utils' @@ -112,7 +112,8 @@ async function processCreateVideoAbuse (actor: ActorModel, videoAbuseToCreateDat const videoAbuseData = { reporterAccountId: account.id, reason: videoAbuseToCreateData.content, - videoId: video.id + videoId: video.id, + state: VideoAbuseState.PENDING } 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' export * from './sort' export * from './users' export * from './videos' +export * from './video-abuses' export * from './video-blacklist' export * from './video-channels' export * 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 @@ +import * as express from 'express' +import 'express-validator' +import { body, param } from 'express-validator/check' +import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' +import { isVideoExist } from '../../helpers/custom-validators/videos' +import { logger } from '../../helpers/logger' +import { areValidationErrors } from './utils' +import { + isVideoAbuseExist, + isVideoAbuseModerationCommentValid, + isVideoAbuseReasonValid, + isVideoAbuseStateValid +} from '../../helpers/custom-validators/video-abuses' + +const videoAbuseReportValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + + return next() + } +] + +const videoAbuseGetValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return + + return next() + } +] + +const videoAbuseUpdateValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), + body('state') + .optional() + .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'), + body('moderationComment') + .optional() + .custom(isVideoAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + videoAbuseReportValidator, + videoAbuseGetValidator, + videoAbuseUpdateValidator +} 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 { import { checkUserCanManageVideo, isScheduleVideoUpdatePrivacyValid, - isVideoAbuseReasonValid, isVideoCategoryValid, isVideoChannelOfAccountExist, isVideoDescriptionValid, @@ -174,20 +173,6 @@ const videosRemoveValidator = [ } ] -const videoAbuseReportValidator = [ - param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), - body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'), - - async (req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) - - if (areValidationErrors(req, res)) return - if (!await isVideoExist(req.params.id, res)) return - - return next() - } -] - const videoRateValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), @@ -299,8 +284,6 @@ export { videosRemoveValidator, videosShareValidator, - videoAbuseReportValidator, - videoRateValidator, 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 @@ -import { AfterCreate, AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { + AfterCreate, + AllowNull, + BelongsTo, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + Is, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' import { VideoAbuse } from '../../../shared/models/videos' -import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos' +import { + isVideoAbuseModerationCommentValid, + isVideoAbuseReasonValid, + isVideoAbuseStateValid +} from '../../helpers/custom-validators/video-abuses' import { Emailer } from '../../lib/emailer' import { AccountModel } from '../account/account' import { getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' +import { VideoAbuseState } from '../../../shared' +import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' @Table({ tableName: 'videoAbuse', @@ -25,6 +44,18 @@ export class VideoAbuseModel extends Model { @Column reason: string + @AllowNull(false) + @Default(null) + @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state')) + @Column + state: VideoAbuseState + + @AllowNull(true) + @Default(null) + @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) + moderationComment: string + @CreatedAt createdAt: Date @@ -60,6 +91,16 @@ export class VideoAbuseModel extends Model { return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) } + static loadByIdAndVideoId (id: number, videoId: number) { + const query = { + where: { + id, + videoId + } + } + return VideoAbuseModel.findOne(query) + } + static listForApi (start: number, count: number, sort: string) { const query = { offset: start, @@ -88,6 +129,11 @@ export class VideoAbuseModel extends Model { id: this.id, reason: this.reason, reporterAccount: this.Account.toFormattedJSON(), + state: { + id: this.state, + label: VideoAbuseModel.getStateLabel(this.state) + }, + moderationComment: this.moderationComment, video: { id: this.Video.id, uuid: this.Video.uuid, @@ -105,4 +151,8 @@ export class VideoAbuseModel extends Model { object: this.Video.url } } + + private static getStateLabel (id: number) { + return VIDEO_ABUSE_STATES[id] || 'Unknown' + } } 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 { video } } + private static getStateLabel (id: number) { return VIDEO_IMPORT_STATES[id] || 'Unknown' } 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 @@ import 'mocha' import { - createUser, flushTests, killallServers, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, - uploadVideo, userLogin + createUser, + deleteVideoAbuse, + flushTests, + killallServers, + makeGetRequest, + makePostBodyRequest, + runServer, + ServerInfo, + setAccessTokensToServers, + updateVideoAbuse, + uploadVideo, + userLogin } from '../../utils' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' +import { VideoAbuseState } from '../../../../shared/models/videos' describe('Test video abuses API validators', function () { let server: ServerInfo let userAccessToken = '' + let videoAbuseId: number // --------------------------------------------------------------- @@ -67,44 +79,111 @@ describe('Test video abuses API validators', function () { describe('When reporting a video abuse', function () { const basePath = '/api/v1/videos/' + let path: string + + before(() => { + path = basePath + server.video.id + '/abuse' + }) it('Should fail with nothing', async function () { - const path = basePath + server.video.id + '/abuse' const fields = {} await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) it('Should fail with a wrong video', async function () { const wrongPath = '/api/v1/videos/blabla/abuse' - const fields = { - reason: 'my super reason' - } + const fields = { reason: 'my super reason' } + await makePostBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields }) }) it('Should fail with a non authenticated user', async function () { - const path = basePath + server.video.id + '/abuse' - const fields = { - reason: 'my super reason' - } + const fields = { reason: 'my super reason' } + await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) }) it('Should fail with a reason too short', async function () { - const path = basePath + server.video.id + '/abuse' - const fields = { - reason: 'h' - } + const fields = { reason: 'h' } + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) it('Should fail with a reason too big', async function () { - const path = basePath + server.video.id + '/abuse' - const fields = { - reason: 'super'.repeat(61) - } + const fields = { reason: 'super'.repeat(61) } + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) + + it('Should succeed with the correct parameters', async function () { + const fields = { reason: 'super reason' } + + const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) + videoAbuseId = res.body.videoAbuse.id + }) + }) + + describe('When updating a video abuse', function () { + const basePath = '/api/v1/videos/' + let path: string + + before(() => { + path = basePath + server.video.id + '/abuse/' + videoAbuseId + }) + + it('Should fail with a non authenticated user', async function () { + await updateVideoAbuse(server.url, 'blabla', server.video.uuid, videoAbuseId, {}, 401) + }) + + it('Should fail with a non admin user', async function () { + await updateVideoAbuse(server.url, userAccessToken, server.video.uuid, videoAbuseId, {}, 403) + }) + + it('Should fail with a bad video id or bad video abuse id', async function () { + await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, 45, {}, 404) + await updateVideoAbuse(server.url, server.accessToken, 52, videoAbuseId, {}, 404) + }) + + it('Should fail with a bad state', async function () { + const body = { state: 5 } + await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400) + }) + + it('Should fail with a bad moderation comment', async function () { + const body = { moderationComment: 'b'.repeat(305) } + await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400) + }) + + it('Should succeed with the correct params', async function () { + const body = { state: VideoAbuseState.ACCEPTED } + await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body) + }) + }) + + describe('When deleting a video abuse', function () { + const basePath = '/api/v1/videos/' + let path: string + + before(() => { + path = basePath + server.video.id + '/abuse/' + videoAbuseId + }) + + it('Should fail with a non authenticated user', async function () { + await deleteVideoAbuse(server.url, 'blabla', server.video.uuid, videoAbuseId, 401) + }) + + it('Should fail with a non admin user', async function () { + await deleteVideoAbuse(server.url, userAccessToken, server.video.uuid, videoAbuseId, 403) + }) + + it('Should fail with a bad video id or bad video abuse id', async function () { + await deleteVideoAbuse(server.url, server.accessToken, server.video.uuid, 45, 404) + await deleteVideoAbuse(server.url, server.accessToken, 52, videoAbuseId, 404) + }) + + it('Should succeed with the correct params', async function () { + await deleteVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId) + }) }) 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 @@ import * as chai from 'chai' import 'mocha' -import { VideoAbuse } from '../../../../shared/models/videos' +import { VideoAbuse, VideoAbuseState } from '../../../../shared/models/videos' import { + deleteVideoAbuse, flushAndRunMultipleServers, getVideoAbusesList, getVideosList, @@ -11,6 +12,7 @@ import { reportVideoAbuse, ServerInfo, setAccessTokensToServers, + updateVideoAbuse, uploadVideo } from '../../utils/index' import { doubleFollow } from '../../utils/server/follows' @@ -20,6 +22,7 @@ const expect = chai.expect describe('Test video abuses', function () { let servers: ServerInfo[] = [] + let abuseServer2: VideoAbuse before(async function () { this.timeout(50000) @@ -105,7 +108,7 @@ describe('Test video abuses', function () { await waitJobs(servers) }) - it('Should have 2 video abuse on server 1 and 1 on server 2', async function () { + it('Should have 2 video abuses on server 1 and 1 on server 2', async function () { const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken) expect(res1.body.total).to.equal(2) expect(res1.body.data).to.be.an('array') @@ -116,22 +119,57 @@ describe('Test video abuses', function () { expect(abuse1.reporterAccount.name).to.equal('root') expect(abuse1.reporterAccount.host).to.equal('localhost:9001') expect(abuse1.video.id).to.equal(servers[0].video.id) + expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING) + expect(abuse1.state.label).to.equal('Pending') + expect(abuse1.moderationComment).to.be.null const abuse2: VideoAbuse = res1.body.data[1] expect(abuse2.reason).to.equal('my super bad reason 2') expect(abuse2.reporterAccount.name).to.equal('root') expect(abuse2.reporterAccount.host).to.equal('localhost:9001') expect(abuse2.video.id).to.equal(servers[1].video.id) + expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING) + expect(abuse2.state.label).to.equal('Pending') + expect(abuse2.moderationComment).to.be.null const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken) expect(res2.body.total).to.equal(1) expect(res2.body.data).to.be.an('array') expect(res2.body.data.length).to.equal(1) - const abuse3: VideoAbuse = res2.body.data[0] - expect(abuse3.reason).to.equal('my super bad reason 2') - expect(abuse3.reporterAccount.name).to.equal('root') - expect(abuse3.reporterAccount.host).to.equal('localhost:9001') + abuseServer2 = res2.body.data[0] + expect(abuseServer2.reason).to.equal('my super bad reason 2') + expect(abuseServer2.reporterAccount.name).to.equal('root') + expect(abuseServer2.reporterAccount.host).to.equal('localhost:9001') + expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING) + expect(abuseServer2.state.label).to.equal('Pending') + expect(abuseServer2.moderationComment).to.be.null + }) + + it('Should update the state of a video abuse', async function () { + const body = { state: VideoAbuseState.REJECTED } + await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) + + const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) + expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED) + }) + + it('Should add a moderation comment', async function () { + const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' } + await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) + + const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) + expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED) + expect(res.body.data[0].moderationComment).to.equal('It is valid') + }) + + it('Should delete the video abuse', async function () { + await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id) + + const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) + expect(res.body.total).to.equal(0) + expect(res.body.data).to.be.an('array') + expect(res.body.data.length).to.equal(0) }) 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 @@ import * as request from 'supertest' +import { VideoAbuseUpdate } from '../../../../shared/models/videos/video-abuse-update.model' +import { makeDeleteRequest, makePutBodyRequest } from '..' -function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 204) { +function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) { const path = '/api/v1/videos/' + videoId + '/abuse' return request(url) @@ -23,9 +25,41 @@ function getVideoAbusesList (url: string, token: string) { .expect('Content-Type', /json/) } +function updateVideoAbuse ( + url: string, + token: string, + videoId: string | number, + videoAbuseId: number, + body: VideoAbuseUpdate, + statusCodeExpected = 204 +) { + const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId + + return makePutBodyRequest({ + url, + token, + path, + fields: body, + statusCodeExpected + }) +} + +function deleteVideoAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) { + const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId + + return makeDeleteRequest({ + url, + token, + path, + statusCodeExpected + }) +} + // --------------------------------------------------------------------------- export { reportVideoAbuse, - getVideoAbusesList + getVideoAbusesList, + updateVideoAbuse, + deleteVideoAbuse } diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 17cf8be24..2d7de2a0d 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -1,6 +1,7 @@ export * from './user-video-rate-update.model' export * from './user-video-rate.model' export * from './user-video-rate.type' +export * from './video-abuse-state.model' export * from './video-abuse-create.model' export * from './video-abuse.model' export * from './video-blacklist.model' diff --git a/shared/models/videos/video-abuse-state.model.ts b/shared/models/videos/video-abuse-state.model.ts new file mode 100644 index 000000000..529f034bd --- /dev/null +++ b/shared/models/videos/video-abuse-state.model.ts @@ -0,0 +1,5 @@ +export enum VideoAbuseState { + PENDING = 1, + REJECTED = 2, + ACCEPTED = 3 +} diff --git a/shared/models/videos/video-abuse-update.model.ts b/shared/models/videos/video-abuse-update.model.ts new file mode 100644 index 000000000..9b32aae48 --- /dev/null +++ b/shared/models/videos/video-abuse-update.model.ts @@ -0,0 +1,6 @@ +import { VideoAbuseState } from './video-abuse-state.model' + +export interface VideoAbuseUpdate { + moderationComment?: string + state?: VideoAbuseState +} diff --git a/shared/models/videos/video-abuse.model.ts b/shared/models/videos/video-abuse.model.ts index 51070a7a3..1fecce037 100644 --- a/shared/models/videos/video-abuse.model.ts +++ b/shared/models/videos/video-abuse.model.ts @@ -1,14 +1,21 @@ import { Account } from '../actors' +import { VideoConstant } from './video-constant.model' +import { VideoAbuseState } from './video-abuse-state.model' export interface VideoAbuse { id: number reason: string reporterAccount: Account + + state: VideoConstant + moderationComment?: string + video: { id: number name: string uuid: string url: string } + createdAt: Date }