Add state and moderationComment for abuses on server side

This commit is contained in:
Chocobozzz 2018-08-10 16:54:01 +02:00
parent 904a463c77
commit 268eebed92
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
19 changed files with 482 additions and 64 deletions

View File

@ -1,5 +1,5 @@
import * as express from 'express' import * as express from 'express'
import { UserRight, VideoAbuseCreate } from '../../../../shared' import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils' import { getFormattedObjects } from '../../../helpers/utils'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
@ -12,8 +12,10 @@ import {
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
setDefaultSort, setDefaultSort,
videoAbuseGetValidator,
videoAbuseReportValidator, videoAbuseReportValidator,
videoAbusesSortValidator videoAbusesSortValidator,
videoAbuseUpdateValidator
} from '../../../middlewares' } from '../../../middlewares'
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
@ -32,11 +34,23 @@ abuseVideoRouter.get('/abuse',
setDefaultPagination, setDefaultPagination,
asyncMiddleware(listVideoAbuses) asyncMiddleware(listVideoAbuses)
) )
abuseVideoRouter.post('/:id/abuse', abuseVideoRouter.put('/:videoId/abuse/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
asyncMiddleware(videoAbuseUpdateValidator),
asyncRetryTransactionMiddleware(updateVideoAbuse)
)
abuseVideoRouter.post('/:videoId/abuse',
authenticate, authenticate,
asyncMiddleware(videoAbuseReportValidator), asyncMiddleware(videoAbuseReportValidator),
asyncRetryTransactionMiddleware(reportVideoAbuse) asyncRetryTransactionMiddleware(reportVideoAbuse)
) )
abuseVideoRouter.delete('/:videoId/abuse/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
asyncMiddleware(videoAbuseGetValidator),
asyncRetryTransactionMiddleware(deleteVideoAbuse)
)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -46,12 +60,39 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function listVideoAbuses (req: express.Request, res: express.Response, next: express.NextFunction) { async function listVideoAbuses (req: express.Request, res: express.Response) {
const resultList = await VideoAbuseModel.listForApi(req.query.start, req.query.count, req.query.sort) const resultList = await VideoAbuseModel.listForApi(req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
async function updateVideoAbuse (req: express.Request, res: express.Response) {
const videoAbuse: VideoAbuseModel = res.locals.videoAbuse
if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment
if (req.body.state !== undefined) videoAbuse.state = req.body.state
await sequelizeTypescript.transaction(t => {
return videoAbuse.save({ transaction: t })
})
// Do not send the delete to other instances, we updated OUR copy of this video abuse
return res.type('json').status(204).end()
}
async function deleteVideoAbuse (req: express.Request, res: express.Response) {
const videoAbuse: VideoAbuseModel = res.locals.videoAbuse
await sequelizeTypescript.transaction(t => {
return videoAbuse.destroy({ transaction: t })
})
// Do not send the delete to other instances, we delete OUR copy of this video abuse
return res.type('json').status(204).end()
}
async function reportVideoAbuse (req: express.Request, res: express.Response) { async function reportVideoAbuse (req: express.Request, res: express.Response) {
const videoInstance = res.locals.video as VideoModel const videoInstance = res.locals.video as VideoModel
const reporterAccount = res.locals.oauth.token.User.Account as AccountModel const reporterAccount = res.locals.oauth.token.User.Account as AccountModel
@ -60,10 +101,11 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
const abuseToCreate = { const abuseToCreate = {
reporterAccountId: reporterAccount.id, reporterAccountId: reporterAccount.id,
reason: body.reason, reason: body.reason,
videoId: videoInstance.id videoId: videoInstance.id,
state: VideoAbuseState.PENDING
} }
await sequelizeTypescript.transaction(async t => { const videoAbuse: VideoAbuseModel = await sequelizeTypescript.transaction(async t => {
const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
videoAbuseInstance.Video = videoInstance videoAbuseInstance.Video = videoInstance
videoAbuseInstance.Account = reporterAccount videoAbuseInstance.Account = reporterAccount
@ -74,8 +116,12 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
} }
auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON()))
logger.info('Abuse report for video %s created.', videoInstance.name)
return videoAbuseInstance
}) })
return res.type('json').status(204).end() logger.info('Abuse report for video %s created.', videoInstance.name)
return res.json({
videoAbuse: videoAbuse.toFormattedJSON()
}).end()
} }

View File

@ -3,7 +3,6 @@ import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers'
import { peertubeTruncate } from '../../core-utils' import { peertubeTruncate } from '../../core-utils'
import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
import { import {
isVideoAbuseReasonValid,
isVideoDurationValid, isVideoDurationValid,
isVideoNameValid, isVideoNameValid,
isVideoStateValid, isVideoStateValid,
@ -13,6 +12,7 @@ import {
} from '../videos' } from '../videos'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
import { VideoState } from '../../../../shared/models/videos' import { VideoState } from '../../../../shared/models/videos'
import { isVideoAbuseReasonValid } from '../video-abuses'
function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
return isBaseActivityValid(activity, 'Create') && return isBaseActivityValid(activity, 'Create') &&

View File

@ -0,0 +1,43 @@
import { Response } from 'express'
import * as validator from 'validator'
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers'
import { exists } from './misc'
import { VideoAbuseModel } from '../../models/video/video-abuse'
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
function isVideoAbuseReasonValid (value: string) {
return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
}
function isVideoAbuseModerationCommentValid (value: string) {
return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
}
function isVideoAbuseStateValid (value: string) {
return exists(value) && VIDEO_ABUSE_STATES[ value ] !== undefined
}
async function isVideoAbuseExist (abuseId: number, videoId: number, res: Response) {
const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId)
if (videoAbuse === null) {
res.status(404)
.json({ error: 'Video abuse not found' })
.end()
return false
}
res.locals.videoAbuse = videoAbuse
return true
}
// ---------------------------------------------------------------------------
export {
isVideoAbuseExist,
isVideoAbuseStateValid,
isVideoAbuseReasonValid,
isVideoAbuseModerationCommentValid
}

View File

@ -6,6 +6,7 @@ import * as validator from 'validator'
import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared'
import { import {
CONSTRAINTS_FIELDS, CONSTRAINTS_FIELDS,
VIDEO_ABUSE_STATES,
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_MIMETYPE_EXT, VIDEO_MIMETYPE_EXT,
@ -18,6 +19,7 @@ import { exists, isArray, isFileValid } from './misc'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
import { UserModel } from '../../models/account/user' import { UserModel } from '../../models/account/user'
import * as magnetUtil from 'magnet-uri' import * as magnetUtil from 'magnet-uri'
import { VideoAbuseModel } from '../../models/video/video-abuse'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
@ -71,10 +73,6 @@ function isVideoTagsValid (tags: string[]) {
) )
} }
function isVideoAbuseReasonValid (value: string) {
return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
}
function isVideoViewsValid (value: string) { function isVideoViewsValid (value: string) {
return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS) return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
} }
@ -220,7 +218,6 @@ export {
isVideoTagsValid, isVideoTagsValid,
isVideoFPSResolutionValid, isVideoFPSResolutionValid,
isScheduleVideoUpdatePrivacyValid, isScheduleVideoUpdatePrivacyValid,
isVideoAbuseReasonValid,
isVideoFile, isVideoFile,
isVideoMagnetUriValid, isVideoMagnetUriValid,
isVideoStateValid, isVideoStateValid,

View File

@ -3,7 +3,7 @@ import { dirname, join } from 'path'
import { JobType, VideoRateType, VideoState } from '../../shared/models' import { JobType, VideoRateType, VideoState } from '../../shared/models'
import { ActivityPubActorType } from '../../shared/models/activitypub' import { ActivityPubActorType } from '../../shared/models/activitypub'
import { FollowState } from '../../shared/models/actors' import { FollowState } from '../../shared/models/actors'
import { VideoPrivacy } from '../../shared/models/videos' import { VideoPrivacy, VideoAbuseState } from '../../shared/models/videos'
// Do not use barrels, remain constants as independent as possible // Do not use barrels, remain constants as independent as possible
import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
@ -15,7 +15,7 @@ let config: IConfig = require('config')
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 245 const LAST_MIGRATION_VERSION = 250
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -258,7 +258,8 @@ const CONSTRAINTS_FIELDS = {
BLOCKED_REASON: { min: 3, max: 250 } // Length BLOCKED_REASON: { min: 3, max: 250 } // Length
}, },
VIDEO_ABUSES: { VIDEO_ABUSES: {
REASON: { min: 2, max: 300 } // Length REASON: { min: 2, max: 300 }, // Length
MODERATION_COMMENT: { min: 2, max: 300 } // Length
}, },
VIDEO_CHANNELS: { VIDEO_CHANNELS: {
NAME: { min: 3, max: 120 }, // Length NAME: { min: 3, max: 120 }, // Length
@ -409,6 +410,12 @@ const VIDEO_IMPORT_STATES = {
[VideoImportState.SUCCESS]: 'Success' [VideoImportState.SUCCESS]: 'Success'
} }
const VIDEO_ABUSE_STATES = {
[VideoAbuseState.PENDING]: 'Pending',
[VideoAbuseState.REJECTED]: 'Rejected',
[VideoAbuseState.ACCEPTED]: 'Accepted'
}
const VIDEO_MIMETYPE_EXT = { const VIDEO_MIMETYPE_EXT = {
'video/webm': '.webm', 'video/webm': '.webm',
'video/ogg': '.ogv', 'video/ogg': '.ogv',
@ -625,6 +632,7 @@ export {
VIDEO_MIMETYPE_EXT, VIDEO_MIMETYPE_EXT,
VIDEO_TRANSCODING_FPS, VIDEO_TRANSCODING_FPS,
FFMPEG_NICE, FFMPEG_NICE,
VIDEO_ABUSE_STATES,
JOB_REQUEST_TIMEOUT, JOB_REQUEST_TIMEOUT,
USER_PASSWORD_RESET_LIFETIME, USER_PASSWORD_RESET_LIFETIME,
IMAGE_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT,

View File

@ -0,0 +1,47 @@
import * as Sequelize from 'sequelize'
import { CONSTRAINTS_FIELDS } from '../constants'
import { VideoAbuseState } from '../../../shared/models/videos'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<any> {
{
const data = {
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: null
}
await utils.queryInterface.addColumn('videoAbuse', 'state', data)
}
{
const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING
await utils.sequelize.query(query)
}
{
const data = {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: null
}
await utils.queryInterface.changeColumn('videoAbuse', 'state', data)
}
{
const data = {
type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max),
allowNull: true,
defaultValue: null
}
await utils.queryInterface.addColumn('videoAbuse', 'moderationComment', data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export { up, down }

View File

@ -1,4 +1,4 @@
import { ActivityCreate, VideoTorrentObject } from '../../../../shared' import { ActivityCreate, VideoAbuseState, VideoTorrentObject } from '../../../../shared'
import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects'
import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
import { retryTransactionWrapper } from '../../../helpers/database-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils'
@ -112,7 +112,8 @@ async function processCreateVideoAbuse (actor: ActorModel, videoAbuseToCreateDat
const videoAbuseData = { const videoAbuseData = {
reporterAccountId: account.id, reporterAccountId: account.id,
reason: videoAbuseToCreateData.content, reason: videoAbuseToCreateData.content,
videoId: video.id videoId: video.id,
state: VideoAbuseState.PENDING
} }
await VideoAbuseModel.create(videoAbuseData) await VideoAbuseModel.create(videoAbuseData)

View File

@ -7,6 +7,7 @@ export * from './feeds'
export * from './sort' export * from './sort'
export * from './users' export * from './users'
export * from './videos' export * from './videos'
export * from './video-abuses'
export * from './video-blacklist' export * from './video-blacklist'
export * from './video-channels' export * from './video-channels'
export * from './webfinger' export * from './webfinger'

View File

@ -0,0 +1,71 @@
import * as express from 'express'
import 'express-validator'
import { body, param } from 'express-validator/check'
import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
import { isVideoExist } from '../../helpers/custom-validators/videos'
import { logger } from '../../helpers/logger'
import { areValidationErrors } from './utils'
import {
isVideoAbuseExist,
isVideoAbuseModerationCommentValid,
isVideoAbuseReasonValid,
isVideoAbuseStateValid
} from '../../helpers/custom-validators/video-abuses'
const videoAbuseReportValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.videoId, res)) return
return next()
}
]
const videoAbuseGetValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.videoId, res)) return
if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return
return next()
}
]
const videoAbuseUpdateValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
body('state')
.optional()
.custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'),
body('moderationComment')
.optional()
.custom(isVideoAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.videoId, res)) return
if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return
return next()
}
]
// ---------------------------------------------------------------------------
export {
videoAbuseReportValidator,
videoAbuseGetValidator,
videoAbuseUpdateValidator
}

View File

@ -14,7 +14,6 @@ import {
import { import {
checkUserCanManageVideo, checkUserCanManageVideo,
isScheduleVideoUpdatePrivacyValid, isScheduleVideoUpdatePrivacyValid,
isVideoAbuseReasonValid,
isVideoCategoryValid, isVideoCategoryValid,
isVideoChannelOfAccountExist, isVideoChannelOfAccountExist,
isVideoDescriptionValid, isVideoDescriptionValid,
@ -174,20 +173,6 @@ const videosRemoveValidator = [
} }
] ]
const videoAbuseReportValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
return next()
}
]
const videoRateValidator = [ const videoRateValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
@ -299,8 +284,6 @@ export {
videosRemoveValidator, videosRemoveValidator,
videosShareValidator, videosShareValidator,
videoAbuseReportValidator,
videoRateValidator, videoRateValidator,
getCommonVideoAttributes getCommonVideoAttributes

View File

@ -1,11 +1,30 @@
import { AfterCreate, AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import {
AfterCreate,
AllowNull,
BelongsTo,
Column,
CreatedAt,
DataType,
Default,
ForeignKey,
Is,
Model,
Table,
UpdatedAt
} from 'sequelize-typescript'
import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
import { VideoAbuse } from '../../../shared/models/videos' import { VideoAbuse } from '../../../shared/models/videos'
import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos' import {
isVideoAbuseModerationCommentValid,
isVideoAbuseReasonValid,
isVideoAbuseStateValid
} from '../../helpers/custom-validators/video-abuses'
import { Emailer } from '../../lib/emailer' import { Emailer } from '../../lib/emailer'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
import { VideoAbuseState } from '../../../shared'
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers'
@Table({ @Table({
tableName: 'videoAbuse', tableName: 'videoAbuse',
@ -25,6 +44,18 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
@Column @Column
reason: string reason: string
@AllowNull(false)
@Default(null)
@Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
@Column
state: VideoAbuseState
@AllowNull(true)
@Default(null)
@Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
moderationComment: string
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -60,6 +91,16 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) return Emailer.Instance.addVideoAbuseReportJob(instance.videoId)
} }
static loadByIdAndVideoId (id: number, videoId: number) {
const query = {
where: {
id,
videoId
}
}
return VideoAbuseModel.findOne(query)
}
static listForApi (start: number, count: number, sort: string) { static listForApi (start: number, count: number, sort: string) {
const query = { const query = {
offset: start, offset: start,
@ -88,6 +129,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
id: this.id, id: this.id,
reason: this.reason, reason: this.reason,
reporterAccount: this.Account.toFormattedJSON(), reporterAccount: this.Account.toFormattedJSON(),
state: {
id: this.state,
label: VideoAbuseModel.getStateLabel(this.state)
},
moderationComment: this.moderationComment,
video: { video: {
id: this.Video.id, id: this.Video.id,
uuid: this.Video.uuid, uuid: this.Video.uuid,
@ -105,4 +151,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
object: this.Video.url object: this.Video.url
} }
} }
private static getStateLabel (id: number) {
return VIDEO_ABUSE_STATES[id] || 'Unknown'
}
} }

View File

@ -171,6 +171,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
video video
} }
} }
private static getStateLabel (id: number) { private static getStateLabel (id: number) {
return VIDEO_IMPORT_STATES[id] || 'Unknown' return VIDEO_IMPORT_STATES[id] || 'Unknown'
} }

View File

@ -3,14 +3,26 @@
import 'mocha' import 'mocha'
import { import {
createUser, flushTests, killallServers, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, createUser,
uploadVideo, userLogin deleteVideoAbuse,
flushTests,
killallServers,
makeGetRequest,
makePostBodyRequest,
runServer,
ServerInfo,
setAccessTokensToServers,
updateVideoAbuse,
uploadVideo,
userLogin
} from '../../utils' } from '../../utils'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
import { VideoAbuseState } from '../../../../shared/models/videos'
describe('Test video abuses API validators', function () { describe('Test video abuses API validators', function () {
let server: ServerInfo let server: ServerInfo
let userAccessToken = '' let userAccessToken = ''
let videoAbuseId: number
// --------------------------------------------------------------- // ---------------------------------------------------------------
@ -67,44 +79,111 @@ describe('Test video abuses API validators', function () {
describe('When reporting a video abuse', function () { describe('When reporting a video abuse', function () {
const basePath = '/api/v1/videos/' const basePath = '/api/v1/videos/'
let path: string
before(() => {
path = basePath + server.video.id + '/abuse'
})
it('Should fail with nothing', async function () { it('Should fail with nothing', async function () {
const path = basePath + server.video.id + '/abuse'
const fields = {} const fields = {}
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
}) })
it('Should fail with a wrong video', async function () { it('Should fail with a wrong video', async function () {
const wrongPath = '/api/v1/videos/blabla/abuse' const wrongPath = '/api/v1/videos/blabla/abuse'
const fields = { const fields = { reason: 'my super reason' }
reason: 'my super reason'
}
await makePostBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields }) await makePostBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields })
}) })
it('Should fail with a non authenticated user', async function () { it('Should fail with a non authenticated user', async function () {
const path = basePath + server.video.id + '/abuse' const fields = { reason: 'my super reason' }
const fields = {
reason: 'my super reason'
}
await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 })
}) })
it('Should fail with a reason too short', async function () { it('Should fail with a reason too short', async function () {
const path = basePath + server.video.id + '/abuse' const fields = { reason: 'h' }
const fields = {
reason: 'h'
}
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
}) })
it('Should fail with a reason too big', async function () { it('Should fail with a reason too big', async function () {
const path = basePath + server.video.id + '/abuse' const fields = { reason: 'super'.repeat(61) }
const fields = {
reason: 'super'.repeat(61)
}
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
}) })
it('Should succeed with the correct parameters', async function () {
const fields = { reason: 'super reason' }
const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
videoAbuseId = res.body.videoAbuse.id
})
})
describe('When updating a video abuse', function () {
const basePath = '/api/v1/videos/'
let path: string
before(() => {
path = basePath + server.video.id + '/abuse/' + videoAbuseId
})
it('Should fail with a non authenticated user', async function () {
await updateVideoAbuse(server.url, 'blabla', server.video.uuid, videoAbuseId, {}, 401)
})
it('Should fail with a non admin user', async function () {
await updateVideoAbuse(server.url, userAccessToken, server.video.uuid, videoAbuseId, {}, 403)
})
it('Should fail with a bad video id or bad video abuse id', async function () {
await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, 45, {}, 404)
await updateVideoAbuse(server.url, server.accessToken, 52, videoAbuseId, {}, 404)
})
it('Should fail with a bad state', async function () {
const body = { state: 5 }
await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400)
})
it('Should fail with a bad moderation comment', async function () {
const body = { moderationComment: 'b'.repeat(305) }
await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400)
})
it('Should succeed with the correct params', async function () {
const body = { state: VideoAbuseState.ACCEPTED }
await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body)
})
})
describe('When deleting a video abuse', function () {
const basePath = '/api/v1/videos/'
let path: string
before(() => {
path = basePath + server.video.id + '/abuse/' + videoAbuseId
})
it('Should fail with a non authenticated user', async function () {
await deleteVideoAbuse(server.url, 'blabla', server.video.uuid, videoAbuseId, 401)
})
it('Should fail with a non admin user', async function () {
await deleteVideoAbuse(server.url, userAccessToken, server.video.uuid, videoAbuseId, 403)
})
it('Should fail with a bad video id or bad video abuse id', async function () {
await deleteVideoAbuse(server.url, server.accessToken, server.video.uuid, 45, 404)
await deleteVideoAbuse(server.url, server.accessToken, 52, videoAbuseId, 404)
})
it('Should succeed with the correct params', async function () {
await deleteVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId)
})
}) })
after(async function () { after(async function () {

View File

@ -2,8 +2,9 @@
import * as chai from 'chai' import * as chai from 'chai'
import 'mocha' import 'mocha'
import { VideoAbuse } from '../../../../shared/models/videos' import { VideoAbuse, VideoAbuseState } from '../../../../shared/models/videos'
import { import {
deleteVideoAbuse,
flushAndRunMultipleServers, flushAndRunMultipleServers,
getVideoAbusesList, getVideoAbusesList,
getVideosList, getVideosList,
@ -11,6 +12,7 @@ import {
reportVideoAbuse, reportVideoAbuse,
ServerInfo, ServerInfo,
setAccessTokensToServers, setAccessTokensToServers,
updateVideoAbuse,
uploadVideo uploadVideo
} from '../../utils/index' } from '../../utils/index'
import { doubleFollow } from '../../utils/server/follows' import { doubleFollow } from '../../utils/server/follows'
@ -20,6 +22,7 @@ const expect = chai.expect
describe('Test video abuses', function () { describe('Test video abuses', function () {
let servers: ServerInfo[] = [] let servers: ServerInfo[] = []
let abuseServer2: VideoAbuse
before(async function () { before(async function () {
this.timeout(50000) this.timeout(50000)
@ -105,7 +108,7 @@ describe('Test video abuses', function () {
await waitJobs(servers) await waitJobs(servers)
}) })
it('Should have 2 video abuse on server 1 and 1 on server 2', async function () { it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken) const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken)
expect(res1.body.total).to.equal(2) expect(res1.body.total).to.equal(2)
expect(res1.body.data).to.be.an('array') expect(res1.body.data).to.be.an('array')
@ -116,22 +119,57 @@ describe('Test video abuses', function () {
expect(abuse1.reporterAccount.name).to.equal('root') expect(abuse1.reporterAccount.name).to.equal('root')
expect(abuse1.reporterAccount.host).to.equal('localhost:9001') expect(abuse1.reporterAccount.host).to.equal('localhost:9001')
expect(abuse1.video.id).to.equal(servers[0].video.id) expect(abuse1.video.id).to.equal(servers[0].video.id)
expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING)
expect(abuse1.state.label).to.equal('Pending')
expect(abuse1.moderationComment).to.be.null
const abuse2: VideoAbuse = res1.body.data[1] const abuse2: VideoAbuse = res1.body.data[1]
expect(abuse2.reason).to.equal('my super bad reason 2') expect(abuse2.reason).to.equal('my super bad reason 2')
expect(abuse2.reporterAccount.name).to.equal('root') expect(abuse2.reporterAccount.name).to.equal('root')
expect(abuse2.reporterAccount.host).to.equal('localhost:9001') expect(abuse2.reporterAccount.host).to.equal('localhost:9001')
expect(abuse2.video.id).to.equal(servers[1].video.id) expect(abuse2.video.id).to.equal(servers[1].video.id)
expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING)
expect(abuse2.state.label).to.equal('Pending')
expect(abuse2.moderationComment).to.be.null
const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken) const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
expect(res2.body.total).to.equal(1) expect(res2.body.total).to.equal(1)
expect(res2.body.data).to.be.an('array') expect(res2.body.data).to.be.an('array')
expect(res2.body.data.length).to.equal(1) expect(res2.body.data.length).to.equal(1)
const abuse3: VideoAbuse = res2.body.data[0] abuseServer2 = res2.body.data[0]
expect(abuse3.reason).to.equal('my super bad reason 2') expect(abuseServer2.reason).to.equal('my super bad reason 2')
expect(abuse3.reporterAccount.name).to.equal('root') expect(abuseServer2.reporterAccount.name).to.equal('root')
expect(abuse3.reporterAccount.host).to.equal('localhost:9001') expect(abuseServer2.reporterAccount.host).to.equal('localhost:9001')
expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING)
expect(abuseServer2.state.label).to.equal('Pending')
expect(abuseServer2.moderationComment).to.be.null
})
it('Should update the state of a video abuse', async function () {
const body = { state: VideoAbuseState.REJECTED }
await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED)
})
it('Should add a moderation comment', async function () {
const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' }
await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED)
expect(res.body.data[0].moderationComment).to.equal('It is valid')
})
it('Should delete the video abuse', async function () {
await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id)
const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
expect(res.body.total).to.equal(0)
expect(res.body.data).to.be.an('array')
expect(res.body.data.length).to.equal(0)
}) })
after(async function () { after(async function () {

View File

@ -1,6 +1,8 @@
import * as request from 'supertest' import * as request from 'supertest'
import { VideoAbuseUpdate } from '../../../../shared/models/videos/video-abuse-update.model'
import { makeDeleteRequest, makePutBodyRequest } from '..'
function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 204) { function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) {
const path = '/api/v1/videos/' + videoId + '/abuse' const path = '/api/v1/videos/' + videoId + '/abuse'
return request(url) return request(url)
@ -23,9 +25,41 @@ function getVideoAbusesList (url: string, token: string) {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
function updateVideoAbuse (
url: string,
token: string,
videoId: string | number,
videoAbuseId: number,
body: VideoAbuseUpdate,
statusCodeExpected = 204
) {
const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
return makePutBodyRequest({
url,
token,
path,
fields: body,
statusCodeExpected
})
}
function deleteVideoAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) {
const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
return makeDeleteRequest({
url,
token,
path,
statusCodeExpected
})
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
reportVideoAbuse, reportVideoAbuse,
getVideoAbusesList getVideoAbusesList,
updateVideoAbuse,
deleteVideoAbuse
} }

View File

@ -1,6 +1,7 @@
export * from './user-video-rate-update.model' export * from './user-video-rate-update.model'
export * from './user-video-rate.model' export * from './user-video-rate.model'
export * from './user-video-rate.type' export * from './user-video-rate.type'
export * from './video-abuse-state.model'
export * from './video-abuse-create.model' export * from './video-abuse-create.model'
export * from './video-abuse.model' export * from './video-abuse.model'
export * from './video-blacklist.model' export * from './video-blacklist.model'

View File

@ -0,0 +1,5 @@
export enum VideoAbuseState {
PENDING = 1,
REJECTED = 2,
ACCEPTED = 3
}

View File

@ -0,0 +1,6 @@
import { VideoAbuseState } from './video-abuse-state.model'
export interface VideoAbuseUpdate {
moderationComment?: string
state?: VideoAbuseState
}

View File

@ -1,14 +1,21 @@
import { Account } from '../actors' import { Account } from '../actors'
import { VideoConstant } from './video-constant.model'
import { VideoAbuseState } from './video-abuse-state.model'
export interface VideoAbuse { export interface VideoAbuse {
id: number id: number
reason: string reason: string
reporterAccount: Account reporterAccount: Account
state: VideoConstant<VideoAbuseState>
moderationComment?: string
video: { video: {
id: number id: number
name: string name: string
uuid: string uuid: string
url: string url: string
} }
createdAt: Date createdAt: Date
} }