Implement abuses check params

This commit is contained in:
Chocobozzz 2020-07-07 10:57:04 +02:00 committed by Chocobozzz
parent d95d155988
commit 57f6896f67
22 changed files with 665 additions and 162 deletions

View File

@ -23,7 +23,7 @@ import { AccountModel } from '../../models/account/account'
const abuseRouter = express.Router()
abuseRouter.get('/abuse',
abuseRouter.get('/',
authenticate,
ensureUserHasRight(UserRight.MANAGE_ABUSES),
paginationValidator,
@ -33,18 +33,18 @@ abuseRouter.get('/abuse',
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),
@ -74,7 +74,7 @@ async function listAbuses (req: express.Request, res: express.Response) {
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,

View File

@ -1,6 +1,6 @@
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
@ -13,7 +13,11 @@ function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) {
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)
}
@ -22,7 +26,9 @@ function isAbuseTimestampValid (value: number) {
}
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) {
@ -44,8 +50,9 @@ function isAbuseVideoIsValid (value: AbuseVideoIs) {
export {
isAbuseReasonValid,
isAbuseFilterValid,
isAbusePredefinedReasonValid,
isAbusePredefinedReasonsValid,
areAbusePredefinedReasonsValid as isAbusePredefinedReasonsValid,
isAbuseTimestampValid,
isAbuseTimestampCoherent,
isAbuseModerationCommentValid,

View File

@ -1,6 +1,8 @@
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
@ -8,8 +10,83 @@ function isValidVideoCommentText (value: string) {
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
}

View File

@ -17,7 +17,6 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
if (abuse === null) {
res.status(404)
.json({ error: 'Video abuse not found' })
.end()
return false
}
@ -26,8 +25,18 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
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
}
// ---------------------------------------------------------------------------

View File

@ -3,8 +3,8 @@ import { AccountModel } from '../../models/account/account'
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)
}

View File

@ -1,6 +1,7 @@
import * as express from 'express'
import { body, param, query } from 'express-validator'
import {
isAbuseFilterValid,
isAbuseModerationCommentValid,
isAbusePredefinedReasonsValid,
isAbusePredefinedReasonValid,
@ -11,29 +12,28 @@ import {
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')
.custom(isIdOrUUIDValid)
.not()
.isEmpty()
.withMessage('Should have a valid videoId'),
body('reason')
.custom(isAbuseReasonValid)
.withMessage('Should have a valid reason'),
body('predefinedReasons')
body('account.id')
.optional()
.custom(isAbusePredefinedReasonsValid)
.withMessage('Should have a valid list of predefined reasons'),
body('startAt')
.custom(isIdValid)
.withMessage('Should have a valid accountId'),
body('video.id')
.optional()
.custom(isIdOrUUIDValid)
.withMessage('Should have a valid videoId'),
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)
@ -42,47 +42,70 @@ const abuseReportValidator = [
.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()
}
@ -92,6 +115,10 @@ const abuseListValidator = [
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)
@ -151,10 +178,7 @@ const videoAbuseReportValidator = [
.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 })

View File

@ -3,13 +3,16 @@ import { body, param } from 'express-validator'
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 = [
@ -120,67 +123,10 @@ export {
// ---------------------------------------------------------------------------
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
}
@ -192,7 +138,7 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC
if (videoComment.isDeleted()) {
res.status(409)
.json({ error: 'This comment is already deleted' })
.end()
return false
}
@ -240,7 +186,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
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
}

View File

@ -19,16 +19,17 @@ import {
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'
@ -38,6 +39,7 @@ import { VideoBlacklistModel } from '../video/video-blacklist'
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'
@ -66,19 +68,18 @@ export enum ScopeNames {
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]: [
@ -110,11 +111,11 @@ export enum ScopeNames {
})
}
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
}
@ -122,13 +123,23 @@ export enum ScopeNames {
}
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: [
@ -222,6 +233,23 @@ export enum ScopeNames {
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,
@ -241,8 +269,7 @@ export enum ScopeNames {
include: [
{
model: AccountModel.scope(AccountScopeNames.SUMMARY),
required: true,
where: searchAttribute(options.searchReportee, 'name')
required: true
}
]
},
@ -256,7 +283,9 @@ export enum ScopeNames {
]
}
],
where
where: {
[Op.and]: whereAnd
}
}
}
}))
@ -348,6 +377,7 @@ export class AbuseModel extends Model<AbuseModel> {
})
VideoAbuse: VideoAbuseModel
// FIXME: deprecated in 2.3. Remove these validators
static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
const videoWhere: WhereOptions = {}
@ -369,6 +399,16 @@ export class AbuseModel extends Model<AbuseModel> {
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
@ -454,6 +494,7 @@ export class AbuseModel extends Model<AbuseModel> {
const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
let video: VideoAbuse
let comment: VideoCommentAbuse
if (this.VideoAbuse) {
const abuseModel = this.VideoAbuse
@ -475,6 +516,24 @@ export class AbuseModel extends Model<AbuseModel> {
}
}
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,
@ -490,7 +549,7 @@ export class AbuseModel extends Model<AbuseModel> {
moderationComment: this.moderationComment,
video,
comment: null,
comment,
createdAt: this.createdAt,
updatedAt: this.updatedAt,

View File

@ -25,7 +25,7 @@ export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> {
@AllowNull(true)
@Default(null)
@Column(DataType.JSONB)
deletedComment: VideoComment
deletedComment: VideoComment & { Video: { name: string, id: number, uuid: string }}
@ForeignKey(() => AbuseModel)
@Column

View File

@ -1,7 +1,22 @@
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'
@ -24,6 +39,7 @@ import {
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'
@ -224,6 +240,53 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
})
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: {

View File

@ -803,14 +803,14 @@ export class VideoModel extends Model<VideoModel> {
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) {

View File

@ -0,0 +1,271 @@
/* 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 ])
})
})

View File

@ -1,3 +1,4 @@
import './abuses'
import './accounts'
import './blocklist'
import './bulk'

View File

@ -152,12 +152,6 @@ describe('Test video abuses API validators', function () {
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 }

View File

@ -3,7 +3,7 @@ import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse
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>
@ -51,6 +51,10 @@ export type MCommentAbuseUrl =
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'>
@ -94,4 +98,5 @@ export type MAbuseFull =
export type MAbuseFormattable =
MAbuse &
Use<'ReporterAccount', MAccountFormattable> &
Use<'VideoAbuse', MVideoAbuseFormattable>
Use<'VideoAbuse', MVideoAbuseFormattable> &
Use<'VideoCommentAbuse', MCommentAbuseFormattable>

View File

@ -91,6 +91,7 @@ declare module 'express' {
accountVideoRate?: MAccountVideoRateAccountVideo
videoComment?: MComment
videoCommentFull?: MCommentOwnerVideoReply
videoCommentThread?: MComment

View File

@ -1,25 +1,57 @@
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'
import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
return request(url)
.post(path)
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token)
.send({ reason, predefinedReasons, startAt, endAt })
.expect(specialStatus)
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: {
@ -28,6 +60,7 @@ function getAbusesList (options: {
id?: number
predefinedReason?: AbusePredefinedReasonsString
search?: string
filter?: AbuseFilter,
state?: AbuseState
videoIs?: AbuseVideoIs
searchReporter?: string
@ -41,6 +74,7 @@ function getAbusesList (options: {
id,
predefinedReason,
search,
filter,
state,
videoIs,
searchReporter,
@ -48,7 +82,7 @@ function getAbusesList (options: {
searchVideo,
searchVideoChannel
} = options
const path = '/api/v1/videos/abuse'
const path = '/api/v1/abuses'
const query = {
sort: 'createdAt',
@ -56,6 +90,7 @@ function getAbusesList (options: {
predefinedReason,
search,
state,
filter,
videoIs,
searchReporter,
searchReportee,
@ -75,12 +110,11 @@ function getAbusesList (options: {
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,
@ -91,8 +125,8 @@ function updateAbuse (
})
}
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,

View File

@ -1,11 +1,14 @@
import { AbusePredefinedReasonsString } from './abuse-reason.model'
export interface AbuseCreate {
accountId: number
reason: string
predefinedReasons?: AbusePredefinedReasonsString[]
account?: {
id: number
}
video?: {
id: number
startAt?: number

View File

@ -1 +0,0 @@
export type AbuseFilter = 'video' | 'comment'

View File

@ -0,0 +1 @@
export type AbuseFilter = 'video' | 'comment' | 'account'

View File

@ -9,6 +9,7 @@ export interface VideoAbuse {
name: string
uuid: string
nsfw: boolean
deleted: boolean
blacklisted: boolean
@ -21,8 +22,15 @@ export interface VideoAbuse {
export interface VideoCommentAbuse {
id: number
account?: Account
video: {
id: number
name: string
uuid: string
}
text: string
deleted: boolean
}

View File

@ -1,4 +1,5 @@
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'