const abuseRouter = express.Router()
-abuseRouter.get('/abuse',
+abuseRouter.get('/',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ABUSES),
paginationValidator,
abuseListValidator,
asyncMiddleware(listAbuses)
)
-abuseRouter.put('/:videoId/abuse/:id',
+abuseRouter.put('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ABUSES),
asyncMiddleware(abuseUpdateValidator),
asyncRetryTransactionMiddleware(updateAbuse)
)
-abuseRouter.post('/:videoId/abuse',
+abuseRouter.post('/',
authenticate,
asyncMiddleware(abuseReportValidator),
asyncRetryTransactionMiddleware(reportAbuse)
)
-abuseRouter.delete('/:videoId/abuse/:id',
+abuseRouter.delete('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ABUSES),
asyncMiddleware(abuseGetValidator),
count: req.query.count,
sort: req.query.sort,
id: req.query.id,
- filter: 'video',
+ filter: req.query.filter,
predefinedReason: req.query.predefinedReason,
search: req.query.search,
state: req.query.state,
import validator from 'validator'
-import { abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs } from '@shared/models'
-import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
+import { AbuseFilter, abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs, AbuseCreate } from '@shared/models'
+import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
import { exists, isArray } from './misc'
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
return exists(value) && value in abusePredefinedReasonsMap
}
-function isAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) {
+function isAbuseFilterValid (value: AbuseFilter) {
+ return value === 'video' || value === 'comment' || value === 'account'
+}
+
+function areAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) {
return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap)
}
}
function isAbuseTimestampCoherent (endAt: number, { req }) {
- return exists(req.body.startAt) && endAt > req.body.startAt
+ const startAt = (req.body as AbuseCreate).video.startAt
+
+ return exists(startAt) && endAt > startAt
}
function isAbuseModerationCommentValid (value: string) {
export {
isAbuseReasonValid,
+ isAbuseFilterValid,
isAbusePredefinedReasonValid,
- isAbusePredefinedReasonsValid,
+ areAbusePredefinedReasonsValid as isAbusePredefinedReasonsValid,
isAbuseTimestampValid,
isAbuseTimestampCoherent,
isAbuseModerationCommentValid,
-import 'multer'
+import * as express from 'express'
import validator from 'validator'
+import { VideoCommentModel } from '@server/models/video/video-comment'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
+import { MVideoId } from '@server/types/models'
const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS
return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT)
}
+async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
+ const id = parseInt(idArg + '', 10)
+ const videoComment = await VideoCommentModel.loadById(id)
+
+ if (!videoComment) {
+ res.status(404)
+ .json({ error: 'Video comment thread not found' })
+ .end()
+
+ return false
+ }
+
+ if (videoComment.videoId !== video.id) {
+ res.status(400)
+ .json({ error: 'Video comment is not associated to this video.' })
+ .end()
+
+ return false
+ }
+
+ if (videoComment.inReplyToCommentId !== null) {
+ res.status(400)
+ .json({ error: 'Video comment is not a thread.' })
+ .end()
+
+ return false
+ }
+
+ res.locals.videoCommentThread = videoComment
+ return true
+}
+
+async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
+ const id = parseInt(idArg + '', 10)
+ const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
+
+ if (!videoComment) {
+ res.status(404)
+ .json({ error: 'Video comment thread not found' })
+ .end()
+
+ return false
+ }
+
+ if (videoComment.videoId !== video.id) {
+ res.status(400)
+ .json({ error: 'Video comment is not associated to this video.' })
+ .end()
+
+ return false
+ }
+
+ res.locals.videoCommentFull = videoComment
+ return true
+}
+
+async function doesCommentIdExist (idArg: number | string, res: express.Response) {
+ const id = parseInt(idArg + '', 10)
+ const videoComment = await VideoCommentModel.loadById(id)
+
+ if (!videoComment) {
+ res.status(404)
+ .json({ error: 'Video comment thread not found' })
+
+ return false
+ }
+
+ res.locals.videoComment = videoComment
+
+ return true
+}
+
// ---------------------------------------------------------------------------
export {
- isValidVideoCommentText
+ isValidVideoCommentText,
+ doesVideoCommentThreadExist,
+ doesVideoCommentExist,
+ doesCommentIdExist
}
if (abuse === null) {
res.status(404)
.json({ error: 'Video abuse not found' })
- .end()
return false
}
return true
}
-async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
+async function doesAbuseExist (abuseId: number | string, res: Response) {
+ const abuse = await AbuseModel.loadById(parseInt(abuseId + '', 10))
+ if (!abuse) {
+ res.status(404)
+ .json({ error: 'Video abuse not found' })
+
+ return false
+ }
+
+ res.locals.abuse = abuse
+ return true
}
// ---------------------------------------------------------------------------
import * as Bluebird from 'bluebird'
import { MAccountDefault } from '../../types/models'
-function doesAccountIdExist (id: number, res: Response, sendNotFound = true) {
- const promise = AccountModel.load(id)
+function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
+ const promise = AccountModel.load(parseInt(id + '', 10))
return doesAccountExist(promise, res, sendNotFound)
}
import * as express from 'express'
import { body, param, query } from 'express-validator'
import {
+ isAbuseFilterValid,
isAbuseModerationCommentValid,
isAbusePredefinedReasonsValid,
isAbusePredefinedReasonValid,
isAbuseVideoIsValid
} from '@server/helpers/custom-validators/abuses'
import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc'
+import { doesCommentIdExist } from '@server/helpers/custom-validators/video-comments'
import { logger } from '@server/helpers/logger'
-import { doesAbuseExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares'
+import { doesAbuseExist, doesAccountIdExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares'
+import { AbuseCreate } from '@shared/models'
import { areValidationErrors } from './utils'
const abuseReportValidator = [
- param('videoId')
+ body('account.id')
+ .optional()
+ .custom(isIdValid)
+ .withMessage('Should have a valid accountId'),
+
+ body('video.id')
+ .optional()
.custom(isIdOrUUIDValid)
- .not()
- .isEmpty()
.withMessage('Should have a valid videoId'),
- body('reason')
- .custom(isAbuseReasonValid)
- .withMessage('Should have a valid reason'),
- body('predefinedReasons')
- .optional()
- .custom(isAbusePredefinedReasonsValid)
- .withMessage('Should have a valid list of predefined reasons'),
- body('startAt')
+ body('video.startAt')
.optional()
.customSanitizer(toIntOrNull)
.custom(isAbuseTimestampValid)
.withMessage('Should have valid starting time value'),
- body('endAt')
+ body('video.endAt')
.optional()
.customSanitizer(toIntOrNull)
.custom(isAbuseTimestampValid)
.custom(isAbuseTimestampCoherent)
.withMessage('Should have a startAt timestamp beginning before endAt'),
+ body('comment.id')
+ .optional()
+ .custom(isIdValid)
+ .withMessage('Should have a valid commentId'),
+
+ body('reason')
+ .custom(isAbuseReasonValid)
+ .withMessage('Should have a valid reason'),
+
+ body('predefinedReasons')
+ .optional()
+ .custom(isAbusePredefinedReasonsValid)
+ .withMessage('Should have a valid list of predefined reasons'),
+
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking abuseReport parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
- if (!await doesVideoExist(req.params.videoId, res)) return
- // TODO: check comment or video (exlusive)
+ const body: AbuseCreate = req.body
+
+ if (body.video?.id && !await doesVideoExist(body.video.id, res)) return
+ if (body.account?.id && !await doesAccountIdExist(body.account.id, res)) return
+ if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
+
+ if (!body.video?.id && !body.account?.id && !body.comment?.id) {
+ res.status(400)
+ .json({ error: 'video id or account id or comment id is required.' })
+
+ return
+ }
return next()
}
]
const abuseGetValidator = [
- param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking abuseGetValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
- // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return
+ if (!await doesAbuseExist(req.params.id, res)) return
return next()
}
]
const abuseUpdateValidator = [
- param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
+
body('state')
.optional()
- .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
+ .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'),
body('moderationComment')
.optional()
- .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
+ .custom(isAbuseModerationCommentValid).withMessage('Should have a valid moderation comment'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
- // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return
+ if (!await doesAbuseExist(req.params.id, res)) return
return next()
}
query('id')
.optional()
.custom(isIdValid).withMessage('Should have a valid id'),
+ query('filter')
+ .optional()
+ .custom(isAbuseFilterValid)
+ .withMessage('Should have a valid filter'),
query('predefinedReason')
.optional()
.custom(isAbusePredefinedReasonValid)
.optional()
.customSanitizer(toIntOrNull)
.custom(isAbuseTimestampValid)
- .withMessage('Should have valid ending time value')
- .bail()
- .custom(isAbuseTimestampCoherent)
- .withMessage('Should have a startAt timestamp beginning before endAt'),
+ .withMessage('Should have valid ending time value'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
import { MUserAccountUrl } from '@server/types/models'
import { UserRight } from '../../../../shared'
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
-import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
+import {
+ doesVideoCommentExist,
+ doesVideoCommentThreadExist,
+ isValidVideoCommentText
+} from '../../../helpers/custom-validators/video-comments'
import { logger } from '../../../helpers/logger'
import { doesVideoExist } from '../../../helpers/middlewares'
import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
import { Hooks } from '../../../lib/plugins/hooks'
-import { VideoCommentModel } from '../../../models/video/video-comment'
-import { MCommentOwnerVideoReply, MVideo, MVideoFullLight, MVideoId } from '../../../types/models/video'
+import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
import { areValidationErrors } from '../utils'
const listVideoCommentThreadsValidator = [
// ---------------------------------------------------------------------------
-async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
- const id = parseInt(idArg + '', 10)
- const videoComment = await VideoCommentModel.loadById(id)
-
- if (!videoComment) {
- res.status(404)
- .json({ error: 'Video comment thread not found' })
- .end()
-
- return false
- }
-
- if (videoComment.videoId !== video.id) {
- res.status(400)
- .json({ error: 'Video comment is not associated to this video.' })
- .end()
-
- return false
- }
-
- if (videoComment.inReplyToCommentId !== null) {
- res.status(400)
- .json({ error: 'Video comment is not a thread.' })
- .end()
-
- return false
- }
-
- res.locals.videoCommentThread = videoComment
- return true
-}
-
-async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
- const id = parseInt(idArg + '', 10)
- const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
-
- if (!videoComment) {
- res.status(404)
- .json({ error: 'Video comment thread not found' })
- .end()
-
- return false
- }
-
- if (videoComment.videoId !== video.id) {
- res.status(400)
- .json({ error: 'Video comment is not associated to this video.' })
- .end()
-
- return false
- }
-
- res.locals.videoCommentFull = videoComment
- return true
-}
-
function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
if (video.commentsEnabled !== true) {
res.status(409)
.json({ error: 'Video comments are disabled for this video.' })
- .end()
return false
}
if (videoComment.isDeleted()) {
res.status(409)
.json({ error: 'This comment is already deleted' })
- .end()
+
return false
}
if (!acceptedResult || acceptedResult.accepted !== true) {
logger.info('Refused local comment.', { acceptedResult, acceptParameters })
res.status(403)
- .json({ error: acceptedResult.errorMessage || 'Refused local comment' })
+ .json({ error: acceptedResult.errorMessage || 'Refused local comment' })
return false
}
import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
import {
Abuse,
+ AbuseFilter,
AbuseObject,
AbusePredefinedReasons,
abusePredefinedReasonsMap,
AbusePredefinedReasonsString,
AbuseState,
AbuseVideoIs,
- VideoAbuse
+ VideoAbuse,
+ VideoCommentAbuse
} from '@shared/models'
-import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter'
-import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
+import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
import { VideoAbuseModel } from './video-abuse'
import { VideoCommentAbuseModel } from './video-comment-abuse'
+import { VideoCommentModel } from '../video/video-comment'
export enum ScopeNames {
FOR_API = 'FOR_API'
serverAccountId: number
userAccountId: number
}) => {
- const onlyBlacklisted = options.videoIs === 'blacklisted'
- const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel)
+ const whereAnd: WhereOptions[] = []
- const where = {
+ whereAnd.push({
reporterAccountId: {
[Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
}
- }
+ })
if (options.search) {
const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%')
- Object.assign(where, {
+ whereAnd.push({
[Op.or]: [
{
[Op.and]: [
})
}
- if (options.id) Object.assign(where, { id: options.id })
- if (options.state) Object.assign(where, { state: options.state })
+ if (options.id) whereAnd.push({ id: options.id })
+ if (options.state) whereAnd.push({ state: options.state })
if (options.videoIs === 'deleted') {
- Object.assign(where, {
+ whereAnd.push({
'$VideoAbuse.deletedVideo$': {
[Op.not]: null
}
}
if (options.predefinedReasonId) {
- Object.assign(where, {
+ whereAnd.push({
predefinedReasons: {
[Op.contains]: [ options.predefinedReasonId ]
}
})
}
+ if (options.filter === 'account') {
+ whereAnd.push({
+ videoId: null,
+ commentId: null
+ })
+ }
+
+ const onlyBlacklisted = options.videoIs === 'blacklisted'
+ const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel)
+
return {
attributes: {
include: [
required: true,
where: searchAttribute(options.searchReportee, 'name')
},
+ {
+ model: VideoCommentAbuseModel.unscoped(),
+ required: options.filter === 'comment',
+ include: [
+ {
+ model: VideoCommentModel.unscoped(),
+ required: false,
+ include: [
+ {
+ model: VideoModel.unscoped(),
+ attributes: [ 'name', 'id', 'uuid' ],
+ required: true
+ }
+ ]
+ }
+ ]
+ },
{
model: VideoAbuseModel,
required: options.filter === 'video' || !!options.videoIs || videoRequired,
include: [
{
model: AccountModel.scope(AccountScopeNames.SUMMARY),
- required: true,
- where: searchAttribute(options.searchReportee, 'name')
+ required: true
}
]
},
]
}
],
- where
+ where: {
+ [Op.and]: whereAnd
+ }
}
}
}))
})
VideoAbuse: VideoAbuseModel
+ // FIXME: deprecated in 2.3. Remove these validators
static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
const videoWhere: WhereOptions = {}
return AbuseModel.findOne(query)
}
+ static loadById (id: number): Bluebird<MAbuse> {
+ const query = {
+ where: {
+ id
+ }
+ }
+
+ return AbuseModel.findOne(query)
+ }
+
static listForApi (parameters: {
start: number
count: number
const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
let video: VideoAbuse
+ let comment: VideoCommentAbuse
if (this.VideoAbuse) {
const abuseModel = this.VideoAbuse
}
}
+ if (this.VideoCommentAbuse) {
+ const abuseModel = this.VideoCommentAbuse
+ const entity = abuseModel.VideoComment || abuseModel.deletedComment
+
+ comment = {
+ id: entity.id,
+ text: entity.text,
+
+ deleted: !abuseModel.VideoComment,
+
+ video: {
+ id: entity.Video.id,
+ name: entity.Video.name,
+ uuid: entity.Video.uuid
+ }
+ }
+ }
+
return {
id: this.id,
reason: this.reason,
moderationComment: this.moderationComment,
video,
- comment: null,
+ comment,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
@AllowNull(true)
@Default(null)
@Column(DataType.JSONB)
- deletedComment: VideoComment
+ deletedComment: VideoComment & { Video: { name: string, id: number, uuid: string }}
@ForeignKey(() => AbuseModel)
@Column
import * as Bluebird from 'bluebird'
import { uniq } from 'lodash'
import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
-import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import {
+ AllowNull,
+ BeforeDestroy,
+ BelongsTo,
+ Column,
+ CreatedAt,
+ DataType,
+ ForeignKey,
+ HasMany,
+ Is,
+ Model,
+ Scopes,
+ Table,
+ UpdatedAt
+} from 'sequelize-typescript'
+import { logger } from '@server/helpers/logger'
import { getServerActor } from '@server/models/application/application'
import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
import { VideoPrivacy } from '@shared/models'
MCommentOwnerVideoReply,
MVideoImmutable
} from '../../types/models/video'
+import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
import { AccountModel } from '../account/account'
import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
})
Account: AccountModel
+ @HasMany(() => VideoCommentAbuseModel, {
+ foreignKey: {
+ name: 'commentId',
+ allowNull: true
+ },
+ onDelete: 'set null'
+ })
+ CommentAbuses: VideoCommentAbuseModel[]
+
+ @BeforeDestroy
+ static async saveEssentialDataToAbuses (instance: VideoCommentModel, options) {
+ const tasks: Promise<any>[] = []
+
+ if (!Array.isArray(instance.CommentAbuses)) {
+ instance.CommentAbuses = await instance.$get('CommentAbuses')
+
+ if (instance.CommentAbuses.length === 0) return undefined
+ }
+
+ if (!instance.Video) {
+ instance.Video = await instance.$get('Video')
+ }
+
+ logger.info('Saving video comment %s for abuse.', instance.url)
+
+ const details = Object.assign(instance.toFormattedJSON(), {
+ Video: {
+ id: instance.Video.id,
+ name: instance.Video.name,
+ uuid: instance.Video.uuid
+ }
+ })
+
+ for (const abuse of instance.CommentAbuses) {
+ abuse.deletedComment = details
+
+ tasks.push(abuse.save({ transaction: options.transaction }))
+ }
+
+ Promise.all(tasks)
+ .catch(err => {
+ logger.error('Some errors when saving details of comment %s in its abuses before destroy hook.', instance.url, { err })
+ })
+
+ return undefined
+ }
+
static loadById (id: number, t?: Transaction): Bluebird<MComment> {
const query: FindOptions = {
where: {
static async saveEssentialDataToAbuses (instance: VideoModel, options) {
const tasks: Promise<any>[] = []
- logger.info('Saving video abuses details of video %s.', instance.url)
-
if (!Array.isArray(instance.VideoAbuses)) {
instance.VideoAbuses = await instance.$get('VideoAbuses')
if (instance.VideoAbuses.length === 0) return undefined
}
+ logger.info('Saving video abuses details of video %s.', instance.url)
+
const details = instance.toFormattedDetailsJSON()
for (const abuse of instance.VideoAbuses) {
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import { AbuseCreate, AbuseState } from '@shared/models'
+import {
+ cleanupTests,
+ createUser,
+ deleteAbuse,
+ flushAndRunServer,
+ makeGetRequest,
+ makePostBodyRequest,
+ ServerInfo,
+ setAccessTokensToServers,
+ updateAbuse,
+ uploadVideo,
+ userLogin
+} from '../../../../shared/extra-utils'
+import {
+ checkBadCountPagination,
+ checkBadSortPagination,
+ checkBadStartPagination
+} from '../../../../shared/extra-utils/requests/check-api-params'
+
+// FIXME: deprecated in 2.3. Remove this controller
+
+describe('Test video abuses API validators', function () {
+ const basePath = '/api/v1/abuses/'
+
+ let server: ServerInfo
+ let userAccessToken = ''
+ let abuseId: number
+
+ // ---------------------------------------------------------------
+
+ before(async function () {
+ this.timeout(30000)
+
+ server = await flushAndRunServer(1)
+
+ await setAccessTokensToServers([ server ])
+
+ const username = 'user1'
+ const password = 'my super password'
+ await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
+ userAccessToken = await userLogin(server, { username, password })
+
+ const res = await uploadVideo(server.url, server.accessToken, {})
+ server.video = res.body.video
+ })
+
+ describe('When listing abuses', function () {
+ const path = basePath
+
+ it('Should fail with a bad start pagination', async function () {
+ await checkBadStartPagination(server.url, path, server.accessToken)
+ })
+
+ it('Should fail with a bad count pagination', async function () {
+ await checkBadCountPagination(server.url, path, server.accessToken)
+ })
+
+ it('Should fail with an incorrect sort', async function () {
+ await checkBadSortPagination(server.url, path, server.accessToken)
+ })
+
+ it('Should fail with a non authenticated user', async function () {
+ await makeGetRequest({
+ url: server.url,
+ path,
+ statusCodeExpected: 401
+ })
+ })
+
+ it('Should fail with a non admin user', async function () {
+ await makeGetRequest({
+ url: server.url,
+ path,
+ token: userAccessToken,
+ statusCodeExpected: 403
+ })
+ })
+
+ it('Should fail with a bad id filter', async function () {
+ await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } })
+ })
+
+ it('Should fail with a bad filter', async function () {
+ await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } })
+ await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } })
+ })
+
+ it('Should fail with bad predefined reason', async function () {
+ await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } })
+ })
+
+ it('Should fail with a bad state filter', async function () {
+ await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } })
+ await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } })
+ })
+
+ it('Should fail with a bad videoIs filter', async function () {
+ await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } })
+ })
+
+ it('Should succeed with the correct params', async function () {
+ const query = {
+ id: 13,
+ predefinedReason: 'violentOrRepulsive',
+ filter: 'comment',
+ state: 2,
+ videoIs: 'deleted'
+ }
+
+ await makeGetRequest({ url: server.url, path, token: server.accessToken, 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 })
+ })
+
+ 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 })
+ })
+
+ 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 })
+ })
+
+ 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 })
+ })
+
+ 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 })
+ })
+
+ 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 })
+ })
+
+ 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 })
+ })
+
+ 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 })
+ })
+
+ it('Should fail with a non authenticated user', async function () {
+ const fields = { video: { id: server.video.id }, reason: 'my super reason' }
+
+ await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 })
+ })
+
+ it('Should fail with a reason too short', async function () {
+ const fields = { video: { id: server.video.id }, reason: 'h' }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, 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 })
+ })
+
+ 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 })
+ 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 })
+ })
+
+ 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 })
+ })
+
+ 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 })
+ })
+
+ it('Should succeed with the corret parameters (advanced)', async function () {
+ const fields: AbuseCreate = {
+ video: {
+ id: server.video.id,
+ startAt: 1,
+ endAt: 5
+ },
+ reason: 'my super reason',
+ predefinedReasons: [ 'serverRules' ]
+ }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
+ })
+ })
+
+ describe('When updating an abuse', function () {
+
+ it('Should fail with a non authenticated user', async function () {
+ await updateAbuse(server.url, 'blabla', abuseId, {}, 401)
+ })
+
+ it('Should fail with a non admin user', async function () {
+ await updateAbuse(server.url, userAccessToken, abuseId, {}, 403)
+ })
+
+ it('Should fail with a bad abuse id', async function () {
+ await updateAbuse(server.url, server.accessToken, 45, {}, 404)
+ })
+
+ it('Should fail with a bad state', async function () {
+ const body = { state: 5 }
+ await updateAbuse(server.url, server.accessToken, abuseId, body, 400)
+ })
+
+ it('Should fail with a bad moderation comment', async function () {
+ const body = { moderationComment: 'b'.repeat(3001) }
+ await updateAbuse(server.url, server.accessToken, abuseId, body, 400)
+ })
+
+ it('Should succeed with the correct params', async function () {
+ const body = { state: AbuseState.ACCEPTED }
+ await updateAbuse(server.url, server.accessToken, abuseId, body)
+ })
+ })
+
+ describe('When deleting a video abuse', function () {
+
+ it('Should fail with a non authenticated user', async function () {
+ await deleteAbuse(server.url, 'blabla', abuseId, 401)
+ })
+
+ it('Should fail with a non admin user', async function () {
+ await deleteAbuse(server.url, userAccessToken, abuseId, 403)
+ })
+
+ it('Should fail with a bad abuse id', async function () {
+ await deleteAbuse(server.url, server.accessToken, 45, 404)
+ })
+
+ it('Should succeed with the correct params', async function () {
+ await deleteAbuse(server.url, server.accessToken, abuseId)
+ })
+ })
+
+ after(async function () {
+ await cleanupTests([ server ])
+ })
+})
+import './abuses'
import './accounts'
import './blocklist'
import './bulk'
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
})
- it('Should fail mith misordered startAt/endAt', async function () {
- const fields = { reason: 'my super reason', startAt: 5, endAt: 1 }
-
- await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
- })
-
it('Should succeed with the corret parameters (advanced)', async function () {
const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 }
import { PickWith } from '@shared/core-utils'
import { AbuseModel } from '../../../models/abuse/abuse'
import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account'
-import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo } from '../video'
+import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video'
import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M>
MCommentAbuse &
UseCommentAbuse<'VideoComment', MCommentUrl>
+export type MCommentAbuseFormattable =
+ MCommentAbuse &
+ UseCommentAbuse<'VideoComment', MComment & PickWith<MCommentVideo, 'Video', Pick<MVideo, 'id' | 'uuid' | 'name'>>>
+
// ############################################################################
export type MAbuseId = Pick<AbuseModel, 'id'>
export type MAbuseFormattable =
MAbuse &
Use<'ReporterAccount', MAccountFormattable> &
- Use<'VideoAbuse', MVideoAbuseFormattable>
+ Use<'VideoAbuse', MVideoAbuseFormattable> &
+ Use<'VideoCommentAbuse', MCommentAbuseFormattable>
accountVideoRate?: MAccountVideoRateAccountVideo
+ videoComment?: MComment
videoCommentFull?: MCommentOwnerVideoReply
videoCommentThread?: MComment
-import * as request from 'supertest'
-import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
-import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
-function reportAbuse (
- url: string,
- token: string,
- videoId: number | string,
- reason: string,
- predefinedReasons?: AbusePredefinedReasonsString[],
- startAt?: number,
- endAt?: number,
- specialStatus = 200
-) {
- const path = '/api/v1/videos/' + videoId + '/abuse'
-
- return request(url)
- .post(path)
- .set('Accept', 'application/json')
- .set('Authorization', 'Bearer ' + token)
- .send({ reason, predefinedReasons, startAt, endAt })
- .expect(specialStatus)
+import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
+import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
+
+function reportAbuse (options: {
+ url: string
+ token: string
+
+ reason: string
+
+ accountId?: number
+ videoId?: number
+ commentId?: number
+
+ predefinedReasons?: AbusePredefinedReasonsString[]
+
+ startAt?: number
+ endAt?: number
+
+ statusCodeExpected?: number
+}) {
+ const path = '/api/v1/abuses'
+
+ const video = options.videoId ? {
+ id: options.videoId,
+ startAt: options.startAt,
+ endAt: options.endAt
+ } : undefined
+
+ const comment = options.commentId ? {
+ id: options.commentId
+ } : undefined
+
+ const account = options.accountId ? {
+ id: options.accountId
+ } : undefined
+
+ const body = {
+ account,
+ video,
+ comment,
+
+ reason: options.reason,
+ predefinedReasons: options.predefinedReasons
+ }
+
+ return makePostBodyRequest({
+ url: options.url,
+ path,
+ token: options.token,
+
+ fields: body,
+ statusCodeExpected: options.statusCodeExpected || 200
+ })
}
function getAbusesList (options: {
id?: number
predefinedReason?: AbusePredefinedReasonsString
search?: string
+ filter?: AbuseFilter,
state?: AbuseState
videoIs?: AbuseVideoIs
searchReporter?: string
id,
predefinedReason,
search,
+ filter,
state,
videoIs,
searchReporter,
searchVideo,
searchVideoChannel
} = options
- const path = '/api/v1/videos/abuse'
+ const path = '/api/v1/abuses'
const query = {
sort: 'createdAt',
predefinedReason,
search,
state,
+ filter,
videoIs,
searchReporter,
searchReportee,
function updateAbuse (
url: string,
token: string,
- videoId: string | number,
- videoAbuseId: number,
+ abuseId: number,
body: AbuseUpdate,
statusCodeExpected = 204
) {
- const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
+ const path = '/api/v1/abuses/' + abuseId
return makePutBodyRequest({
url,
})
}
-function deleteAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) {
- const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
+function deleteAbuse (url: string, token: string, abuseId: number, statusCodeExpected = 204) {
+ const path = '/api/v1/abuses/' + abuseId
return makeDeleteRequest({
url,
import { AbusePredefinedReasonsString } from './abuse-reason.model'
export interface AbuseCreate {
- accountId: number
-
reason: string
+
predefinedReasons?: AbusePredefinedReasonsString[]
+ account?: {
+ id: number
+ }
+
video?: {
id: number
startAt?: number
+++ /dev/null
-export type AbuseFilter = 'video' | 'comment'
--- /dev/null
+export type AbuseFilter = 'video' | 'comment' | 'account'
name: string
uuid: string
nsfw: boolean
+
deleted: boolean
blacklisted: boolean
export interface VideoCommentAbuse {
id: number
- account?: Account
+
+ video: {
+ id: number
+ name: string
+ uuid: string
+ }
+
text: string
+
deleted: boolean
}
export * from './abuse-create.model'
+export * from './abuse-filter.type'
export * from './abuse-reason.model'
export * from './abuse-state.model'
export * from './abuse-update.model'