]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add state and moderationComment for abuses on server side
authorChocobozzz <me@florianbigard.com>
Fri, 10 Aug 2018 14:54:01 +0000 (16:54 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 10 Aug 2018 14:54:01 +0000 (16:54 +0200)
19 files changed:
server/controllers/api/videos/abuse.ts
server/helpers/custom-validators/activitypub/videos.ts
server/helpers/custom-validators/video-abuses.ts [new file with mode: 0644]
server/helpers/custom-validators/videos.ts
server/initializers/constants.ts
server/initializers/migrations/0250-video-abuse-state.ts [new file with mode: 0644]
server/lib/activitypub/process/process-create.ts
server/middlewares/validators/index.ts
server/middlewares/validators/video-abuses.ts [new file with mode: 0644]
server/middlewares/validators/videos.ts
server/models/video/video-abuse.ts
server/models/video/video-import.ts
server/tests/api/check-params/video-abuses.ts
server/tests/api/videos/video-abuse.ts
server/tests/utils/videos/video-abuses.ts
shared/models/videos/index.ts
shared/models/videos/video-abuse-state.model.ts [new file with mode: 0644]
shared/models/videos/video-abuse-update.model.ts [new file with mode: 0644]
shared/models/videos/video-abuse.model.ts

index 7782fc6390cb3aa187d11ba23b5f5cc3e632548a..59bdf6257c3282302d594be51e376a2e5a0b48b1 100644 (file)
@@ -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()
 }
index b8075f3c79389dcb580f1cae71a0c4e9fc76a724..702c09842e23918fe5ffffbef057754827faa55f 100644 (file)
@@ -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 (file)
index 0000000..290efb1
--- /dev/null
@@ -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
+}
index f4c1c8b0720d5f1944ed8f59f0c3ddf16a5515de..5e6cfe2176a8e96a8938d15b253f02218fffd4d0 100644 (file)
@@ -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,
index ea561b686faa701a6ff8742227f26ae6d41ccfaa..a008bf4c57905cd39b117ae3cebbd22953ac97f5 100644 (file)
@@ -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 (file)
index 0000000..acb668a
--- /dev/null
@@ -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<any> {
+  {
+    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 }
index 6364bf135be31c23582e44c5a421e9813de0db06..791148919f2958c2e1a6e9528543cbb478d2962d 100644 (file)
@@ -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)
index c5400c8f5b5abb2aaf75d7b2944e890f94f8a86a..ccbedd57d85130b173a1b6179098fb3a0b615805 100644 (file)
@@ -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 (file)
index 0000000..f15d55a
--- /dev/null
@@ -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
+}
index c812d4677e28da3762284d4cef087526d5e8f95b..203a00876c3a807a9b3c9b8acc58be78f857d0ab 100644 (file)
@@ -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
index 39f0c2cb2c6ae605e733d8d70b8762b4f110f0c9..10a191372207930d2409a58213bc73b2b5179ea4 100644 (file)
@@ -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<VideoAbuseModel> {
   @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<VideoAbuseModel> {
     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<VideoAbuseModel> {
       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<VideoAbuseModel> {
       object: this.Video.url
     }
   }
+
+  private static getStateLabel (id: number) {
+    return VIDEO_ABUSE_STATES[id] || 'Unknown'
+  }
 }
index b794d83244af4361e234673d822af2ea2b24b875..9d1f783c75e3a91d6cd647544ec13d89b15e2b0e 100644 (file)
@@ -171,6 +171,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
       video
     }
   }
+
   private static getStateLabel (id: number) {
     return VIDEO_IMPORT_STATES[id] || 'Unknown'
   }
index 68b965bbed8cd2571ed78498bc72dd278cff0d87..d2bed6a2a2e59cf60145f60517b76b99f751fde4 100644 (file)
@@ -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 () {
index dde309b96a5a5746d80748836d4305ac22336000..a17f3c8de953dae10b2ab20a2a2455d2c12a1c24 100644 (file)
@@ -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 () {
index 0d72bf457a8384a0fde325df6cad0a0c3a72be5e..5f138d6b3b773bb18918cd21c7c2bb5df5aeba92 100644 (file)
@@ -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
 }
index 17cf8be24883441dbb59fc1e5645d750822b3d98..2d7de2a0d08d708cdd5f3b92692312959df0099f 100644 (file)
@@ -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 (file)
index 0000000..529f034
--- /dev/null
@@ -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 (file)
index 0000000..9b32aae
--- /dev/null
@@ -0,0 +1,6 @@
+import { VideoAbuseState } from './video-abuse-state.model'
+
+export interface VideoAbuseUpdate {
+  moderationComment?: string
+  state?: VideoAbuseState
+}
index 51070a7a33b01e6fa36524269744bedcd02a3499..1fecce03789a88a96736e72d98475ced38b96bd5 100644 (file)
@@ -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<VideoAbuseState>
+  moderationComment?: string
+
   video: {
     id: number
     name: string
     uuid: string
     url: string
   }
+
   createdAt: Date
 }