From edbc9325462ddf4536775871ebc25e06f46612d1 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 24 Jul 2020 15:05:51 +0200 Subject: [PATCH] Add server API to abuse messages --- server/controllers/api/abuse.ts | 81 ++++- server/controllers/api/users/index.ts | 26 +- server/controllers/api/users/my-abuses.ts | 48 +++ server/controllers/api/videos/abuse.ts | 8 +- server/helpers/audit-logger.ts | 4 +- server/helpers/custom-validators/abuses.ts | 8 +- server/initializers/constants.ts | 3 + server/initializers/database.ts | 2 + server/lib/emailer.ts | 4 +- server/lib/moderation.ts | 2 +- server/lib/notifier.ts | 6 +- server/middlewares/user-right.ts | 6 +- server/middlewares/validators/abuse.ts | 105 +++++- server/models/abuse/abuse-message.ts | 103 ++++++ server/models/abuse/abuse-query-builder.ts | 15 +- server/models/abuse/abuse.ts | 182 ++++++++--- server/tests/api/check-params/abuses.ts | 153 ++++++++- server/tests/api/moderation/abuses.ts | 307 ++++++++++++++---- server/tests/api/users/users.ts | 4 +- server/tests/api/videos/video-abuse.ts | 18 +- .../types/models/moderation/abuse-message.ts | 20 ++ server/types/models/moderation/abuse.ts | 8 +- server/types/models/moderation/index.ts | 1 + server/typings/express/index.d.ts | 2 + shared/extra-utils/moderation/abuses.ts | 86 ++++- .../moderation/abuse/abuse-message.model.ts | 9 + shared/models/moderation/abuse/abuse.model.ts | 19 +- shared/models/moderation/abuse/index.ts | 1 + 28 files changed, 1038 insertions(+), 193 deletions(-) create mode 100644 server/controllers/api/users/my-abuses.ts create mode 100644 server/models/abuse/abuse-message.ts create mode 100644 server/types/models/moderation/abuse-message.ts create mode 100644 shared/models/moderation/abuse/abuse-message.model.ts diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts index 04a0c06e3..50d068157 100644 --- a/server/controllers/api/abuse.ts +++ b/server/controllers/api/abuse.ts @@ -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) +} diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 5939f6125..d339c2a1c 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts @@ -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 index 000000000..e43fc483e --- /dev/null +++ b/server/controllers/api/users/my-abuses.ts @@ -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()) + }) +} diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index b92a66360..9c4d00849 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts @@ -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) { diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index 954b0b69d..6aae5e821 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts @@ -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) } } diff --git a/server/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts index 0ca06a252..0ddde4b06 100644 --- a/server/helpers/custom-validators/abuses.ts +++ b/server/helpers/custom-validators/abuses.ts @@ -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, diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index fd5bf5868..a40a22395 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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 }, diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 0775f1fad..8ce32f6fa 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -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, diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 48ba7421e..c6ad03328 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -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 }) { diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index 4fc9cd747..b140d5aa9 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts @@ -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({ diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index c567e1c20..8f165d2fd 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts @@ -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 }) { diff --git a/server/middlewares/user-right.ts b/server/middlewares/user-right.ts index 4da7b9802..4d836485c 100644 --- a/server/middlewares/user-right.ts +++ b/server/middlewares/user-right.ts @@ -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() diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts index 966d1f7fb..cb0bc658a 100644 --- a/server/middlewares/validators/abuse.ts +++ b/server/middlewares/validators/abuse.ts @@ -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 index 000000000..f7721c87d --- /dev/null +++ b/server/models/abuse/abuse-message.ts @@ -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 { + + @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 { + 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 + } + } +} diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts index 5fddcf3c4..9d7cb75aa 100644 --- a/server/models/abuse/abuse-query-builder.ts +++ b/server/models/abuse/abuse-query-builder.ts @@ -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 = [ diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index bd96cf79c..7002502d5 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts @@ -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 { 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 { 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 { 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 { 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 { diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts index 8964c0ab2..5e1d66c25 100644 --- a/server/tests/api/check-params/abuses.ts +++ b/server/tests/api/check-params/abuses.ts @@ -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 () { diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts index f186f7ea0..601125fdf 100644 --- a/server/tests/api/moderation/abuses.ts +++ b/server/tests/api/moderation/abuses.ts @@ -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[0], 'url' | 'token'>) { + async function list (query: Omit[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) }) diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index ea74bde6a..edb0b4bb3 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -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) diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts index baeb543e0..0b6a0e8ae 100644 --- a/server/tests/api/videos/video-abuse.ts +++ b/server/tests/api/videos/video-abuse.ts @@ -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 index 000000000..565eca706 --- /dev/null +++ b/server/types/models/moderation/abuse-message.ts @@ -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 = PickWith + +// ############################################################################ + +export type MAbuseMessage = Omit + +export type MAbuseMessageId = Pick + +// ############################################################################ + +// Format for API + +export type MAbuseMessageFormattable = + MAbuseMessage & + Use<'Account', MAccountFormattable> diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts index a0bf4b08f..39ef50771 100644 --- a/server/types/models/moderation/abuse.ts +++ b/server/types/models/moderation/abuse.ts @@ -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> diff --git a/server/types/models/moderation/index.ts b/server/types/models/moderation/index.ts index 8bea1708f..1ed91b249 100644 --- a/server/types/models/moderation/index.ts +++ b/server/types/models/moderation/index.ts @@ -1 +1,2 @@ export * from './abuse' +export * from './abuse-message' diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index 7595e6d86..452c6e1a0 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts @@ -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 diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts index 62af9556e..7db75cebb 100644 --- a/shared/extra-utils/moderation/abuses.ts +++ b/shared/extra-utils/moderation/abuses.ts @@ -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 index 000000000..02072d5ce --- /dev/null +++ b/shared/models/moderation/abuse/abuse-message.model.ts @@ -0,0 +1,9 @@ +import { AccountSummary } from '@shared/models' + +export interface AbuseMessage { + id: number + message: string + byModerator: boolean + + account: AccountSummary +} diff --git a/shared/models/moderation/abuse/abuse.model.ts b/shared/models/moderation/abuse/abuse.model.ts index 0a0c6bd35..7f126ba4a 100644 --- a/shared/models/moderation/abuse/abuse.model.ts +++ b/shared/models/moderation/abuse/abuse.model.ts @@ -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 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 + +export type UserVideoCommentAbuse = AdminVideoCommentAbuse + +export type UserAbuse = Omit diff --git a/shared/models/moderation/abuse/index.ts b/shared/models/moderation/abuse/index.ts index 55046426a..b518517a6 100644 --- a/shared/models/moderation/abuse/index.ts +++ b/shared/models/moderation/abuse/index.ts @@ -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' -- 2.41.0