]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add server API to abuse messages
authorChocobozzz <me@florianbigard.com>
Fri, 24 Jul 2020 13:05:51 +0000 (15:05 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Fri, 31 Jul 2020 09:35:19 +0000 (11:35 +0200)
28 files changed:
server/controllers/api/abuse.ts
server/controllers/api/users/index.ts
server/controllers/api/users/my-abuses.ts [new file with mode: 0644]
server/controllers/api/videos/abuse.ts
server/helpers/audit-logger.ts
server/helpers/custom-validators/abuses.ts
server/initializers/constants.ts
server/initializers/database.ts
server/lib/emailer.ts
server/lib/moderation.ts
server/lib/notifier.ts
server/middlewares/user-right.ts
server/middlewares/validators/abuse.ts
server/models/abuse/abuse-message.ts [new file with mode: 0644]
server/models/abuse/abuse-query-builder.ts
server/models/abuse/abuse.ts
server/tests/api/check-params/abuses.ts
server/tests/api/moderation/abuses.ts
server/tests/api/users/users.ts
server/tests/api/videos/video-abuse.ts
server/types/models/moderation/abuse-message.ts [new file with mode: 0644]
server/types/models/moderation/abuse.ts
server/types/models/moderation/index.ts
server/typings/express/index.d.ts
shared/extra-utils/moderation/abuses.ts
shared/models/moderation/abuse/abuse-message.model.ts [new file with mode: 0644]
shared/models/moderation/abuse/abuse.model.ts
shared/models/moderation/abuse/index.ts

index 04a0c06e33cf62c79b24238dbe02385287dcdbc8..50d068157a5734b84ab82d78c8e062ad1d77a8fd 100644 (file)
@@ -1,20 +1,24 @@
 import * as express from 'express'
 import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
 import { AbuseModel } from '@server/models/abuse/abuse'
+import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
 import { getServerActor } from '@server/models/application/application'
 import { AbuseCreate, abusePredefinedReasonsMap, AbuseState, UserRight } from '../../../shared'
 import { getFormattedObjects } from '../../helpers/utils'
 import { sequelizeTypescript } from '../../initializers/database'
 import {
   abuseGetValidator,
-  abuseListValidator,
+  abuseListForAdminsValidator,
   abuseReportValidator,
   abusesSortValidator,
   abuseUpdateValidator,
+  addAbuseMessageValidator,
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
   authenticate,
+  deleteAbuseMessageValidator,
   ensureUserHasRight,
+  getAbuseValidator,
   paginationValidator,
   setDefaultPagination,
   setDefaultSort
@@ -30,8 +34,8 @@ abuseRouter.get('/',
   abusesSortValidator,
   setDefaultSort,
   setDefaultPagination,
-  abuseListValidator,
-  asyncMiddleware(listAbuses)
+  abuseListForAdminsValidator,
+  asyncMiddleware(listAbusesForAdmins)
 )
 abuseRouter.put('/:id',
   authenticate,
@@ -51,13 +55,33 @@ abuseRouter.delete('/:id',
   asyncRetryTransactionMiddleware(deleteAbuse)
 )
 
+abuseRouter.get('/:id/messages',
+  authenticate,
+  asyncMiddleware(getAbuseValidator),
+  asyncRetryTransactionMiddleware(listAbuseMessages)
+)
+
+abuseRouter.post('/:id/messages',
+  authenticate,
+  asyncMiddleware(getAbuseValidator),
+  addAbuseMessageValidator,
+  asyncRetryTransactionMiddleware(addAbuseMessage)
+)
+
+abuseRouter.delete('/:id/messages/:messageId',
+  authenticate,
+  asyncMiddleware(getAbuseValidator),
+  asyncMiddleware(deleteAbuseMessageValidator),
+  asyncRetryTransactionMiddleware(deleteAbuseMessage)
+)
+
 // ---------------------------------------------------------------------------
 
 export {
   abuseRouter,
 
   // FIXME: deprecated in 2.3. Remove these exports
-  listAbuses,
+  listAbusesForAdmins,
   updateAbuse,
   deleteAbuse,
   reportAbuse
@@ -65,11 +89,11 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function listAbuses (req: express.Request, res: express.Response) {
+async function listAbusesForAdmins (req: express.Request, res: express.Response) {
   const user = res.locals.oauth.token.user
   const serverActor = await getServerActor()
 
-  const resultList = await AbuseModel.listForApi({
+  const resultList = await AbuseModel.listForAdminApi({
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
@@ -87,7 +111,10 @@ async function listAbuses (req: express.Request, res: express.Response) {
     user
   })
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  return res.json({
+    total: resultList.total,
+    data: resultList.data.map(d => d.toFormattedAdminJSON())
+  })
 }
 
 async function updateAbuse (req: express.Request, res: express.Response) {
@@ -100,6 +127,8 @@ async function updateAbuse (req: express.Request, res: express.Response) {
     return abuse.save({ transaction: t })
   })
 
+  // TODO: Notification
+
   // Do not send the delete to other instances, we updated OUR copy of this abuse
 
   return res.type('json').status(204).end()
@@ -166,3 +195,41 @@ async function reportAbuse (req: express.Request, res: express.Response) {
 
   return res.json({ abuse: { id } })
 }
+
+async function listAbuseMessages (req: express.Request, res: express.Response) {
+  const abuse = res.locals.abuse
+
+  const resultList = await AbuseMessageModel.listForApi(abuse.id)
+
+  return res.json(getFormattedObjects(resultList.data, resultList.total))
+}
+
+async function addAbuseMessage (req: express.Request, res: express.Response) {
+  const abuse = res.locals.abuse
+  const user = res.locals.oauth.token.user
+
+  const abuseMessage = await AbuseMessageModel.create({
+    message: req.body.message,
+    byModerator: abuse.reporterAccountId !== user.Account.id,
+    accountId: user.Account.id,
+    abuseId: abuse.id
+  })
+
+  // TODO: Notification
+
+  return res.json({
+    abuseMessage: {
+      id: abuseMessage.id
+    }
+  })
+}
+
+async function deleteAbuseMessage (req: express.Request, res: express.Response) {
+  const abuseMessage = res.locals.abuseMessage
+
+  await sequelizeTypescript.transaction(t => {
+    return abuseMessage.destroy({ transaction: t })
+  })
+
+  return res.sendStatus(204)
+}
index 5939f612523f8441a9609f8a36f11de84f9ac53a..d339c2a1c0abf498c36d48b5bdf60f265471c477 100644 (file)
@@ -1,10 +1,20 @@
 import * as express from 'express'
 import * as RateLimit from 'express-rate-limit'
+import { tokensRouter } from '@server/controllers/api/users/token'
+import { Hooks } from '@server/lib/plugins/hooks'
+import { MUser, MUserAccountDefault } from '@server/types/models'
 import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
+import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
+import { UserRegister } from '../../../../shared/models/users/user-register.model'
+import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
 import { logger } from '../../../helpers/logger'
 import { generateRandomString, getFormattedObjects } from '../../../helpers/utils'
+import { CONFIG } from '../../../initializers/config'
 import { WEBSERVER } from '../../../initializers/constants'
+import { sequelizeTypescript } from '../../../initializers/database'
 import { Emailer } from '../../../lib/emailer'
+import { Notifier } from '../../../lib/notifier'
+import { deleteUserToken } from '../../../lib/oauth-model'
 import { Redis } from '../../../lib/redis'
 import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
 import {
@@ -18,9 +28,9 @@ import {
   setDefaultPagination,
   setDefaultSort,
   userAutocompleteValidator,
-  usersListValidator,
   usersAddValidator,
   usersGetValidator,
+  usersListValidator,
   usersRegisterValidator,
   usersRemoveValidator,
   usersSortValidator,
@@ -35,22 +45,13 @@ import {
   usersVerifyEmailValidator
 } from '../../../middlewares/validators'
 import { UserModel } from '../../../models/account/user'
-import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
 import { meRouter } from './me'
-import { deleteUserToken } from '../../../lib/oauth-model'
+import { myAbusesRouter } from './my-abuses'
 import { myBlocklistRouter } from './my-blocklist'
-import { myVideoPlaylistsRouter } from './my-video-playlists'
 import { myVideosHistoryRouter } from './my-history'
 import { myNotificationsRouter } from './my-notifications'
-import { Notifier } from '../../../lib/notifier'
 import { mySubscriptionsRouter } from './my-subscriptions'
-import { CONFIG } from '../../../initializers/config'
-import { sequelizeTypescript } from '../../../initializers/database'
-import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
-import { UserRegister } from '../../../../shared/models/users/user-register.model'
-import { MUser, MUserAccountDefault } from '@server/types/models'
-import { Hooks } from '@server/lib/plugins/hooks'
-import { tokensRouter } from '@server/controllers/api/users/token'
+import { myVideoPlaylistsRouter } from './my-video-playlists'
 
 const auditLogger = auditLoggerFactory('users')
 
@@ -72,6 +73,7 @@ usersRouter.use('/', mySubscriptionsRouter)
 usersRouter.use('/', myBlocklistRouter)
 usersRouter.use('/', myVideosHistoryRouter)
 usersRouter.use('/', myVideoPlaylistsRouter)
+usersRouter.use('/', myAbusesRouter)
 usersRouter.use('/', meRouter)
 
 usersRouter.get('/autocomplete',
diff --git a/server/controllers/api/users/my-abuses.ts b/server/controllers/api/users/my-abuses.ts
new file mode 100644 (file)
index 0000000..e43fc48
--- /dev/null
@@ -0,0 +1,48 @@
+import * as express from 'express'
+import { AbuseModel } from '@server/models/abuse/abuse'
+import {
+  abuseListForUserValidator,
+  abusesSortValidator,
+  asyncMiddleware,
+  authenticate,
+  paginationValidator,
+  setDefaultPagination,
+  setDefaultSort
+} from '../../../middlewares'
+
+const myAbusesRouter = express.Router()
+
+myAbusesRouter.get('/me/abuses',
+  authenticate,
+  paginationValidator,
+  abusesSortValidator,
+  setDefaultSort,
+  setDefaultPagination,
+  abuseListForUserValidator,
+  asyncMiddleware(listMyAbuses)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  myAbusesRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function listMyAbuses (req: express.Request, res: express.Response) {
+  const resultList = await AbuseModel.listForUserApi({
+    start: req.query.start,
+    count: req.query.count,
+    sort: req.query.sort,
+    id: req.query.id,
+    search: req.query.search,
+    state: req.query.state,
+    user: res.locals.oauth.token.User
+  })
+
+  return res.json({
+    total: resultList.total,
+    data: resultList.data.map(d => d.toFormattedAdminJSON())
+  })
+}
index b92a66360a684ad4599c742fea705a958690123e..9c4d008496c247ddadd92e92fc112cddc4540356 100644 (file)
@@ -2,7 +2,6 @@ import * as express from 'express'
 import { AbuseModel } from '@server/models/abuse/abuse'
 import { getServerActor } from '@server/models/application/application'
 import { AbuseCreate, UserRight, VideoAbuseCreate } from '../../../../shared'
-import { getFormattedObjects } from '../../../helpers/utils'
 import {
   abusesSortValidator,
   asyncMiddleware,
@@ -63,7 +62,7 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
   const user = res.locals.oauth.token.user
   const serverActor = await getServerActor()
 
-  const resultList = await AbuseModel.listForApi({
+  const resultList = await AbuseModel.listForAdminApi({
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
@@ -81,7 +80,10 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
     user
   })
 
-  return res.json(getFormattedObjects(resultList.data, resultList.total))
+  return res.json({
+    total: resultList.total,
+    data: resultList.data.map(d => d.toFormattedAdminJSON())
+  })
 }
 
 async function updateVideoAbuse (req: express.Request, res: express.Response) {
index 954b0b69da8a3abe9666c3484c562d8712d38092..6aae5e82112725fc6d843d91c68315e4469741e8 100644 (file)
@@ -5,7 +5,7 @@ import { chain } from 'lodash'
 import * as path from 'path'
 import * as winston from 'winston'
 import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
-import { Abuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
+import { AdminAbuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
 import { CustomConfig } from '../../shared/models/server/custom-config.model'
 import { VideoComment } from '../../shared/models/videos/video-comment.model'
 import { CONFIG } from '../initializers/config'
@@ -219,7 +219,7 @@ const abuseKeysToKeep = [
   'createdAt'
 ]
 class AbuseAuditView extends EntityAuditView {
-  constructor (private readonly abuse: Abuse) {
+  constructor (private readonly abuse: AdminAbuse) {
     super(abuseKeysToKeep, 'abuse', abuse)
   }
 }
index 0ca06a2522df7124f1288219860b6e9a215f04ed..0ddde4b066b1e1fedd05705ceff88566125acba7 100644 (file)
@@ -4,6 +4,7 @@ import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
 import { exists, isArray } from './misc'
 
 const ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
+const ABUSE_MESSAGES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSE_MESSAGES
 
 function isAbuseReasonValid (value: string) {
   return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.REASON)
@@ -46,13 +47,18 @@ function isAbuseVideoIsValid (value: AbuseVideoIs) {
   )
 }
 
+function isAbuseMessageValid (value: string) {
+  return exists(value) && validator.isLength(value, ABUSE_MESSAGES_CONSTRAINTS_FIELDS.MESSAGE)
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   isAbuseReasonValid,
   isAbuseFilterValid,
   isAbusePredefinedReasonValid,
-  areAbusePredefinedReasonsValid as isAbusePredefinedReasonsValid,
+  isAbuseMessageValid,
+  areAbusePredefinedReasonsValid,
   isAbuseTimestampValid,
   isAbuseTimestampCoherent,
   isAbuseModerationCommentValid,
index fd5bf58685e1cf82d3973b5e92dcf1b40358c356..a40a2239580c76aef1cc2a1ce65968d6aba194e6 100644 (file)
@@ -206,6 +206,9 @@ const CONSTRAINTS_FIELDS = {
     REASON: { min: 2, max: 3000 }, // Length
     MODERATION_COMMENT: { min: 2, max: 3000 } // Length
   },
+  ABUSE_MESSAGES: {
+    MESSAGE: { min: 2, max: 3000 } // Length
+  },
   VIDEO_BLACKLIST: {
     REASON: { min: 2, max: 300 } // Length
   },
index 0775f1fadc75ed56ede1826f62ee7cc440821c00..8ce32f6fa9bf945f59484e74014d3521845e32f0 100644 (file)
@@ -1,6 +1,7 @@
 import { QueryTypes, Transaction } from 'sequelize'
 import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
 import { AbuseModel } from '@server/models/abuse/abuse'
+import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
 import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
 import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
 import { isTestInstance } from '../helpers/core-utils'
@@ -87,6 +88,7 @@ async function initDatabaseModels (silent: boolean) {
     TagModel,
     AccountVideoRateModel,
     UserModel,
+    AbuseMessageModel,
     AbuseModel,
     VideoCommentAbuseModel,
     VideoAbuseModel,
index 48ba7421e4c80370866ee9c92104c8aa37a0bb77..c6ad03328d256acb00967cfb740eb948937a1b74 100644 (file)
@@ -5,7 +5,7 @@ import { join } from 'path'
 import { VideoChannelModel } from '@server/models/video/video-channel'
 import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
 import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
-import { Abuse, EmailPayload } from '@shared/models'
+import { UserAbuse, EmailPayload } from '@shared/models'
 import { SendEmailOptions } from '../../shared/models/server/emailer.model'
 import { isTestInstance, root } from '../helpers/core-utils'
 import { bunyanLogger, logger } from '../helpers/logger'
@@ -283,7 +283,7 @@ class Emailer {
   }
 
   addAbuseModeratorsNotification (to: string[], parameters: {
-    abuse: Abuse
+    abuse: UserAbuse
     abuseInstance: MAbuseFull
     reporter: string
   }) {
index 4fc9cd747609a22d0d612caafc8e6c1fb944b566..b140d5aa9ad054105a7433060c0b850af4f09ae0 100644 (file)
@@ -213,7 +213,7 @@ async function createAbuse (options: {
     await sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
   }
 
-  const abuseJSON = abuseInstance.toFormattedJSON()
+  const abuseJSON = abuseInstance.toFormattedAdminJSON()
   auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON))
 
   Notifier.Instance.notifyOnNewAbuse({
index c567e1c200f945c4e604385854b2e5a2488b599a..8f165d2fd6cf8e10781974c4efe63efe83e80f85 100644 (file)
@@ -10,7 +10,7 @@ import {
 } from '@server/types/models/user'
 import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
 import { MVideoImportVideo } from '@server/types/models/video/video-import'
-import { Abuse } from '@shared/models'
+import { UserAbuse } from '@shared/models'
 import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
 import { VideoPrivacy, VideoState } from '../../shared/models/videos'
 import { logger } from '../helpers/logger'
@@ -73,7 +73,7 @@ class Notifier {
         .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
   }
 
-  notifyOnNewAbuse (parameters: { abuse: Abuse, abuseInstance: MAbuseFull, reporter: string }): void {
+  notifyOnNewAbuse (parameters: { abuse: UserAbuse, abuseInstance: MAbuseFull, reporter: string }): void {
     this.notifyModeratorsOfNewAbuse(parameters)
         .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err }))
   }
@@ -350,7 +350,7 @@ class Notifier {
   }
 
   private async notifyModeratorsOfNewAbuse (parameters: {
-    abuse: Abuse
+    abuse: UserAbuse
     abuseInstance: MAbuseFull
     reporter: string
   }) {
index 4da7b9802516d02c0ac028557e15878dc119bb8a..4d836485ccc4ec7513170dd3029b1418cc2e84ae 100644 (file)
@@ -9,11 +9,7 @@ function ensureUserHasRight (userRight: UserRight) {
       const message = `User ${user.username} does not have right ${UserRight[userRight]} to access to ${req.path}.`
       logger.info(message)
 
-      return res.status(403)
-        .json({
-          error: message
-        })
-        .end()
+      return res.status(403).json({ error: message })
     }
 
     return next()
index 966d1f7fb2fb15ccd333b0434e26f130818843c2..cb0bc658a12f4bda3cb79ea61b20588ac4b23b29 100644 (file)
@@ -2,8 +2,9 @@ import * as express from 'express'
 import { body, param, query } from 'express-validator'
 import {
   isAbuseFilterValid,
+  isAbuseMessageValid,
   isAbuseModerationCommentValid,
-  isAbusePredefinedReasonsValid,
+  areAbusePredefinedReasonsValid,
   isAbusePredefinedReasonValid,
   isAbuseReasonValid,
   isAbuseStateValid,
@@ -15,7 +16,8 @@ import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers
 import { doesCommentIdExist } from '@server/helpers/custom-validators/video-comments'
 import { logger } from '@server/helpers/logger'
 import { doesAbuseExist, doesAccountIdExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares'
-import { AbuseCreate } from '@shared/models'
+import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
+import { AbuseCreate, UserRight } from '@shared/models'
 import { areValidationErrors } from './utils'
 
 const abuseReportValidator = [
@@ -53,7 +55,7 @@ const abuseReportValidator = [
 
   body('predefinedReasons')
     .optional()
-    .custom(isAbusePredefinedReasonsValid)
+    .custom(areAbusePredefinedReasonsValid)
     .withMessage('Should have a valid list of predefined reasons'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -111,7 +113,7 @@ const abuseUpdateValidator = [
   }
 ]
 
-const abuseListValidator = [
+const abuseListForAdminsValidator = [
   query('id')
     .optional()
     .custom(isIdValid).withMessage('Should have a valid id'),
@@ -146,7 +148,7 @@ const abuseListValidator = [
     .custom(exists).withMessage('Should have a valid video channel search'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking abuseListValidator parameters', { parameters: req.body })
+    logger.debug('Checking abuseListForAdminsValidator parameters', { parameters: req.body })
 
     if (areValidationErrors(req, res)) return
 
@@ -154,6 +156,91 @@ const abuseListValidator = [
   }
 ]
 
+const abuseListForUserValidator = [
+  query('id')
+    .optional()
+    .custom(isIdValid).withMessage('Should have a valid id'),
+
+  query('search')
+    .optional()
+    .custom(exists).withMessage('Should have a valid search'),
+
+  query('state')
+    .optional()
+    .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking abuseListForUserValidator parameters', { parameters: req.body })
+
+    if (areValidationErrors(req, res)) return
+
+    return next()
+  }
+]
+
+const getAbuseValidator = [
+  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 getAbuseValidator parameters', { parameters: req.body })
+
+    if (areValidationErrors(req, res)) return
+    if (!await doesAbuseExist(req.params.id, res)) return
+
+    const user = res.locals.oauth.token.user
+    const abuse = res.locals.abuse
+
+    if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuse.reporterAccountId !== user.Account.id) {
+      const message = `User ${user.username} does not have right to get abuse ${abuse.id}`
+      logger.warn(message)
+
+      return res.status(403).json({ error: message })
+    }
+
+    return next()
+  }
+]
+
+const addAbuseMessageValidator = [
+  body('message').custom(isAbuseMessageValid).not().isEmpty().withMessage('Should have a valid abuse message'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking addAbuseMessageValidator parameters', { parameters: req.body })
+
+    if (areValidationErrors(req, res)) return
+
+    return next()
+  }
+]
+
+const deleteAbuseMessageValidator = [
+  param('messageId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid message id'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking deleteAbuseMessageValidator parameters', { parameters: req.body })
+
+    if (areValidationErrors(req, res)) return
+
+    const user = res.locals.oauth.token.user
+    const abuse = res.locals.abuse
+
+    const messageId = parseInt(req.params.messageId + '', 10)
+    const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id)
+
+    if (!abuseMessage) {
+      return res.status(404).json({ error: 'Abuse message not found' })
+    }
+
+    if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) {
+      return res.status(403).json({ error: 'Cannot delete this abuse message' })
+    }
+
+    res.locals.abuseMessage = abuseMessage
+
+    return next()
+  }
+]
+
 // FIXME: deprecated in 2.3. Remove these validators
 
 const videoAbuseReportValidator = [
@@ -167,7 +254,7 @@ const videoAbuseReportValidator = [
     .withMessage('Should have a valid reason'),
   body('predefinedReasons')
     .optional()
-    .custom(isAbusePredefinedReasonsValid)
+    .custom(areAbusePredefinedReasonsValid)
     .withMessage('Should have a valid list of predefined reasons'),
   body('startAt')
     .optional()
@@ -266,10 +353,14 @@ const videoAbuseListValidator = [
 // ---------------------------------------------------------------------------
 
 export {
-  abuseListValidator,
+  abuseListForAdminsValidator,
   abuseReportValidator,
   abuseGetValidator,
+  addAbuseMessageValidator,
   abuseUpdateValidator,
+  deleteAbuseMessageValidator,
+  abuseListForUserValidator,
+  getAbuseValidator,
   videoAbuseReportValidator,
   videoAbuseGetValidator,
   videoAbuseUpdateValidator,
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts
new file mode 100644 (file)
index 0000000..f7721c8
--- /dev/null
@@ -0,0 +1,103 @@
+import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses'
+import { AbuseMessage } from '@shared/models'
+import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
+import { throwIfNotValid, getSort } from '../utils'
+import { AbuseModel } from './abuse'
+import { MAbuseMessageFormattable, MAbuseMessage } from '@server/types/models'
+
+@Table({
+  tableName: 'abuseMessage',
+  indexes: [
+    {
+      fields: [ 'abuseId' ]
+    },
+    {
+      fields: [ 'accountId' ]
+    }
+  ]
+})
+export class AbuseMessageModel extends Model<AbuseMessageModel> {
+
+  @AllowNull(false)
+  @Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message'))
+  @Column(DataType.TEXT)
+  message: string
+
+  @AllowNull(false)
+  @Column
+  byModerator: boolean
+
+  @CreatedAt
+  createdAt: Date
+
+  @UpdatedAt
+  updatedAt: Date
+
+  @ForeignKey(() => AccountModel)
+  @Column
+  accountId: number
+
+  @BelongsTo(() => AccountModel, {
+    foreignKey: {
+      name: 'accountId',
+      allowNull: true
+    },
+    onDelete: 'set null'
+  })
+  Account: AccountModel
+
+  @ForeignKey(() => AbuseModel)
+  @Column
+  abuseId: number
+
+  @BelongsTo(() => AbuseModel, {
+    foreignKey: {
+      name: 'abuseId',
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
+  Abuse: AbuseModel
+
+  static listForApi (abuseId: number) {
+    const options = {
+      where: { abuseId },
+
+      order: getSort('createdAt'),
+
+      include: [
+        {
+          model: AccountModel.scope(AccountScopeNames.SUMMARY),
+          required: false
+        }
+      ]
+    }
+
+    return AbuseMessageModel.findAndCountAll(options)
+      .then(({ rows, count }) => ({ data: rows, total: count }))
+  }
+
+  static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> {
+    return AbuseMessageModel.findOne({
+      where: {
+        id: messageId,
+        abuseId
+      }
+    })
+  }
+
+  toFormattedJSON (this: MAbuseMessageFormattable): AbuseMessage {
+    const account = this.Account
+      ? this.Account.toFormattedSummaryJSON()
+      : null
+
+    return {
+      id: this.id,
+      byModerator: this.byModerator,
+      message: this.message,
+
+      account
+    }
+  }
+}
index 5fddcf3c439d451b4b8267fc11d7ad600c4f75c6..9d7cb75aa37cf75b8d7145fd632ceaa3c6272914 100644 (file)
@@ -26,8 +26,10 @@ export type BuildAbusesQueryOptions = {
   state?: AbuseState
 
   // accountIds
-  serverAccountId: number
-  userAccountId: number
+  serverAccountId?: number
+  userAccountId?: number
+
+  reporterAccountId?: number
 }
 
 function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | 'id') {
@@ -45,7 +47,14 @@ function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' |
     'LEFT JOIN "videoComment" ON "commentAbuse"."videoCommentId" = "videoComment"."id"'
   ]
 
-  whereAnd.push('"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
+  if (options.serverAccountId || options.userAccountId) {
+    whereAnd.push('"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
+  }
+
+  if (options.reporterAccountId) {
+    whereAnd.push('"abuse"."reporterAccountId" = :reporterAccountId')
+    replacements.reporterAccountId = options.reporterAccountId
+  }
 
   if (options.search) {
     const searchWhereOr = [
index bd96cf79c5dd6885a8ea34d623b76471853bd9c1..7002502d547436880ecda35abc2d7d97299d3897 100644 (file)
@@ -18,7 +18,6 @@ import {
 } from 'sequelize-typescript'
 import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
 import {
-  Abuse,
   AbuseFilter,
   AbuseObject,
   AbusePredefinedReasons,
@@ -26,11 +25,14 @@ import {
   AbusePredefinedReasonsString,
   AbuseState,
   AbuseVideoIs,
-  VideoAbuse,
-  VideoCommentAbuse
+  AdminVideoAbuse,
+  AdminAbuse,
+  AdminVideoCommentAbuse,
+  UserAbuse,
+  UserVideoAbuse
 } from '@shared/models'
 import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
-import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
+import { MAbuse, MAbuseAdminFormattable, MAbuseAP, MUserAccountId, MAbuseUserFormattable } from '../../types/models'
 import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
 import { getSort, throwIfNotValid } from '../utils'
 import { ThumbnailModel } from '../video/thumbnail'
@@ -51,6 +53,16 @@ export enum ScopeNames {
     return {
       attributes: {
         include: [
+          [
+            literal(
+              '(' +
+                'SELECT count(*) ' +
+                'FROM "abuseMessage" ' +
+                'WHERE "abuseId" = "AbuseModel"."id"' +
+              ')'
+            ),
+            'countMessages'
+          ],
           [
             // we don't care about this count for deleted videos, so there are not included
             literal(
@@ -285,7 +297,7 @@ export class AbuseModel extends Model<AbuseModel> {
     return AbuseModel.findOne(query)
   }
 
-  static async listForApi (parameters: {
+  static async listForAdminApi (parameters: {
     start: number
     count: number
     sort: string
@@ -353,71 +365,98 @@ export class AbuseModel extends Model<AbuseModel> {
     return { total, data }
   }
 
-  toFormattedJSON (this: MAbuseFormattable): Abuse {
-    const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+  static async listForUserApi (parameters: {
+    user: MUserAccountId
 
-    const countReportsForVideo = this.get('countReportsForVideo') as number
-    const nthReportForVideo = this.get('nthReportForVideo') as number
+    start: number
+    count: number
+    sort: string
 
-    const countReportsForReporter = this.get('countReportsForReporter') as number
-    const countReportsForReportee = this.get('countReportsForReportee') as number
+    id?: number
+    search?: string
+    state?: AbuseState
+  }) {
+    const {
+      start,
+      count,
+      sort,
+      search,
+      user,
+      state,
+      id
+    } = parameters
 
-    let video: VideoAbuse = null
-    let comment: VideoCommentAbuse = null
+    const queryOptions: BuildAbusesQueryOptions = {
+      start,
+      count,
+      sort,
+      id,
+      search,
+      state,
+      reporterAccountId: user.Account.id
+    }
+
+    const [ total, data ] = await Promise.all([
+      AbuseModel.internalCountForApi(queryOptions),
+      AbuseModel.internalListForApi(queryOptions)
+    ])
+
+    return { total, data }
+  }
 
-    if (this.VideoAbuse) {
-      const abuseModel = this.VideoAbuse
-      const entity = abuseModel.Video || abuseModel.deletedVideo
+  buildBaseVideoCommentAbuse (this: MAbuseUserFormattable) {
+    if (!this.VideoCommentAbuse) return null
 
-      video = {
-        id: entity.id,
-        uuid: entity.uuid,
-        name: entity.name,
-        nsfw: entity.nsfw,
+    const abuseModel = this.VideoCommentAbuse
+    const entity = abuseModel.VideoComment
 
-        startAt: abuseModel.startAt,
-        endAt: abuseModel.endAt,
+    return {
+      id: entity.id,
+      threadId: entity.getThreadId(),
 
-        deleted: !abuseModel.Video,
-        blacklisted: abuseModel.Video?.isBlacklisted() || false,
-        thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
+      text: entity.text ?? '',
 
-        channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel,
+      deleted: entity.isDeleted(),
 
-        countReports: countReportsForVideo,
-        nthReport: nthReportForVideo
+      video: {
+        id: entity.Video.id,
+        name: entity.Video.name,
+        uuid: entity.Video.uuid
       }
     }
+  }
 
-    if (this.VideoCommentAbuse) {
-      const abuseModel = this.VideoCommentAbuse
-      const entity = abuseModel.VideoComment
+  buildBaseVideoAbuse (this: MAbuseUserFormattable): UserVideoAbuse {
+    if (!this.VideoAbuse) return null
 
-      comment = {
-        id: entity.id,
-        threadId: entity.getThreadId(),
+    const abuseModel = this.VideoAbuse
+    const entity = abuseModel.Video || abuseModel.deletedVideo
 
-        text: entity.text ?? '',
+    return {
+      id: entity.id,
+      uuid: entity.uuid,
+      name: entity.name,
+      nsfw: entity.nsfw,
 
-        deleted: entity.isDeleted(),
+      startAt: abuseModel.startAt,
+      endAt: abuseModel.endAt,
 
-        video: {
-          id: entity.Video.id,
-          name: entity.Video.name,
-          uuid: entity.Video.uuid
-        }
-      }
+      deleted: !abuseModel.Video,
+      blacklisted: abuseModel.Video?.isBlacklisted() || false,
+      thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
+
+      channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel,
     }
+  }
+
+  buildBaseAbuse (this: MAbuseUserFormattable, countMessages: number): UserAbuse {
+    const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
 
     return {
       id: this.id,
       reason: this.reason,
       predefinedReasons,
 
-      reporterAccount: this.ReporterAccount
-        ? this.ReporterAccount.toFormattedJSON()
-        : null,
-
       flaggedAccount: this.FlaggedAccount
         ? this.FlaggedAccount.toFormattedJSON()
         : null,
@@ -429,11 +468,41 @@ export class AbuseModel extends Model<AbuseModel> {
 
       moderationComment: this.moderationComment,
 
+      countMessages,
+
+      createdAt: this.createdAt,
+      updatedAt: this.updatedAt
+    }
+  }
+
+  toFormattedAdminJSON (this: MAbuseAdminFormattable): AdminAbuse {
+    const countReportsForVideo = this.get('countReportsForVideo') as number
+    const nthReportForVideo = this.get('nthReportForVideo') as number
+
+    const countReportsForReporter = this.get('countReportsForReporter') as number
+    const countReportsForReportee = this.get('countReportsForReportee') as number
+
+    const countMessages = this.get('countMessages') as number
+
+    const baseVideo = this.buildBaseVideoAbuse()
+    const video: AdminVideoAbuse = baseVideo
+      ? Object.assign(baseVideo, {
+        countReports: countReportsForVideo,
+        nthReport: nthReportForVideo
+      })
+      : null
+
+    const comment: AdminVideoCommentAbuse = this.buildBaseVideoCommentAbuse()
+
+    const abuse = this.buildBaseAbuse(countMessages || 0)
+
+    return Object.assign(abuse, {
       video,
       comment,
 
-      createdAt: this.createdAt,
-      updatedAt: this.updatedAt,
+      reporterAccount: this.ReporterAccount
+        ? this.ReporterAccount.toFormattedJSON()
+        : null,
 
       countReportsForReporter: (countReportsForReporter || 0),
       countReportsForReportee: (countReportsForReportee || 0),
@@ -443,7 +512,20 @@ export class AbuseModel extends Model<AbuseModel> {
       endAt: null,
       count: countReportsForVideo || 0,
       nth: nthReportForVideo || 0
-    }
+    })
+  }
+
+  toFormattedUserJSON (this: MAbuseUserFormattable): UserAbuse {
+    const countMessages = this.get('countMessages') as number
+
+    const video = this.buildBaseVideoAbuse()
+    const comment: AdminVideoCommentAbuse = this.buildBaseVideoCommentAbuse()
+    const abuse = this.buildBaseAbuse(countMessages || 0)
+
+    return Object.assign(abuse, {
+      video,
+      comment
+    })
   }
 
   toActivityPubObject (this: MAbuseAP): AbuseObject {
index 8964c0ab2aa4bc0e53b052a10b20d212fa4d2a4a..5e1d66c254a32950b90712a2106009fa14afa08e 100644 (file)
@@ -13,7 +13,11 @@ import {
   setAccessTokensToServers,
   updateAbuse,
   uploadVideo,
-  userLogin
+  userLogin,
+  generateUserAccessToken,
+  addAbuseMessage,
+  listAbuseMessages,
+  deleteAbuseMessage
 } from '../../../../shared/extra-utils'
 import {
   checkBadCountPagination,
@@ -26,7 +30,9 @@ describe('Test abuses API validators', function () {
 
   let server: ServerInfo
   let userAccessToken = ''
+  let userAccessToken2 = ''
   let abuseId: number
+  let messageId: number
 
   // ---------------------------------------------------------------
 
@@ -42,11 +48,15 @@ describe('Test abuses API validators', function () {
     await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
     userAccessToken = await userLogin(server, { username, password })
 
+    {
+      userAccessToken2 = await generateUserAccessToken(server, 'user_2')
+    }
+
     const res = await uploadVideo(server.url, server.accessToken, {})
     server.video = res.body.video
   })
 
-  describe('When listing abuses', function () {
+  describe('When listing abuses for admins', function () {
     const path = basePath
 
     it('Should fail with a bad start pagination', async function () {
@@ -113,47 +123,89 @@ describe('Test abuses API validators', function () {
     })
   })
 
+  describe('When listing abuses for users', function () {
+    const path = '/api/v1/users/me/abuses'
+
+    it('Should fail with a bad start pagination', async function () {
+      await checkBadStartPagination(server.url, path, userAccessToken)
+    })
+
+    it('Should fail with a bad count pagination', async function () {
+      await checkBadCountPagination(server.url, path, userAccessToken)
+    })
+
+    it('Should fail with an incorrect sort', async function () {
+      await checkBadSortPagination(server.url, path, userAccessToken)
+    })
+
+    it('Should fail with a non authenticated user', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path,
+        statusCodeExpected: 401
+      })
+    })
+
+    it('Should fail with a bad id filter', async function () {
+      await makeGetRequest({ url: server.url, path, token: userAccessToken, query: { id: 'toto' } })
+    })
+
+    it('Should fail with a bad state filter', async function () {
+      await makeGetRequest({ url: server.url, path, token: userAccessToken, query: { state: 'toto' } })
+      await makeGetRequest({ url: server.url, path, token: userAccessToken, query: { state: 0 } })
+    })
+
+    it('Should succeed with the correct params', async function () {
+      const query = {
+        id: 13,
+        state: 2
+      }
+
+      await makeGetRequest({ url: server.url, path, token: userAccessToken, query, statusCodeExpected: 200 })
+    })
+  })
+
   describe('When reporting an abuse', function () {
     const path = basePath
 
     it('Should fail with nothing', async function () {
       const fields = {}
-      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
     })
 
     it('Should fail with a wrong video', async function () {
       const fields = { video: { id: 'blabla' }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields })
     })
 
     it('Should fail with an unknown video', async function () {
       const fields = { video: { id: 42 }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
+      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields, statusCodeExpected: 404 })
     })
 
     it('Should fail with a wrong comment', async function () {
       const fields = { comment: { id: 'blabla' }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields })
     })
 
     it('Should fail with an unknown comment', async function () {
       const fields = { comment: { id: 42 }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
+      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields, statusCodeExpected: 404 })
     })
 
     it('Should fail with a wrong account', async function () {
       const fields = { account: { id: 'blabla' }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields })
     })
 
     it('Should fail with an unknown account', async function () {
       const fields = { account: { id: 42 }, reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
+      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields, statusCodeExpected: 404 })
     })
 
     it('Should fail with not account, comment or video', async function () {
       const fields = { reason: 'my super reason' }
-      await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 400 })
+      await makePostBodyRequest({ url: server.url, path: path, token: userAccessToken, fields, statusCodeExpected: 400 })
     })
 
     it('Should fail with a non authenticated user', async function () {
@@ -165,38 +217,38 @@ describe('Test abuses API validators', function () {
     it('Should fail with a reason too short', async function () {
       const fields = { video: { id: server.video.id }, reason: 'h' }
 
-      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
     })
 
     it('Should fail with a too big reason', async function () {
       const fields = { video: { id: server.video.id }, reason: 'super'.repeat(605) }
 
-      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
     })
 
     it('Should succeed with the correct parameters (basic)', async function () {
       const fields: AbuseCreate = { video: { id: server.video.id }, reason: 'my super reason' }
 
-      const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
+      const res = await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: 200 })
       abuseId = res.body.abuse.id
     })
 
     it('Should fail with a wrong predefined reason', async function () {
       const fields = { video: { id: server.video.id }, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
 
-      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
     })
 
     it('Should fail with negative timestamps', async function () {
       const fields = { video: { id: server.video.id, startAt: -1 }, reason: 'my super reason' }
 
-      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
     })
 
     it('Should fail mith misordered startAt/endAt', async function () {
       const fields = { video: { id: server.video.id, startAt: 5, endAt: 1 }, reason: 'my super reason' }
 
-      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
     })
 
     it('Should succeed with the corret parameters (advanced)', async function () {
@@ -210,7 +262,7 @@ describe('Test abuses API validators', function () {
         predefinedReasons: [ 'serverRules' ]
       }
 
-      await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
+      await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, statusCodeExpected: 200 })
     })
   })
 
@@ -244,6 +296,73 @@ describe('Test abuses API validators', function () {
     })
   })
 
+  describe('When creating an abuse message', function () {
+    const message = 'my super message'
+
+    it('Should fail with an invalid abuse id', async function () {
+      await addAbuseMessage(server.url, userAccessToken2, 888, message, 404)
+    })
+
+    it('Should fail with a non authenticated user', async function () {
+      await addAbuseMessage(server.url, 'fake_token', abuseId, message, 401)
+    })
+
+    it('Should fail with an invalid logged in user', async function () {
+      await addAbuseMessage(server.url, userAccessToken2, abuseId, message, 403)
+    })
+
+    it('Should fail with an invalid message', async function () {
+      await addAbuseMessage(server.url, userAccessToken, abuseId, 'a'.repeat(5000), 400)
+    })
+
+    it('Should suceed with the correct params', async function () {
+      const res = await addAbuseMessage(server.url, userAccessToken, abuseId, message)
+      messageId = res.body.abuseMessage.id
+    })
+  })
+
+  describe('When listing abuse message', function () {
+
+    it('Should fail with an invalid abuse id', async function () {
+      await listAbuseMessages(server.url, userAccessToken, 888, 404)
+    })
+
+    it('Should fail with a non authenticated user', async function () {
+      await listAbuseMessages(server.url, 'fake_token', abuseId, 401)
+    })
+
+    it('Should fail with an invalid logged in user', async function () {
+      await listAbuseMessages(server.url, userAccessToken2, abuseId, 403)
+    })
+
+    it('Should succeed with the correct params', async function () {
+      await listAbuseMessages(server.url, userAccessToken, abuseId)
+    })
+  })
+
+  describe('When deleting an abuse message', function () {
+
+    it('Should fail with an invalid abuse id', async function () {
+      await deleteAbuseMessage(server.url, userAccessToken, 888, messageId, 404)
+    })
+
+    it('Should fail with an invalid message id', async function () {
+      await deleteAbuseMessage(server.url, userAccessToken, abuseId, 888, 404)
+    })
+
+    it('Should fail with a non authenticated user', async function () {
+      await deleteAbuseMessage(server.url, 'fake_token', abuseId, messageId, 401)
+    })
+
+    it('Should fail with an invalid logged in user', async function () {
+      await deleteAbuseMessage(server.url, userAccessToken2, abuseId, messageId, 403)
+    })
+
+    it('Should succeed with the correct params', async function () {
+      await deleteAbuseMessage(server.url, userAccessToken, abuseId, messageId)
+    })
+  })
+
   describe('When deleting a video abuse', function () {
 
     it('Should fail with a non authenticated user', async function () {
index f186f7ea0b63b0a95c68073811ec87eb138aaee6..601125fdff45f33d6cd3c5e522fdbc57e58e982d 100644 (file)
@@ -2,7 +2,7 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { Abuse, AbuseFilter, AbusePredefinedReasonsString, AbuseState, VideoComment, Account } from '@shared/models'
+import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, Account, AdminAbuse, UserAbuse, VideoComment, AbuseMessage } from '@shared/models'
 import {
   addVideoCommentThread,
   cleanupTests,
@@ -10,11 +10,15 @@ import {
   deleteAbuse,
   deleteVideoComment,
   flushAndRunMultipleServers,
-  getAbusesList,
+  generateUserAccessToken,
+  getAccount,
+  getAdminAbusesList,
+  getUserAbusesList,
   getVideoCommentThreads,
   getVideoIdFromUUID,
   getVideosList,
   immutableAssign,
+  removeUser,
   removeVideo,
   reportAbuse,
   ServerInfo,
@@ -23,9 +27,9 @@ import {
   uploadVideo,
   uploadVideoAndGetId,
   userLogin,
-  getAccount,
-  removeUser,
-  generateUserAccessToken
+  addAbuseMessage,
+  listAbuseMessages,
+  deleteAbuseMessage
 } from '../../../../shared/extra-utils/index'
 import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
 import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
@@ -40,8 +44,8 @@ const expect = chai.expect
 
 describe('Test abuses', function () {
   let servers: ServerInfo[] = []
-  let abuseServer1: Abuse
-  let abuseServer2: Abuse
+  let abuseServer1: AdminAbuse
+  let abuseServer2: AdminAbuse
 
   before(async function () {
     this.timeout(50000)
@@ -87,7 +91,7 @@ describe('Test abuses', function () {
     })
 
     it('Should not have abuses', async function () {
-      const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
 
       expect(res.body.total).to.equal(0)
       expect(res.body.data).to.be.an('array')
@@ -105,13 +109,13 @@ describe('Test abuses', function () {
     })
 
     it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
-      const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+      const res1 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
 
       expect(res1.body.total).to.equal(1)
       expect(res1.body.data).to.be.an('array')
       expect(res1.body.data.length).to.equal(1)
 
-      const abuse: Abuse = res1.body.data[0]
+      const abuse: AdminAbuse = res1.body.data[0]
       expect(abuse.reason).to.equal('my super bad reason')
 
       expect(abuse.reporterAccount.name).to.equal('root')
@@ -131,7 +135,7 @@ describe('Test abuses', function () {
       expect(abuse.countReportsForReporter).to.equal(1)
       expect(abuse.countReportsForReportee).to.equal(1)
 
-      const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+      const res2 = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken })
       expect(res2.body.total).to.equal(0)
       expect(res2.body.data).to.be.an('array')
       expect(res2.body.data.length).to.equal(0)
@@ -141,19 +145,20 @@ describe('Test abuses', function () {
       this.timeout(10000)
 
       const reason = 'my super bad reason 2'
-      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[1].video.id, reason })
+      const videoId = await getVideoIdFromUUID(servers[0].url, servers[1].video.uuid)
+      await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId, reason })
 
       // We wait requests propagation
       await waitJobs(servers)
     })
 
     it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
-      const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+      const res1 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
 
       expect(res1.body.total).to.equal(2)
       expect(res1.body.data.length).to.equal(2)
 
-      const abuse1: Abuse = res1.body.data[0]
+      const abuse1: AdminAbuse = res1.body.data[0]
       expect(abuse1.reason).to.equal('my super bad reason')
       expect(abuse1.reporterAccount.name).to.equal('root')
       expect(abuse1.reporterAccount.host).to.equal(servers[0].host)
@@ -171,7 +176,7 @@ describe('Test abuses', function () {
       expect(abuse1.state.label).to.equal('Pending')
       expect(abuse1.moderationComment).to.be.null
 
-      const abuse2: Abuse = res1.body.data[1]
+      const abuse2: AdminAbuse = res1.body.data[1]
       expect(abuse2.reason).to.equal('my super bad reason 2')
 
       expect(abuse2.reporterAccount.name).to.equal('root')
@@ -188,7 +193,7 @@ describe('Test abuses', function () {
       expect(abuse2.state.label).to.equal('Pending')
       expect(abuse2.moderationComment).to.be.null
 
-      const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+      const res2 = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken })
       expect(res2.body.total).to.equal(1)
       expect(res2.body.data.length).to.equal(1)
 
@@ -213,7 +218,7 @@ describe('Test abuses', function () {
         await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'will mute this' })
         await waitJobs(servers)
 
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
         expect(res.body.total).to.equal(3)
       }
 
@@ -222,7 +227,7 @@ describe('Test abuses', function () {
       {
         await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
 
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
         expect(res.body.total).to.equal(2)
 
         const abuse = res.body.data.find(a => a.reason === 'will mute this')
@@ -232,7 +237,7 @@ describe('Test abuses', function () {
       {
         await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
 
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
         expect(res.body.total).to.equal(3)
       }
     })
@@ -243,7 +248,7 @@ describe('Test abuses', function () {
       {
         await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
 
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
         expect(res.body.total).to.equal(2)
 
         const abuse = res.body.data.find(a => a.reason === 'will mute this')
@@ -253,7 +258,7 @@ describe('Test abuses', function () {
       {
         await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock)
 
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
         expect(res.body.total).to.equal(3)
       }
     })
@@ -265,11 +270,11 @@ describe('Test abuses', function () {
 
       await waitJobs(servers)
 
-      const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+      const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken })
       expect(res.body.total).to.equal(2, "wrong number of videos returned")
       expect(res.body.data).to.have.lengthOf(2, "wrong number of videos returned")
 
-      const abuse: Abuse = res.body.data[0]
+      const abuse: AdminAbuse = res.body.data[0]
       expect(abuse.id).to.equal(abuseServer2.id, "wrong origin server id for first video")
       expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
       expect(abuse.video.channel).to.exist
@@ -303,8 +308,8 @@ describe('Test abuses', function () {
       await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: servers[0].video.id, reason: reason4 })
 
       {
-        const res2 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
-        const abuses = res2.body.data as Abuse[]
+        const res2 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+        const abuses = res2.body.data as AdminAbuse[]
 
         const abuseVideo3 = res2.body.data.find(a => a.video.id === video3.id)
         expect(abuseVideo3).to.not.be.undefined
@@ -333,10 +338,10 @@ describe('Test abuses', function () {
         endAt: 5
       })).body.abuse
 
-      const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
 
       {
-        const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id)
+        const abuse = (res.body.data as AdminAbuse[]).find(a => a.id === createdAbuse.id)
         expect(abuse.reason).to.equals(reason5)
         expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
         expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
@@ -352,14 +357,14 @@ describe('Test abuses', function () {
       await waitJobs(servers)
 
       {
-        const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
+        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken })
         expect(res.body.total).to.equal(1)
         expect(res.body.data.length).to.equal(1)
         expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
       }
 
       {
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken })
         expect(res.body.total).to.equal(6)
       }
     })
@@ -367,7 +372,7 @@ describe('Test abuses', function () {
     it('Should list and filter video abuses', async function () {
       this.timeout(10000)
 
-      async function list (query: Omit<Parameters<typeof getAbusesList>[0], 'url' | 'token'>) {
+      async function list (query: Omit<Parameters<typeof getAdminAbusesList>[0], 'url' | 'token'>) {
         const options = {
           url: servers[0].url,
           token: servers[0].accessToken
@@ -375,9 +380,9 @@ describe('Test abuses', function () {
 
         Object.assign(options, query)
 
-        const res = await getAbusesList(options)
+        const res = await getAdminAbusesList(options)
 
-        return res.body.data as Abuse[]
+        return res.body.data as AdminAbuse[]
       }
 
       expect(await list({ id: 56 })).to.have.lengthOf(0)
@@ -446,12 +451,12 @@ describe('Test abuses', function () {
     it('Should have 1 comment abuse on server 1 and 0 on server 2', async function () {
       {
         const comment = await getComment(servers[0].url, servers[0].video.id)
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
 
         expect(res.body.total).to.equal(1)
         expect(res.body.data).to.have.lengthOf(1)
 
-        const abuse: Abuse = res.body.data[0]
+        const abuse: AdminAbuse = res.body.data[0]
         expect(abuse.reason).to.equal('it is a bad comment')
 
         expect(abuse.reporterAccount.name).to.equal('root')
@@ -471,7 +476,7 @@ describe('Test abuses', function () {
       }
 
       {
-        const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
+        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
         expect(res.body.total).to.equal(0)
         expect(res.body.data.length).to.equal(0)
       }
@@ -491,16 +496,16 @@ describe('Test abuses', function () {
     it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
       const commentServer2 = await getComment(servers[0].url, servers[1].video.id)
 
-      const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
+      const res1 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
       expect(res1.body.total).to.equal(2)
       expect(res1.body.data.length).to.equal(2)
 
-      const abuse: Abuse = res1.body.data[0]
+      const abuse: AdminAbuse = res1.body.data[0]
       expect(abuse.reason).to.equal('it is a bad comment')
       expect(abuse.countReportsForReporter).to.equal(6)
       expect(abuse.countReportsForReportee).to.equal(5)
 
-      const abuse2: Abuse = res1.body.data[1]
+      const abuse2: AdminAbuse = res1.body.data[1]
 
       expect(abuse2.reason).to.equal('it is a really bad comment')
 
@@ -523,7 +528,7 @@ describe('Test abuses', function () {
       expect(abuse2.countReportsForReporter).to.equal(6)
       expect(abuse2.countReportsForReportee).to.equal(2)
 
-      const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
+      const res2 = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
       expect(res2.body.total).to.equal(1)
       expect(res2.body.data.length).to.equal(1)
 
@@ -550,11 +555,11 @@ describe('Test abuses', function () {
 
       await waitJobs(servers)
 
-      const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
+      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
       expect(res.body.total).to.equal(2)
       expect(res.body.data).to.have.lengthOf(2)
 
-      const abuse = (res.body.data as Abuse[]).find(a => a.comment?.id === commentServer2.id)
+      const abuse = (res.body.data as AdminAbuse[]).find(a => a.comment?.id === commentServer2.id)
       expect(abuse).to.not.be.undefined
 
       expect(abuse.comment.text).to.be.empty
@@ -570,36 +575,46 @@ describe('Test abuses', function () {
       await waitJobs(servers)
 
       {
-        const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
+        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
         expect(res.body.total).to.equal(0)
         expect(res.body.data.length).to.equal(0)
       }
 
       {
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
         expect(res.body.total).to.equal(2)
       }
     })
 
     it('Should list and filter video abuses', async function () {
       {
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment', searchReportee: 'foo' })
+        const res = await getAdminAbusesList({
+          url: servers[0].url,
+          token: servers[0].accessToken,
+          filter: 'comment',
+          searchReportee: 'foo'
+        })
         expect(res.body.total).to.equal(0)
       }
 
       {
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment', searchReportee: 'ot' })
+        const res = await getAdminAbusesList({
+          url: servers[0].url,
+          token: servers[0].accessToken,
+          filter: 'comment',
+          searchReportee: 'ot'
+        })
         expect(res.body.total).to.equal(2)
       }
 
       {
         const baseParams = { url: servers[0].url, token: servers[0].accessToken, filter: 'comment' as AbuseFilter, start: 1, count: 1 }
 
-        const res1 = await getAbusesList(immutableAssign(baseParams, { sort: 'createdAt' }))
+        const res1 = await getAdminAbusesList(immutableAssign(baseParams, { sort: 'createdAt' }))
         expect(res1.body.data).to.have.lengthOf(1)
         expect(res1.body.data[0].comment.text).to.be.empty
 
-        const res2 = await getAbusesList(immutableAssign(baseParams, { sort: '-createdAt' }))
+        const res2 = await getAdminAbusesList(immutableAssign(baseParams, { sort: '-createdAt' }))
         expect(res2.body.data).to.have.lengthOf(1)
         expect(res2.body.data[0].comment.text).to.equal('comment server 1')
       }
@@ -638,12 +653,12 @@ describe('Test abuses', function () {
 
     it('Should have 1 account abuse on server 1 and 0 on server 2', async function () {
       {
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
 
         expect(res.body.total).to.equal(1)
         expect(res.body.data).to.have.lengthOf(1)
 
-        const abuse: Abuse = res.body.data[0]
+        const abuse: AdminAbuse = res.body.data[0]
         expect(abuse.reason).to.equal('it is a bad account')
 
         expect(abuse.reporterAccount.name).to.equal('root')
@@ -657,7 +672,7 @@ describe('Test abuses', function () {
       }
 
       {
-        const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
+        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
         expect(res.body.total).to.equal(0)
         expect(res.body.data.length).to.equal(0)
       }
@@ -675,14 +690,14 @@ describe('Test abuses', function () {
     })
 
     it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
-      const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
+      const res1 = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
       expect(res1.body.total).to.equal(2)
       expect(res1.body.data.length).to.equal(2)
 
-      const abuse: Abuse = res1.body.data[0]
+      const abuse: AdminAbuse = res1.body.data[0]
       expect(abuse.reason).to.equal('it is a bad account')
 
-      const abuse2: Abuse = res1.body.data[1]
+      const abuse2: AdminAbuse = res1.body.data[1]
       expect(abuse2.reason).to.equal('it is a really bad account')
 
       expect(abuse2.reporterAccount.name).to.equal('root')
@@ -696,7 +711,7 @@ describe('Test abuses', function () {
 
       expect(abuse2.moderationComment).to.be.null
 
-      const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' })
+      const res2 = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' })
       expect(res2.body.total).to.equal(1)
       expect(res2.body.data.length).to.equal(1)
 
@@ -721,11 +736,11 @@ describe('Test abuses', function () {
 
       await waitJobs(servers)
 
-      const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
+      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
       expect(res.body.total).to.equal(2)
       expect(res.body.data).to.have.lengthOf(2)
 
-      const abuse = (res.body.data as Abuse[]).find(a => a.reason === 'it is a really bad account')
+      const abuse = (res.body.data as AdminAbuse[]).find(a => a.reason === 'it is a really bad account')
       expect(abuse).to.not.be.undefined
     })
 
@@ -737,13 +752,13 @@ describe('Test abuses', function () {
       await waitJobs(servers)
 
       {
-        const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' })
+        const res = await getAdminAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' })
         expect(res.body.total).to.equal(0)
         expect(res.body.data.length).to.equal(0)
       }
 
       {
-        const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
+        const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
         expect(res.body.total).to.equal(2)
 
         abuseServer1 = res.body.data[0]
@@ -757,7 +772,7 @@ describe('Test abuses', function () {
       const body = { state: AbuseState.REJECTED }
       await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body)
 
-      const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
+      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
       expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
     })
 
@@ -765,12 +780,184 @@ describe('Test abuses', function () {
       const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
       await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body)
 
-      const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
+      const res = await getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
       expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
       expect(res.body.data[0].moderationComment).to.equal('It is valid')
     })
   })
 
+  describe('My abuses', async function () {
+    let abuseId1: number
+    let userAccessToken: string
+
+    before(async function () {
+      userAccessToken = await generateUserAccessToken(servers[0], 'user_42')
+
+      await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: servers[0].video.id, reason: 'user reason 1' })
+
+      const videoId = await getVideoIdFromUUID(servers[0].url, servers[1].video.uuid)
+      await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId, reason: 'user reason 2' })
+    })
+
+    it('Should correctly list my abuses', async function () {
+      {
+        const res = await getUserAbusesList({ url: servers[0].url, token: userAccessToken, start: 0, count: 5, sort: 'createdAt' })
+        expect(res.body.total).to.equal(2)
+
+        const abuses: UserAbuse[] = res.body.data
+        expect(abuses[0].reason).to.equal('user reason 1')
+        expect(abuses[1].reason).to.equal('user reason 2')
+
+        abuseId1 = abuses[0].id
+      }
+
+      {
+        const res = await getUserAbusesList({ url: servers[0].url, token: userAccessToken, start: 1, count: 1, sort: 'createdAt' })
+        expect(res.body.total).to.equal(2)
+
+        const abuses: UserAbuse[] = res.body.data
+        expect(abuses[0].reason).to.equal('user reason 2')
+      }
+
+      {
+        const res = await getUserAbusesList({ url: servers[0].url, token: userAccessToken, start: 1, count: 1, sort: '-createdAt' })
+        expect(res.body.total).to.equal(2)
+
+        const abuses: UserAbuse[] = res.body.data
+        expect(abuses[0].reason).to.equal('user reason 1')
+      }
+    })
+
+    it('Should correctly filter my abuses by id', async function () {
+      const res = await getUserAbusesList({ url: servers[0].url, token: userAccessToken, id: abuseId1 })
+
+      expect(res.body.total).to.equal(1)
+
+      const abuses: UserAbuse[] = res.body.data
+      expect(abuses[0].reason).to.equal('user reason 1')
+    })
+
+    it('Should correctly filter my abuses by search', async function () {
+      const res = await getUserAbusesList({
+        url: servers[0].url,
+        token: userAccessToken,
+        search: 'server 2'
+      })
+
+      expect(res.body.total).to.equal(1)
+
+      const abuses: UserAbuse[] = res.body.data
+      expect(abuses[0].reason).to.equal('user reason 2')
+    })
+
+    it('Should correctly filter my abuses by state', async function () {
+      const body = { state: AbuseState.REJECTED }
+      await updateAbuse(servers[0].url, servers[0].accessToken, abuseId1, body)
+
+      const res = await getUserAbusesList({
+        url: servers[0].url,
+        token: userAccessToken,
+        state: AbuseState.REJECTED
+      })
+
+      expect(res.body.total).to.equal(1)
+
+      const abuses: UserAbuse[] = res.body.data
+      expect(abuses[0].reason).to.equal('user reason 1')
+    })
+  })
+
+  describe('Abuse messages', async function () {
+    let abuseId: number
+    let userAccessToken: string
+    let abuseMessageUserId: number
+    let abuseMessageModerationId: number
+
+    before(async function () {
+      userAccessToken = await generateUserAccessToken(servers[0], 'user_43')
+
+      const res = await reportAbuse({
+        url: servers[0].url,
+        token: userAccessToken,
+        videoId: servers[0].video.id,
+        reason: 'user 43 reason 1'
+      })
+
+      abuseId = res.body.abuse.id
+    })
+
+    it('Should create some messages on the abuse', async function () {
+      await addAbuseMessage(servers[0].url, userAccessToken, abuseId, 'message 1')
+      await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, 'message 2')
+      await addAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, 'message 3')
+      await addAbuseMessage(servers[0].url, userAccessToken, abuseId, 'message 4')
+    })
+
+    it('Should have the correct messages count when listing abuses', async function () {
+      const results = await Promise.all([
+        getAdminAbusesList({ url: servers[0].url, token: servers[0].accessToken, start: 0, count: 50 }),
+        getUserAbusesList({ url: servers[0].url, token: userAccessToken, start: 0, count: 50 })
+      ])
+
+      for (const res of results) {
+        const abuses: AdminAbuse[] = res.body.data
+        const abuse = abuses.find(a => a.id === abuseId)
+        expect(abuse.countMessages).to.equal(4)
+      }
+    })
+
+    it('Should correctly list messages of this abuse', async function () {
+      const results = await Promise.all([
+        listAbuseMessages(servers[0].url, servers[0].accessToken, abuseId),
+        listAbuseMessages(servers[0].url, userAccessToken, abuseId)
+      ])
+
+      for (const res of results) {
+        expect(res.body.total).to.equal(4)
+
+        const abuseMessages: AbuseMessage[] = res.body.data
+
+        expect(abuseMessages[0].message).to.equal('message 1')
+        expect(abuseMessages[0].byModerator).to.be.false
+        expect(abuseMessages[0].account.name).to.equal('user_43')
+
+        abuseMessageUserId = abuseMessages[0].id
+
+        expect(abuseMessages[1].message).to.equal('message 2')
+        expect(abuseMessages[1].byModerator).to.be.true
+        expect(abuseMessages[1].account.name).to.equal('root')
+
+        expect(abuseMessages[2].message).to.equal('message 3')
+        expect(abuseMessages[2].byModerator).to.be.true
+        expect(abuseMessages[2].account.name).to.equal('root')
+        abuseMessageModerationId = abuseMessages[2].id
+
+        expect(abuseMessages[3].message).to.equal('message 4')
+        expect(abuseMessages[3].byModerator).to.be.false
+        expect(abuseMessages[3].account.name).to.equal('user_43')
+      }
+    })
+
+    it('Should delete messages', async function () {
+      await deleteAbuseMessage(servers[0].url, servers[0].accessToken, abuseId, abuseMessageModerationId)
+      await deleteAbuseMessage(servers[0].url, userAccessToken, abuseId, abuseMessageUserId)
+
+      const results = await Promise.all([
+        listAbuseMessages(servers[0].url, servers[0].accessToken, abuseId),
+        listAbuseMessages(servers[0].url, userAccessToken, abuseId)
+      ])
+
+      for (const res of results) {
+        expect(res.body.total).to.equal(2)
+
+        const abuseMessages: AbuseMessage[] = res.body.data
+
+        expect(abuseMessages[0].message).to.equal('message 2')
+        expect(abuseMessages[1].message).to.equal('message 4')
+      }
+    })
+  })
+
   after(async function () {
     await cleanupTests(servers)
   })
index ea74bde6ad4b1f23feab3e6efeb4f4746a00dfa7..edb0b4bb3d4a8f15669992f363d7c5390227ef33 100644 (file)
@@ -11,8 +11,8 @@ import {
   createUser,
   deleteMe,
   flushAndRunServer,
-  getAbusesList,
   getAccountRatings,
+  getAdminAbusesList,
   getBlacklistedVideosList,
   getCustomConfig,
   getMyUserInformation,
@@ -928,7 +928,7 @@ describe('Test users', function () {
       const reason = 'my super bad reason'
       await reportAbuse({ url: server.url, token: user17AccessToken, videoId, reason })
 
-      const res1 = await getAbusesList({ url: server.url, token: server.accessToken })
+      const res1 = await getAdminAbusesList({ url: server.url, token: server.accessToken })
       const abuseId = res1.body.data[0].id
 
       const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
index baeb543e0b939ec0b8b33948253f49685f8b8d0c..0b6a0e8ae34b7a0193c0c66f606dd3b7b9a40961 100644 (file)
@@ -2,7 +2,7 @@
 
 import 'mocha'
 import * as chai from 'chai'
-import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models'
+import { AbusePredefinedReasonsString, AbuseState, AdminAbuse } from '@shared/models'
 import {
   cleanupTests,
   createUser,
@@ -33,7 +33,7 @@ const expect = chai.expect
 
 describe('Test video abuses', function () {
   let servers: ServerInfo[] = []
-  let abuseServer2: Abuse
+  let abuseServer2: AdminAbuse
 
   before(async function () {
     this.timeout(50000)
@@ -97,7 +97,7 @@ describe('Test video abuses', function () {
     expect(res1.body.data).to.be.an('array')
     expect(res1.body.data.length).to.equal(1)
 
-    const abuse: Abuse = res1.body.data[0]
+    const abuse: AdminAbuse = res1.body.data[0]
     expect(abuse.reason).to.equal('my super bad reason')
     expect(abuse.reporterAccount.name).to.equal('root')
     expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
@@ -130,7 +130,7 @@ describe('Test video abuses', function () {
     expect(res1.body.data).to.be.an('array')
     expect(res1.body.data.length).to.equal(2)
 
-    const abuse1: Abuse = res1.body.data[0]
+    const abuse1: AdminAbuse = res1.body.data[0]
     expect(abuse1.reason).to.equal('my super bad reason')
     expect(abuse1.reporterAccount.name).to.equal('root')
     expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
@@ -141,7 +141,7 @@ describe('Test video abuses', function () {
     expect(abuse1.video.countReports).to.equal(1)
     expect(abuse1.video.nthReport).to.equal(1)
 
-    const abuse2: Abuse = res1.body.data[1]
+    const abuse2: AdminAbuse = 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:' + servers[0].port)
@@ -245,7 +245,7 @@ describe('Test video abuses', function () {
     expect(res.body.data.length).to.equal(2, "wrong number of videos returned")
     expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video")
 
-    const abuse: Abuse = res.body.data[0]
+    const abuse: AdminAbuse = res.body.data[0]
     expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
     expect(abuse.video.channel).to.exist
     expect(abuse.video.deleted).to.be.true
@@ -279,7 +279,7 @@ describe('Test video abuses', function () {
     const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
 
     {
-      for (const abuse of res2.body.data as Abuse[]) {
+      for (const abuse of res2.body.data as AdminAbuse[]) {
         if (abuse.video.id === video3.id) {
           expect(abuse.video.countReports).to.equal(1, "wrong reports count for video 3")
           expect(abuse.video.nthReport).to.equal(1, "wrong report position in report list for video 3")
@@ -311,7 +311,7 @@ describe('Test video abuses', function () {
     const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
 
     {
-      const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id)
+      const abuse = (res.body.data as AdminAbuse[]).find(a => a.id === createdAbuse.id)
       expect(abuse.reason).to.equals(reason5)
       expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
       expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
@@ -350,7 +350,7 @@ describe('Test video abuses', function () {
 
       const res = await getVideoAbusesList(options)
 
-      return res.body.data as Abuse[]
+      return res.body.data as AdminAbuse[]
     }
 
     expect(await list({ id: 56 })).to.have.lengthOf(0)
diff --git a/server/types/models/moderation/abuse-message.ts b/server/types/models/moderation/abuse-message.ts
new file mode 100644 (file)
index 0000000..565eca7
--- /dev/null
@@ -0,0 +1,20 @@
+import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
+import { PickWith } from '@shared/core-utils'
+import { AbuseModel } from '../../../models/abuse/abuse'
+import { MAccountFormattable } from '../account'
+
+type Use<K extends keyof AbuseMessageModel, M> = PickWith<AbuseMessageModel, K, M>
+
+// ############################################################################
+
+export type MAbuseMessage = Omit<AbuseMessageModel, 'Account' | 'Abuse' | 'toFormattedJSON'>
+
+export type MAbuseMessageId = Pick<AbuseModel, 'id'>
+
+// ############################################################################
+
+// Format for API
+
+export type MAbuseMessageFormattable =
+  MAbuseMessage &
+  Use<'Account', MAccountFormattable>
index a0bf4b08f9fde2534821c02c20ece8e81c680fb9..39ef507714ce54a0a120059a3ad5cd7131cb54dd 100644 (file)
@@ -95,9 +95,15 @@ export type MAbuseFull =
 
 // Format for API or AP object
 
-export type MAbuseFormattable =
+export type MAbuseAdminFormattable =
   MAbuse &
   Use<'ReporterAccount', MAccountFormattable> &
   Use<'FlaggedAccount', MAccountFormattable> &
   Use<'VideoAbuse', MVideoAbuseFormattable> &
   Use<'VideoCommentAbuse', MCommentAbuseFormattable>
+
+export type MAbuseUserFormattable =
+  MAbuse &
+  Use<'FlaggedAccount', MAccountFormattable> &
+  Use<'VideoAbuse', MVideoAbuseFormattable> &
+  Use<'VideoCommentAbuse', MCommentAbuseFormattable>
index 8bea1708f17cf957c9e537063b0170808ef4992a..1ed91b2491fe8c753160a871ef21c3f7cd33e035 100644 (file)
@@ -1 +1,2 @@
 export * from './abuse'
+export * from './abuse-message'
index 7595e6d86c22ceaafd3bef731f3fd74e1dcc7788..452c6e1a027fe72c0de490127c3292384ae58e99 100644 (file)
@@ -1,6 +1,7 @@
 import { RegisterServerAuthExternalOptions } from '@server/types'
 import {
   MAbuse,
+  MAbuseMessage,
   MAccountBlocklist,
   MActorUrl,
   MStreamingPlaylist,
@@ -78,6 +79,7 @@ declare module 'express' {
       videoCaption?: MVideoCaptionVideo
 
       abuse?: MAbuse
+      abuseMessage?: MAbuseMessage
 
       videoStreamingPlaylist?: MStreamingPlaylist
 
index 62af9556e67a789e888592980b6fa5964156e05b..7db75cebb793f27173fb4429fb826b71b64de62b 100644 (file)
@@ -54,7 +54,7 @@ function reportAbuse (options: {
   })
 }
 
-function getAbusesList (options: {
+function getAdminAbusesList (options: {
   url: string
   token: string
 
@@ -117,6 +117,48 @@ function getAbusesList (options: {
   })
 }
 
+function getUserAbusesList (options: {
+  url: string
+  token: string
+
+  start?: number
+  count?: number
+  sort?: string
+
+  id?: number
+  search?: string
+  state?: AbuseState
+}) {
+  const {
+    url,
+    token,
+    start,
+    count,
+    sort,
+    id,
+    search,
+    state
+  } = options
+  const path = '/api/v1/users/me/abuses'
+
+  const query = {
+    id,
+    search,
+    state,
+    start,
+    count,
+    sort: sort || 'createdAt'
+  }
+
+  return makeGetRequest({
+    url,
+    path,
+    token,
+    query,
+    statusCodeExpected: 200
+  })
+}
+
 function updateAbuse (
   url: string,
   token: string,
@@ -146,11 +188,49 @@ function deleteAbuse (url: string, token: string, abuseId: number, statusCodeExp
   })
 }
 
+function listAbuseMessages (url: string, token: string, abuseId: number, statusCodeExpected = 200) {
+  const path = '/api/v1/abuses/' + abuseId + '/messages'
+
+  return makeGetRequest({
+    url,
+    token,
+    path,
+    statusCodeExpected
+  })
+}
+
+function deleteAbuseMessage (url: string, token: string, abuseId: number, messageId: number, statusCodeExpected = 204) {
+  const path = '/api/v1/abuses/' + abuseId + '/messages/' + messageId
+
+  return makeDeleteRequest({
+    url,
+    token,
+    path,
+    statusCodeExpected
+  })
+}
+
+function addAbuseMessage (url: string, token: string, abuseId: number, message: string, statusCodeExpected = 200) {
+  const path = '/api/v1/abuses/' + abuseId + '/messages'
+
+  return makePostBodyRequest({
+    url,
+    token,
+    path,
+    fields: { message },
+    statusCodeExpected
+  })
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   reportAbuse,
-  getAbusesList,
+  getAdminAbusesList,
   updateAbuse,
-  deleteAbuse
+  deleteAbuse,
+  getUserAbusesList,
+  listAbuseMessages,
+  deleteAbuseMessage,
+  addAbuseMessage
 }
diff --git a/shared/models/moderation/abuse/abuse-message.model.ts b/shared/models/moderation/abuse/abuse-message.model.ts
new file mode 100644 (file)
index 0000000..02072d5
--- /dev/null
@@ -0,0 +1,9 @@
+import { AccountSummary } from '@shared/models'
+
+export interface AbuseMessage {
+  id: number
+  message: string
+  byModerator: boolean
+
+  account: AccountSummary
+}
index 0a0c6bd3583751beffafd3c586bb90761446f40c..7f126ba4a0a15bb4a6100ab6aefb82ef5c63a995 100644 (file)
@@ -4,7 +4,7 @@ import { AbusePredefinedReasonsString } from './abuse-reason.model'
 import { VideoConstant } from '../../videos/video-constant.model'
 import { VideoChannel } from '../../videos/channel/video-channel.model'
 
-export interface VideoAbuse {
+export interface AdminVideoAbuse {
   id: number
   name: string
   uuid: string
@@ -23,7 +23,7 @@ export interface VideoAbuse {
   nthReport: number
 }
 
-export interface VideoCommentAbuse {
+export interface AdminVideoCommentAbuse {
   id: number
   threadId: number
 
@@ -38,7 +38,7 @@ export interface VideoCommentAbuse {
   deleted: boolean
 }
 
-export interface Abuse {
+export interface AdminAbuse {
   id: number
 
   reason: string
@@ -50,8 +50,8 @@ export interface Abuse {
   state: VideoConstant<AbuseState>
   moderationComment?: string
 
-  video?: VideoAbuse
-  comment?: VideoCommentAbuse
+  video?: AdminVideoAbuse
+  comment?: AdminVideoCommentAbuse
 
   createdAt: Date
   updatedAt: Date
@@ -59,6 +59,8 @@ export interface Abuse {
   countReportsForReporter?: number
   countReportsForReportee?: number
 
+  countMessages: number
+
   // FIXME: deprecated in 2.3, remove the following properties
 
   // @deprecated
@@ -71,3 +73,10 @@ export interface Abuse {
   // @deprecated
   nth?: number
 }
+
+export type UserVideoAbuse = Omit<AdminVideoAbuse, 'countReports' | 'nthReport'>
+
+export type UserVideoCommentAbuse = AdminVideoCommentAbuse
+
+export type UserAbuse = Omit<AdminAbuse, 'reporterAccount' | 'countReportsForReportee' | 'countReportsForReporter' | 'startAt' | 'endAt'
+| 'count' | 'nth'>
index 55046426ab3ffaa3547f2b4863a5bedb4aef20d9..b518517a6f305052da356849dc8e0597ad8e96b7 100644 (file)
@@ -1,5 +1,6 @@
 export * from './abuse-create.model'
 export * from './abuse-filter.type'
+export * from './abuse-message.model'
 export * from './abuse-reason.model'
 export * from './abuse-state.model'
 export * from './abuse-update.model'