Server: add video abuse support

This commit is contained in:
Chocobozzz 2017-01-04 20:59:23 +01:00
parent a6fd2b30bf
commit 55fa55a9be
32 changed files with 921 additions and 175 deletions

View File

@ -30,7 +30,7 @@ export class FriendListComponent implements OnInit {
private getFriends() { private getFriends() {
this.friendService.getFriends().subscribe( this.friendService.getFriends().subscribe(
friends => this.friends = friends, res => this.friends = res.friends,
err => alert(err.text) err => alert(err.text)
); );

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Friend } from './friend.model'; import { Friend } from './friend.model';
import { AuthHttp, RestExtractor } from '../../../shared'; import { AuthHttp, RestExtractor, ResultList } from '../../../shared';
@Injectable() @Injectable()
export class FriendService { export class FriendService {
@ -13,11 +13,10 @@ export class FriendService {
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
getFriends(): Observable<Friend[]> { getFriends() {
return this.authHttp.get(FriendService.BASE_FRIEND_URL) return this.authHttp.get(FriendService.BASE_FRIEND_URL)
// Not implemented as a data list by the server yet .map(this.restExtractor.extractDataList)
// .map(this.restExtractor.extractDataList) .map(this.extractFriends)
.map((res) => res.json())
.catch((res) => this.restExtractor.handleError(res)); .catch((res) => this.restExtractor.handleError(res));
} }
@ -36,4 +35,11 @@ export class FriendService {
.map(res => res.status) .map(res => res.status)
.catch((res) => this.restExtractor.handleError(res)); .catch((res) => this.restExtractor.handleError(res));
} }
private extractFriends(result: ResultList) {
const friends: Friend[] = result.data;
const totalFriends = result.total;
return { friends, totalFriends };
}
} }

View File

@ -10,8 +10,8 @@ database:
hostname: 'localhost' hostname: 'localhost'
port: 5432 port: 5432
suffix: '_dev' suffix: '_dev'
username: peertube username: 'peertube'
password: peertube password: 'peertube'
# From the project root directory # From the project root directory
storage: storage:

View File

@ -57,7 +57,8 @@ app.use(expressValidator({
customValidators.misc, customValidators.misc,
customValidators.pods, customValidators.pods,
customValidators.users, customValidators.users,
customValidators.videos customValidators.videos,
customValidators.remote.videos
) )
})) }))

View File

@ -5,6 +5,7 @@ const waterfall = require('async/waterfall')
const db = require('../../initializers/database') const db = require('../../initializers/database')
const logger = require('../../helpers/logger') const logger = require('../../helpers/logger')
const utils = require('../../helpers/utils')
const friends = require('../../lib/friends') const friends = require('../../lib/friends')
const middlewares = require('../../middlewares') const middlewares = require('../../middlewares')
const admin = middlewares.admin const admin = middlewares.admin
@ -36,7 +37,7 @@ router.get('/quitfriends',
) )
// Post because this is a secured request // Post because this is a secured request
router.post('/remove', router.post('/remove',
signatureValidator, signatureValidator.signature,
checkSignature, checkSignature,
removePods removePods
) )
@ -86,7 +87,7 @@ function listPods (req, res, next) {
db.Pod.list(function (err, podsList) { db.Pod.list(function (err, podsList) {
if (err) return next(err) if (err) return next(err)
res.json(getFormatedPods(podsList)) res.json(utils.getFormatedObjects(podsList, podsList.length))
}) })
} }
@ -130,15 +131,3 @@ function quitFriends (req, res, next) {
res.type('json').status(204).end() res.type('json').status(204).end()
}) })
} }
// ---------------------------------------------------------------------------
function getFormatedPods (pods) {
const formatedPods = []
pods.forEach(function (pod) {
formatedPods.push(pod.toFormatedJSON())
})
return formatedPods
}

View File

@ -7,15 +7,16 @@ const waterfall = require('async/waterfall')
const db = require('../../../initializers/database') const db = require('../../../initializers/database')
const middlewares = require('../../../middlewares') const middlewares = require('../../../middlewares')
const secureMiddleware = middlewares.secure const secureMiddleware = middlewares.secure
const validators = middlewares.validators.remote const videosValidators = middlewares.validators.remote.videos
const signatureValidators = middlewares.validators.remote.signature
const logger = require('../../../helpers/logger') const logger = require('../../../helpers/logger')
const router = express.Router() const router = express.Router()
router.post('/', router.post('/',
validators.signature, signatureValidators.signature,
secureMiddleware.checkSignature, secureMiddleware.checkSignature,
validators.remoteVideos, videosValidators.remoteVideos,
remoteVideos remoteVideos
) )
@ -32,19 +33,23 @@ function remoteVideos (req, res, next) {
// We need to process in the same order to keep consistency // We need to process in the same order to keep consistency
// TODO: optimization // TODO: optimization
eachSeries(requests, function (request, callbackEach) { eachSeries(requests, function (request, callbackEach) {
const videoData = request.data const data = request.data
switch (request.type) { switch (request.type) {
case 'add': case 'add':
addRemoteVideo(videoData, fromPod, callbackEach) addRemoteVideo(data, fromPod, callbackEach)
break break
case 'update': case 'update':
updateRemoteVideo(videoData, fromPod, callbackEach) updateRemoteVideo(data, fromPod, callbackEach)
break break
case 'remove': case 'remove':
removeRemoteVideo(videoData, fromPod, callbackEach) removeRemoteVideo(data, fromPod, callbackEach)
break
case 'report-abuse':
reportAbuseRemoteVideo(data, fromPod, callbackEach)
break break
default: default:
@ -164,13 +169,8 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
}, },
function findVideo (t, callback) { function findVideo (t, callback) {
db.Video.loadByHostAndRemoteId(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) { fetchVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
if (err || !videoInstance) { return callback(err, t, videoInstance)
logger.error('Cannot load video from host and remote id.', { error: err.message })
return callback(err)
}
return callback(null, t, videoInstance)
}) })
}, },
@ -225,13 +225,45 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
function removeRemoteVideo (videoToRemoveData, fromPod, callback) { function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
// We need the instance because we have to remove some other stuffs (thumbnail etc) // We need the instance because we have to remove some other stuffs (thumbnail etc)
db.Video.loadByHostAndRemoteId(fromPod.host, videoToRemoveData.remoteId, function (err, video) { fetchVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
if (err || !video) { if (err) return callback(err)
logger.error('Cannot load video from host and remote id.', { error: err.message })
return callback(err)
}
logger.debug('Removing remote video %s.', video.remoteId) logger.debug('Removing remote video %s.', video.remoteId)
video.destroy().asCallback(callback) video.destroy().asCallback(callback)
}) })
} }
function reportAbuseRemoteVideo (reportData, fromPod, callback) {
db.Video.load(reportData.videoRemoteId, function (err, video) {
if (err || !video) {
if (!err) err = new Error('video not found')
logger.error('Cannot load video from host and remote id.', { error: err })
return callback(err)
}
logger.debug('Reporting remote abuse for video %s.', video.id)
const videoAbuseData = {
reporterUsername: reportData.reporterUsername,
reason: reportData.reportReason,
reporterPodId: fromPod.id,
videoId: video.id
}
db.VideoAbuse.create(videoAbuseData).asCallback(callback)
})
}
function fetchVideo (podHost, remoteId, callback) {
db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
if (err || !video) {
if (!err) err = new Error('video not found')
logger.error('Cannot load video from host and remote id.', { error: err })
return callback(err)
}
return callback(null, video)
})
}

View File

@ -6,6 +6,7 @@ const waterfall = require('async/waterfall')
const constants = require('../../initializers/constants') const constants = require('../../initializers/constants')
const db = require('../../initializers/database') const db = require('../../initializers/database')
const logger = require('../../helpers/logger') const logger = require('../../helpers/logger')
const utils = require('../../helpers/utils')
const middlewares = require('../../middlewares') const middlewares = require('../../middlewares')
const admin = middlewares.admin const admin = middlewares.admin
const oAuth = middlewares.oauth const oAuth = middlewares.oauth
@ -82,7 +83,7 @@ function listUsers (req, res, next) {
db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
if (err) return next(err) if (err) return next(err)
res.json(getFormatedUsers(usersList, usersTotal)) res.json(utils.getFormatedObjects(usersList, usersTotal))
}) })
} }
@ -121,18 +122,3 @@ function updateUser (req, res, next) {
function success (req, res, next) { function success (req, res, next) {
res.end() res.end()
} }
// ---------------------------------------------------------------------------
function getFormatedUsers (users, usersTotal) {
const formatedUsers = []
users.forEach(function (user) {
formatedUsers.push(user.toFormatedJSON())
})
return {
total: usersTotal,
data: formatedUsers
}
}

View File

@ -11,6 +11,7 @@ const db = require('../../initializers/database')
const logger = require('../../helpers/logger') const logger = require('../../helpers/logger')
const friends = require('../../lib/friends') const friends = require('../../lib/friends')
const middlewares = require('../../middlewares') const middlewares = require('../../middlewares')
const admin = middlewares.admin
const oAuth = middlewares.oauth const oAuth = middlewares.oauth
const pagination = middlewares.pagination const pagination = middlewares.pagination
const validators = middlewares.validators const validators = middlewares.validators
@ -43,6 +44,21 @@ const storage = multer.diskStorage({
const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
router.get('/abuse',
oAuth.authenticate,
admin.ensureIsAdmin,
validatorsPagination.pagination,
validatorsSort.videoAbusesSort,
sort.setVideoAbusesSort,
pagination.setPagination,
listVideoAbuses
)
router.post('/:id/abuse',
oAuth.authenticate,
validatorsVideos.videoAbuseReport,
reportVideoAbuse
)
router.get('/', router.get('/',
validatorsPagination.pagination, validatorsPagination.pagination,
validatorsSort.videosSort, validatorsSort.videosSort,
@ -283,7 +299,7 @@ function listVideos (req, res, next) {
db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
if (err) return next(err) if (err) return next(err)
res.json(getFormatedVideos(videosList, videosTotal)) res.json(utils.getFormatedObjects(videosList, videosTotal))
}) })
} }
@ -306,22 +322,45 @@ function searchVideos (req, res, next) {
function (err, videosList, videosTotal) { function (err, videosList, videosTotal) {
if (err) return next(err) if (err) return next(err)
res.json(getFormatedVideos(videosList, videosTotal)) res.json(utils.getFormatedObjects(videosList, videosTotal))
} }
) )
} }
// --------------------------------------------------------------------------- function listVideoAbuses (req, res, next) {
db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
if (err) return next(err)
function getFormatedVideos (videos, videosTotal) { res.json(utils.getFormatedObjects(abusesList, abusesTotal))
const formatedVideos = []
videos.forEach(function (video) {
formatedVideos.push(video.toFormatedJSON())
}) })
}
return { function reportVideoAbuse (req, res, next) {
total: videosTotal, const videoInstance = res.locals.video
data: formatedVideos const reporterUsername = res.locals.oauth.token.User.username
const abuse = {
reporterUsername,
reason: req.body.reason,
videoId: videoInstance.id,
reporterPodId: null // This is our pod that reported this abuse
} }
db.VideoAbuse.create(abuse).asCallback(function (err) {
if (err) return next(err)
// We send the information to the destination pod
if (videoInstance.isOwned() === false) {
const reportData = {
reporterUsername,
reportReason: abuse.reason,
videoRemoteId: videoInstance.remoteId
} }
friends.reportAbuseVideoToFriend(reportData, videoInstance)
}
return res.type('json').status(204).end()
})
}

View File

@ -2,12 +2,14 @@
const miscValidators = require('./misc') const miscValidators = require('./misc')
const podsValidators = require('./pods') const podsValidators = require('./pods')
const remoteValidators = require('./remote')
const usersValidators = require('./users') const usersValidators = require('./users')
const videosValidators = require('./videos') const videosValidators = require('./videos')
const validators = { const validators = {
misc: miscValidators, misc: miscValidators,
pods: podsValidators, pods: podsValidators,
remote: remoteValidators,
users: usersValidators, users: usersValidators,
videos: videosValidators videos: videosValidators
} }

View File

@ -0,0 +1,11 @@
'use strict'
const remoteVideosValidators = require('./videos')
const validators = {
videos: remoteVideosValidators
}
// ---------------------------------------------------------------------------
module.exports = validators

View File

@ -0,0 +1,74 @@
'use strict'
const videosValidators = require('../videos')
const miscValidators = require('../misc')
const remoteVideosValidators = {
isEachRemoteRequestVideosValid
}
function isEachRemoteRequestVideosValid (requests) {
return miscValidators.isArray(requests) &&
requests.every(function (request) {
const video = request.data
return (
isRequestTypeAddValid(request.type) &&
videosValidators.isVideoAuthorValid(video.author) &&
videosValidators.isVideoDateValid(video.createdAt) &&
videosValidators.isVideoDateValid(video.updatedAt) &&
videosValidators.isVideoDescriptionValid(video.description) &&
videosValidators.isVideoDurationValid(video.duration) &&
videosValidators.isVideoInfoHashValid(video.infoHash) &&
videosValidators.isVideoNameValid(video.name) &&
videosValidators.isVideoTagsValid(video.tags) &&
videosValidators.isVideoThumbnailDataValid(video.thumbnailData) &&
videosValidators.isVideoRemoteIdValid(video.remoteId) &&
videosValidators.isVideoExtnameValid(video.extname)
) ||
(
isRequestTypeUpdateValid(request.type) &&
videosValidators.isVideoDateValid(video.createdAt) &&
videosValidators.isVideoDateValid(video.updatedAt) &&
videosValidators.isVideoDescriptionValid(video.description) &&
videosValidators.isVideoDurationValid(video.duration) &&
videosValidators.isVideoInfoHashValid(video.infoHash) &&
videosValidators.isVideoNameValid(video.name) &&
videosValidators.isVideoTagsValid(video.tags) &&
videosValidators.isVideoRemoteIdValid(video.remoteId) &&
videosValidators.isVideoExtnameValid(video.extname)
) ||
(
isRequestTypeRemoveValid(request.type) &&
videosValidators.isVideoNameValid(video.name) &&
videosValidators.isVideoRemoteIdValid(video.remoteId)
) ||
(
isRequestTypeReportAbuseValid(request.type) &&
videosValidators.isVideoRemoteIdValid(request.data.videoRemoteId) &&
videosValidators.isVideoAbuseReasonValid(request.data.reportReason) &&
videosValidators.isVideoAbuseReporterUsernameValid(request.data.reporterUsername)
)
})
}
// ---------------------------------------------------------------------------
module.exports = remoteVideosValidators
// ---------------------------------------------------------------------------
function isRequestTypeAddValid (value) {
return value === 'add'
}
function isRequestTypeUpdateValid (value) {
return value === 'update'
}
function isRequestTypeRemoveValid (value) {
return value === 'remove'
}
function isRequestTypeReportAbuseValid (value) {
return value === 'report-abuse'
}

View File

@ -6,9 +6,9 @@ const constants = require('../../initializers/constants')
const usersValidators = require('./users') const usersValidators = require('./users')
const miscValidators = require('./misc') const miscValidators = require('./misc')
const VIDEOS_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.VIDEOS const VIDEOS_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.VIDEOS
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = constants.CONSTRAINTS_FIELDS.VIDEO_ABUSES
const videosValidators = { const videosValidators = {
isEachRemoteVideosValid,
isVideoAuthorValid, isVideoAuthorValid,
isVideoDateValid, isVideoDateValid,
isVideoDescriptionValid, isVideoDescriptionValid,
@ -17,45 +17,11 @@ const videosValidators = {
isVideoNameValid, isVideoNameValid,
isVideoTagsValid, isVideoTagsValid,
isVideoThumbnailValid, isVideoThumbnailValid,
isVideoThumbnailDataValid isVideoThumbnailDataValid,
} isVideoExtnameValid,
isVideoRemoteIdValid,
function isEachRemoteVideosValid (requests) { isVideoAbuseReasonValid,
return miscValidators.isArray(requests) && isVideoAbuseReporterUsernameValid
requests.every(function (request) {
const video = request.data
return (
isRequestTypeAddValid(request.type) &&
isVideoAuthorValid(video.author) &&
isVideoDateValid(video.createdAt) &&
isVideoDateValid(video.updatedAt) &&
isVideoDescriptionValid(video.description) &&
isVideoDurationValid(video.duration) &&
isVideoInfoHashValid(video.infoHash) &&
isVideoNameValid(video.name) &&
isVideoTagsValid(video.tags) &&
isVideoThumbnailDataValid(video.thumbnailData) &&
isVideoRemoteIdValid(video.remoteId) &&
isVideoExtnameValid(video.extname)
) ||
(
isRequestTypeUpdateValid(request.type) &&
isVideoDateValid(video.createdAt) &&
isVideoDateValid(video.updatedAt) &&
isVideoDescriptionValid(video.description) &&
isVideoDurationValid(video.duration) &&
isVideoInfoHashValid(video.infoHash) &&
isVideoNameValid(video.name) &&
isVideoTagsValid(video.tags) &&
isVideoRemoteIdValid(video.remoteId) &&
isVideoExtnameValid(video.extname)
) ||
(
isRequestTypeRemoveValid(request.type) &&
isVideoNameValid(video.name) &&
isVideoRemoteIdValid(video.remoteId)
)
})
} }
function isVideoAuthorValid (value) { function isVideoAuthorValid (value) {
@ -107,20 +73,14 @@ function isVideoRemoteIdValid (value) {
return validator.isUUID(value, 4) return validator.isUUID(value, 4)
} }
function isVideoAbuseReasonValid (value) {
return validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
}
function isVideoAbuseReporterUsernameValid (value) {
return usersValidators.isUserUsernameValid(value)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
module.exports = videosValidators module.exports = videosValidators
// ---------------------------------------------------------------------------
function isRequestTypeAddValid (value) {
return value === 'add'
}
function isRequestTypeUpdateValid (value) {
return value === 'update'
}
function isRequestTypeRemoveValid (value) {
return value === 'remove'
}

View File

@ -8,7 +8,8 @@ const utils = {
badRequest, badRequest,
cleanForExit, cleanForExit,
generateRandomString, generateRandomString,
isTestInstance isTestInstance,
getFormatedObjects
} }
function badRequest (req, res, next) { function badRequest (req, res, next) {
@ -32,6 +33,19 @@ function isTestInstance () {
return (process.env.NODE_ENV === 'test') return (process.env.NODE_ENV === 'test')
} }
function getFormatedObjects (objects, objectsTotal) {
const formatedObjects = []
objects.forEach(function (object) {
formatedObjects.push(object.toFormatedJSON())
})
return {
total: objectsTotal,
data: formatedObjects
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
module.exports = utils module.exports = utils

View File

@ -19,6 +19,7 @@ const SEARCHABLE_COLUMNS = {
// Sortable columns per schema // Sortable columns per schema
const SORTABLE_COLUMNS = { const SORTABLE_COLUMNS = {
USERS: [ 'username', '-username', 'createdAt', '-createdAt' ], USERS: [ 'username', '-username', 'createdAt', '-createdAt' ],
VIDEO_ABUSES: [ 'createdAt', '-createdAt' ],
VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdAt', '-createdAt' ] VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdAt', '-createdAt' ]
} }
@ -65,6 +66,9 @@ const CONSTRAINTS_FIELDS = {
USERNAME: { min: 3, max: 20 }, // Length USERNAME: { min: 3, max: 20 }, // Length
PASSWORD: { min: 6, max: 255 } // Length PASSWORD: { min: 6, max: 255 } // Length
}, },
VIDEO_ABUSES: {
REASON: { min: 2, max: 300 } // Length
},
VIDEOS: { VIDEOS: {
NAME: { min: 3, max: 50 }, // Length NAME: { min: 3, max: 50 }, // Length
DESCRIPTION: { min: 3, max: 250 }, // Length DESCRIPTION: { min: 3, max: 250 }, // Length

View File

@ -15,6 +15,7 @@ const requests = require('../helpers/requests')
const friends = { const friends = {
addVideoToFriends, addVideoToFriends,
updateVideoToFriends, updateVideoToFriends,
reportAbuseVideoToFriend,
hasFriends, hasFriends,
getMyCertificate, getMyCertificate,
makeFriends, makeFriends,
@ -23,12 +24,20 @@ const friends = {
sendOwnedVideosToPod sendOwnedVideosToPod
} }
function addVideoToFriends (video) { function addVideoToFriends (videoData) {
createRequest('add', constants.REQUEST_ENDPOINTS.VIDEOS, video) createRequest('add', constants.REQUEST_ENDPOINTS.VIDEOS, videoData)
} }
function updateVideoToFriends (video) { function updateVideoToFriends (videoData) {
createRequest('update', constants.REQUEST_ENDPOINTS.VIDEOS, video) createRequest('update', constants.REQUEST_ENDPOINTS.VIDEOS, videoData)
}
function removeVideoToFriends (videoParams) {
createRequest('remove', constants.REQUEST_ENDPOINTS.VIDEOS, videoParams)
}
function reportAbuseVideoToFriend (reportData, video) {
createRequest('report-abuse', constants.REQUEST_ENDPOINTS.VIDEOS, reportData, [ video.Author.podId ])
} }
function hasFriends (callback) { function hasFriends (callback) {
@ -120,10 +129,6 @@ function quitFriends (callback) {
}) })
} }
function removeVideoToFriends (videoParams) {
createRequest('remove', constants.REQUEST_ENDPOINTS.VIDEOS, videoParams)
}
function sendOwnedVideosToPod (podId) { function sendOwnedVideosToPod (podId) {
db.Video.listOwnedAndPopulateAuthorAndTags(function (err, videosList) { db.Video.listOwnedAndPopulateAuthorAndTags(function (err, videosList) {
if (err) { if (err) {
@ -152,10 +157,10 @@ module.exports = friends
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function computeForeignPodsList (host, podsScore, callback) { function computeForeignPodsList (host, podsScore, callback) {
getForeignPodsList(host, function (err, foreignPodsList) { getForeignPodsList(host, function (err, res) {
if (err) return callback(err) if (err) return callback(err)
if (!foreignPodsList) foreignPodsList = [] const foreignPodsList = res.data
// Let's give 1 point to the pod we ask the friends list // Let's give 1 point to the pod we ask the friends list
foreignPodsList.push({ host }) foreignPodsList.push({ host })
@ -252,11 +257,11 @@ function makeRequestsToWinningPods (cert, podsList, callback) {
}) })
} }
// Wrapper that populate "to" argument with all our friends if it is not specified // Wrapper that populate "toIds" argument with all our friends if it is not specified
function createRequest (type, endpoint, data, to) { function createRequest (type, endpoint, data, toIds) {
if (to) return _createRequest(type, endpoint, data, to) if (toIds) return _createRequest(type, endpoint, data, toIds)
// If the "to" pods is not specified, we send the request to all our friends // If the "toIds" pods is not specified, we send the request to all our friends
db.Pod.listAllIds(function (err, podIds) { db.Pod.listAllIds(function (err, podIds) {
if (err) { if (err) {
logger.error('Cannot get pod ids', { error: err }) logger.error('Cannot get pod ids', { error: err })
@ -267,13 +272,13 @@ function createRequest (type, endpoint, data, to) {
}) })
} }
function _createRequest (type, endpoint, data, to) { function _createRequest (type, endpoint, data, toIds) {
const pods = [] const pods = []
// If there are no destination pods abort // If there are no destination pods abort
if (to.length === 0) return if (toIds.length === 0) return
to.forEach(function (toPod) { toIds.forEach(function (toPod) {
pods.push(db.Pod.build({ id: toPod })) pods.push(db.Pod.build({ id: toPod }))
}) })

View File

@ -2,6 +2,7 @@
const sortMiddleware = { const sortMiddleware = {
setUsersSort, setUsersSort,
setVideoAbusesSort,
setVideosSort setVideosSort
} }
@ -11,6 +12,12 @@ function setUsersSort (req, res, next) {
return next() return next()
} }
function setVideoAbusesSort (req, res, next) {
if (!req.query.sort) req.query.sort = '-createdAt'
return next()
}
function setVideosSort (req, res, next) { function setVideosSort (req, res, next) {
if (!req.query.sort) req.query.sort = '-createdAt' if (!req.query.sort) req.query.sort = '-createdAt'

View File

@ -0,0 +1,13 @@
'use strict'
const remoteSignatureValidators = require('./signature')
const remoteVideosValidators = require('./videos')
const validators = {
signature: remoteSignatureValidators,
videos: remoteVideosValidators
}
// ---------------------------------------------------------------------------
module.exports = validators

View File

@ -1,21 +1,12 @@
'use strict' 'use strict'
const checkErrors = require('./utils').checkErrors const checkErrors = require('../utils').checkErrors
const logger = require('../../helpers/logger') const logger = require('../../../helpers/logger')
const validatorsRemote = { const validatorsRemoteSignature = {
remoteVideos,
signature signature
} }
function remoteVideos (req, res, next) {
req.checkBody('data').isEachRemoteVideosValid()
logger.debug('Checking remoteVideos parameters', { parameters: req.body })
checkErrors(req, res, next)
}
function signature (req, res, next) { function signature (req, res, next) {
req.checkBody('signature.host', 'Should have a signature host').isURL() req.checkBody('signature.host', 'Should have a signature host').isURL()
req.checkBody('signature.signature', 'Should have a signature').notEmpty() req.checkBody('signature.signature', 'Should have a signature').notEmpty()
@ -27,4 +18,4 @@ function signature (req, res, next) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
module.exports = validatorsRemote module.exports = validatorsRemoteSignature

View File

@ -0,0 +1,20 @@
'use strict'
const checkErrors = require('../utils').checkErrors
const logger = require('../../../helpers/logger')
const validatorsRemoteVideos = {
remoteVideos
}
function remoteVideos (req, res, next) {
req.checkBody('data').isEachRemoteRequestVideosValid()
logger.debug('Checking remoteVideos parameters', { parameters: req.body })
checkErrors(req, res, next)
}
// ---------------------------------------------------------------------------
module.exports = validatorsRemoteVideos

View File

@ -6,29 +6,38 @@ const logger = require('../../helpers/logger')
const validatorsSort = { const validatorsSort = {
usersSort, usersSort,
videoAbusesSort,
videosSort videosSort
} }
function usersSort (req, res, next) { function usersSort (req, res, next) {
const sortableColumns = constants.SORTABLE_COLUMNS.USERS const sortableColumns = constants.SORTABLE_COLUMNS.USERS
req.checkQuery('sort', 'Should have correct sortable column').optional().isIn(sortableColumns) checkSort(req, res, next, sortableColumns)
}
logger.debug('Checking sort parameters', { parameters: req.query }) function videoAbusesSort (req, res, next) {
const sortableColumns = constants.SORTABLE_COLUMNS.VIDEO_ABUSES
checkErrors(req, res, next) checkSort(req, res, next, sortableColumns)
} }
function videosSort (req, res, next) { function videosSort (req, res, next) {
const sortableColumns = constants.SORTABLE_COLUMNS.VIDEOS const sortableColumns = constants.SORTABLE_COLUMNS.VIDEOS
checkSort(req, res, next, sortableColumns)
}
// ---------------------------------------------------------------------------
module.exports = validatorsSort
// ---------------------------------------------------------------------------
function checkSort (req, res, next, sortableColumns) {
req.checkQuery('sort', 'Should have correct sortable column').optional().isIn(sortableColumns) req.checkQuery('sort', 'Should have correct sortable column').optional().isIn(sortableColumns)
logger.debug('Checking sort parameters', { parameters: req.query }) logger.debug('Checking sort parameters', { parameters: req.query })
checkErrors(req, res, next) checkErrors(req, res, next)
} }
// ---------------------------------------------------------------------------
module.exports = validatorsSort

View File

@ -11,7 +11,9 @@ const validatorsVideos = {
videosUpdate, videosUpdate,
videosGet, videosGet,
videosRemove, videosRemove,
videosSearch videosSearch,
videoAbuseReport
} }
function videosAdd (req, res, next) { function videosAdd (req, res, next) {
@ -97,6 +99,17 @@ function videosSearch (req, res, next) {
checkErrors(req, res, next) checkErrors(req, res, next)
} }
function videoAbuseReport (req, res, next) {
req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
req.checkBody('reason', 'Should have a valid reason').isVideoAbuseReasonValid()
logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
checkErrors(req, res, function () {
checkVideoExists(req.params.id, res, next)
})
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
module.exports = validatorsVideos module.exports = validatorsVideos

View File

@ -0,0 +1,113 @@
'use strict'
const constants = require('../initializers/constants')
const modelUtils = require('./utils')
const customVideosValidators = require('../helpers/custom-validators').videos
module.exports = function (sequelize, DataTypes) {
const VideoAbuse = sequelize.define('VideoAbuse',
{
reporterUsername: {
type: DataTypes.STRING,
allowNull: false,
validate: {
reporterUsernameValid: function (value) {
const res = customVideosValidators.isVideoAbuseReporterUsernameValid(value)
if (res === false) throw new Error('Video abuse reporter username is not valid.')
}
}
},
reason: {
type: DataTypes.STRING,
allowNull: false,
validate: {
reasonValid: function (value) {
const res = customVideosValidators.isVideoAbuseReasonValid(value)
if (res === false) throw new Error('Video abuse reason is not valid.')
}
}
}
},
{
indexes: [
{
fields: [ 'videoId' ]
},
{
fields: [ 'reporterPodId' ]
}
],
classMethods: {
associate,
listForApi
},
instanceMethods: {
toFormatedJSON
}
}
)
return VideoAbuse
}
// ---------------------------------------------------------------------------
function associate (models) {
this.belongsTo(models.Pod, {
foreignKey: {
name: 'reporterPodId',
allowNull: true
},
onDelete: 'cascade'
})
this.belongsTo(models.Video, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
}
function listForApi (start, count, sort, callback) {
const query = {
offset: start,
limit: count,
order: [ modelUtils.getSort(sort) ],
include: [
{
model: this.sequelize.models.Pod,
required: false
}
]
}
return this.findAndCountAll(query).asCallback(function (err, result) {
if (err) return callback(err)
return callback(null, result.rows, result.count)
})
}
function toFormatedJSON () {
let reporterPodHost
if (this.Pod) {
reporterPodHost = this.Pod.host
} else {
// It means it's our video
reporterPodHost = constants.CONFIG.WEBSERVER.HOST
}
const json = {
id: this.id,
reporterPodHost,
reason: this.reason,
reporterUsername: this.reporterUsername,
videoId: this.videoId
}
return json
}

View File

@ -248,6 +248,14 @@ function associate (models) {
through: models.VideoTag, through: models.VideoTag,
onDelete: 'cascade' onDelete: 'cascade'
}) })
this.hasMany(models.VideoAbuse, {
foreignKey: {
name: 'videoId',
allowNull: false
},
onDelete: 'cascade'
})
} }
function generateMagnetUri () { function generateMagnetUri () {

View File

@ -6,3 +6,4 @@ require('./remotes')
require('./users') require('./users')
require('./requests') require('./requests')
require('./videos') require('./videos')
require('./video-abuses')

View File

@ -47,6 +47,10 @@ describe('Test remote videos API validators', function () {
it('Should check when removing a video') it('Should check when removing a video')
}) })
describe('When reporting abuse on a video', function () {
it('Should check when reporting a video abuse')
})
after(function (done) { after(function (done) {
process.kill(-server.app.pid) process.kill(-server.app.pid)

View File

@ -0,0 +1,180 @@
'use strict'
const request = require('supertest')
const series = require('async/series')
const loginUtils = require('../../utils/login')
const requestsUtils = require('../../utils/requests')
const serversUtils = require('../../utils/servers')
const usersUtils = require('../../utils/users')
const videosUtils = require('../../utils/videos')
describe('Test video abuses API validators', function () {
let server = null
let userAccessToken = null
// ---------------------------------------------------------------
before(function (done) {
this.timeout(20000)
series([
function (next) {
serversUtils.flushTests(next)
},
function (next) {
serversUtils.runServer(1, function (server1) {
server = server1
next()
})
},
function (next) {
loginUtils.loginAndGetAccessToken(server, function (err, token) {
if (err) throw err
server.accessToken = token
next()
})
},
function (next) {
const username = 'user1'
const password = 'my super password'
usersUtils.createUser(server.url, server.accessToken, username, password, next)
},
function (next) {
const user = {
username: 'user1',
password: 'my super password'
}
loginUtils.getUserAccessToken(server, user, function (err, accessToken) {
if (err) throw err
userAccessToken = accessToken
next()
})
},
// Upload some videos on each pods
function (next) {
const name = 'my super name for pod'
const description = 'my super description for pod'
const tags = [ 'tag' ]
const file = 'video_short2.webm'
videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, file, next)
},
function (next) {
videosUtils.getVideosList(server.url, function (err, res) {
if (err) throw err
const videos = res.body.data
server.video = videos[0]
next()
})
}
], done)
})
describe('When listing video abuses', function () {
const path = '/api/v1/videos/abuse'
it('Should fail with a bad start pagination', function (done) {
request(server.url)
.get(path)
.query({ start: 'hello' })
.set('Authorization', 'Bearer ' + server.accessToken)
.set('Accept', 'application/json')
.expect(400, done)
})
it('Should fail with a bad count pagination', function (done) {
request(server.url)
.get(path)
.query({ count: 'hello' })
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + server.accessToken)
.expect(400, done)
})
it('Should fail with an incorrect sort', function (done) {
request(server.url)
.get(path)
.query({ sort: 'hello' })
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + server.accessToken)
.expect(400, done)
})
it('Should fail with a non authenticated user', function (done) {
request(server.url)
.get(path)
.query({ sort: 'hello' })
.set('Accept', 'application/json')
.expect(401, done)
})
it('Should fail with a non admin user', function (done) {
request(server.url)
.get(path)
.query({ sort: 'hello' })
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + userAccessToken)
.expect(403, done)
})
})
describe('When reporting a video abuse', function () {
const basePath = '/api/v1/videos/'
it('Should fail with nothing', function (done) {
const path = basePath + server.video + '/abuse'
const data = {}
requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
})
it('Should fail with a wrong video', function (done) {
const wrongPath = '/api/v1/videos/blabla/abuse'
const data = {}
requestsUtils.makePostBodyRequest(server.url, wrongPath, server.accessToken, data, done)
})
it('Should fail with a non authenticated user', function (done) {
const data = {}
const path = basePath + server.video + '/abuse'
requestsUtils.makePostBodyRequest(server.url, path, 'hello', data, done, 401)
})
it('Should fail with a reason too short', function (done) {
const data = {
reason: 'h'
}
const path = basePath + server.video + '/abuse'
requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
})
it('Should fail with a reason too big', function (done) {
const data = {
reason: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' +
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' +
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' +
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
}
const path = basePath + server.video + '/abuse'
requestsUtils.makePostBodyRequest(server.url, path, server.accessToken, data, done)
})
})
after(function (done) {
process.kill(-server.app.pid)
// Keep the logs if the test failed
if (this.ok) {
serversUtils.flushTests(done)
} else {
done()
}
})
})

View File

@ -86,7 +86,7 @@ describe('Test advanced friends', function () {
getFriendsList(5, function (err, res) { getFriendsList(5, function (err, res) {
if (err) throw err if (err) throw err
expect(res.body.length).to.equal(0) expect(res.body.data.length).to.equal(0)
done() done()
}) })
@ -111,7 +111,7 @@ describe('Test advanced friends', function () {
getFriendsList(i, function (err, res) { getFriendsList(i, function (err, res) {
if (err) throw err if (err) throw err
expect(res.body.length).to.equal(0) expect(res.body.data.length).to.equal(0)
callback() callback()
}) })
@ -140,7 +140,7 @@ describe('Test advanced friends', function () {
getFriendsList(i, function (err, res) { getFriendsList(i, function (err, res) {
if (err) throw err if (err) throw err
expect(res.body.length).to.equal(3) expect(res.body.data.length).to.equal(3)
callback() callback()
}) })
@ -182,7 +182,7 @@ describe('Test advanced friends', function () {
if (err) throw err if (err) throw err
// Pod 4 didn't know pod 1 and 2 removed it // Pod 4 didn't know pod 1 and 2 removed it
expect(res.body.length).to.equal(3) expect(res.body.data.length).to.equal(3)
next() next()
}) })
}, },
@ -200,7 +200,7 @@ describe('Test advanced friends', function () {
if (err) throw err if (err) throw err
// Pod 4 should not be our friend // Pod 4 should not be our friend
const result = res.body const result = res.body.data
expect(result.length).to.equal(3) expect(result.length).to.equal(3)
for (const pod of result) { for (const pod of result) {
expect(pod.host).not.equal(servers[3].host) expect(pod.host).not.equal(servers[3].host)

View File

@ -28,7 +28,7 @@ describe('Test basic friends', function () {
podsUtils.getFriendsList(serverToTest.url, function (err, res) { podsUtils.getFriendsList(serverToTest.url, function (err, res) {
if (err) throw err if (err) throw err
const result = res.body const result = res.body.data
expect(result).to.be.an('array') expect(result).to.be.an('array')
expect(result.length).to.equal(2) expect(result.length).to.equal(2)
@ -65,7 +65,7 @@ describe('Test basic friends', function () {
podsUtils.getFriendsList(server.url, function (err, res) { podsUtils.getFriendsList(server.url, function (err, res) {
if (err) throw err if (err) throw err
const result = res.body const result = res.body.data
expect(result).to.be.an('array') expect(result).to.be.an('array')
expect(result.length).to.equal(0) expect(result.length).to.equal(0)
callback() callback()
@ -90,7 +90,7 @@ describe('Test basic friends', function () {
podsUtils.getFriendsList(servers[1].url, function (err, res) { podsUtils.getFriendsList(servers[1].url, function (err, res) {
if (err) throw err if (err) throw err
const result = res.body const result = res.body.data
expect(result).to.be.an('array') expect(result).to.be.an('array')
expect(result.length).to.equal(1) expect(result.length).to.equal(1)
@ -107,7 +107,7 @@ describe('Test basic friends', function () {
podsUtils.getFriendsList(servers[2].url, function (err, res) { podsUtils.getFriendsList(servers[2].url, function (err, res) {
if (err) throw err if (err) throw err
const result = res.body const result = res.body.data
expect(result).to.be.an('array') expect(result).to.be.an('array')
expect(result.length).to.equal(1) expect(result.length).to.equal(1)
@ -154,7 +154,7 @@ describe('Test basic friends', function () {
podsUtils.getFriendsList(servers[1].url, function (err, res) { podsUtils.getFriendsList(servers[1].url, function (err, res) {
if (err) throw err if (err) throw err
const result = res.body const result = res.body.data
expect(result).to.be.an('array') expect(result).to.be.an('array')
expect(result.length).to.equal(0) expect(result.length).to.equal(0)
@ -167,7 +167,7 @@ describe('Test basic friends', function () {
podsUtils.getFriendsList(url, function (err, res) { podsUtils.getFriendsList(url, function (err, res) {
if (err) throw err if (err) throw err
const result = res.body const result = res.body.data
expect(result).to.be.an('array') expect(result).to.be.an('array')
expect(result.length).to.equal(1) expect(result.length).to.equal(1)
expect(result[0].host).not.to.be.equal(servers[1].host) expect(result[0].host).not.to.be.equal(servers[1].host)

View File

@ -0,0 +1,191 @@
'use strict'
const chai = require('chai')
const each = require('async/each')
const expect = chai.expect
const series = require('async/series')
const loginUtils = require('../utils/login')
const podsUtils = require('../utils/pods')
const serversUtils = require('../utils/servers')
const videosUtils = require('../utils/videos')
const videoAbusesUtils = require('../utils/video-abuses')
describe('Test video abuses', function () {
let servers = []
before(function (done) {
this.timeout(30000)
series([
// Run servers
function (next) {
serversUtils.flushAndRunMultipleServers(2, function (serversRun) {
servers = serversRun
next()
})
},
// Get the access tokens
function (next) {
each(servers, function (server, callbackEach) {
loginUtils.loginAndGetAccessToken(server, function (err, accessToken) {
if (err) return callbackEach(err)
server.accessToken = accessToken
callbackEach()
})
}, next)
},
// Pod 1 make friends too
function (next) {
const server = servers[0]
podsUtils.makeFriends(server.url, server.accessToken, next)
},
// Upload some videos on each pods
function (next) {
const name = 'my super name for pod 1'
const description = 'my super description for pod 1'
const tags = [ 'tag' ]
const file = 'video_short2.webm'
videosUtils.uploadVideo(servers[0].url, servers[0].accessToken, name, description, tags, file, next)
},
function (next) {
const name = 'my super name for pod 2'
const description = 'my super description for pod 2'
const tags = [ 'tag' ]
const file = 'video_short2.webm'
videosUtils.uploadVideo(servers[1].url, servers[1].accessToken, name, description, tags, file, next)
},
// Wait videos propagation
function (next) {
setTimeout(next, 11000)
},
function (next) {
videosUtils.getVideosList(servers[0].url, function (err, res) {
if (err) throw err
const videos = res.body.data
expect(videos.length).to.equal(2)
servers[0].video = videos.find(function (video) { return video.name === 'my super name for pod 1' })
servers[1].video = videos.find(function (video) { return video.name === 'my super name for pod 2' })
next()
})
}
], done)
})
it('Should not have video abuses', function (done) {
videoAbusesUtils.getVideoAbusesList(servers[0].url, servers[0].accessToken, function (err, res) {
if (err) throw err
expect(res.body.total).to.equal(0)
expect(res.body.data).to.be.an('array')
expect(res.body.data.length).to.equal(0)
done()
})
})
it('Should report abuse on a local video', function (done) {
this.timeout(15000)
const reason = 'my super bad reason'
videoAbusesUtils.reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[0].video.id, reason, function (err) {
if (err) throw err
// We wait requests propagation, even if the pod 1 is not supposed to make a request to pod 2
setTimeout(done, 11000)
})
})
it('Should have 1 video abuses on pod 1 and 0 on pod 2', function (done) {
videoAbusesUtils.getVideoAbusesList(servers[0].url, servers[0].accessToken, function (err, res) {
if (err) throw err
expect(res.body.total).to.equal(1)
expect(res.body.data).to.be.an('array')
expect(res.body.data.length).to.equal(1)
const abuse = res.body.data[0]
expect(abuse.reason).to.equal('my super bad reason')
expect(abuse.reporterUsername).to.equal('root')
expect(abuse.reporterPodHost).to.equal('localhost:9001')
expect(abuse.videoId).to.equal(servers[0].video.id)
videoAbusesUtils.getVideoAbusesList(servers[1].url, servers[1].accessToken, function (err, res) {
if (err) throw err
expect(res.body.total).to.equal(0)
expect(res.body.data).to.be.an('array')
expect(res.body.data.length).to.equal(0)
done()
})
})
})
it('Should report abuse on a remote video', function (done) {
this.timeout(15000)
const reason = 'my super bad reason 2'
videoAbusesUtils.reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[1].video.id, reason, function (err) {
if (err) throw err
// We wait requests propagation
setTimeout(done, 11000)
})
})
it('Should have 2 video abuse on pod 1 and 1 on pod 2', function (done) {
videoAbusesUtils.getVideoAbusesList(servers[0].url, servers[0].accessToken, function (err, res) {
if (err) throw err
expect(res.body.total).to.equal(2)
expect(res.body.data).to.be.an('array')
expect(res.body.data.length).to.equal(2)
let abuse = res.body.data[0]
expect(abuse.reason).to.equal('my super bad reason')
expect(abuse.reporterUsername).to.equal('root')
expect(abuse.reporterPodHost).to.equal('localhost:9001')
expect(abuse.videoId).to.equal(servers[0].video.id)
abuse = res.body.data[1]
expect(abuse.reason).to.equal('my super bad reason 2')
expect(abuse.reporterUsername).to.equal('root')
expect(abuse.reporterPodHost).to.equal('localhost:9001')
expect(abuse.videoId).to.equal(servers[1].video.id)
videoAbusesUtils.getVideoAbusesList(servers[1].url, servers[1].accessToken, function (err, res) {
if (err) throw err
expect(res.body.total).to.equal(1)
expect(res.body.data).to.be.an('array')
expect(res.body.data.length).to.equal(1)
let abuse = res.body.data[0]
expect(abuse.reason).to.equal('my super bad reason 2')
expect(abuse.reporterUsername).to.equal('root')
expect(abuse.reporterPodHost).to.equal('localhost:9001')
done()
})
})
})
after(function (done) {
servers.forEach(function (server) {
process.kill(-server.app.pid)
})
// Keep the logs if the test failed
if (this.ok) {
serversUtils.flushTests(done)
} else {
done()
}
})
})

View File

@ -0,0 +1,73 @@
'use strict'
const request = require('supertest')
const videosUtils = {
getVideoAbusesList,
getVideoAbusesListPagination,
getVideoAbusesListSort,
reportVideoAbuse
}
// ---------------------- Export functions --------------------
function reportVideoAbuse (url, token, videoId, reason, specialStatus, end) {
if (!end) {
end = specialStatus
specialStatus = 204
}
const path = '/api/v1/videos/' + videoId + '/abuse'
request(url)
.post(path)
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token)
.send({ reason })
.expect(specialStatus)
.end(end)
}
function getVideoAbusesList (url, token, end) {
const path = '/api/v1/videos/abuse'
request(url)
.get(path)
.query({ sort: 'createdAt' })
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token)
.expect(200)
.expect('Content-Type', /json/)
.end(end)
}
function getVideoAbusesListPagination (url, token, start, count, end) {
const path = '/api/v1/videos/abuse'
request(url)
.get(path)
.query({ start: start })
.query({ count: count })
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token)
.expect(200)
.expect('Content-Type', /json/)
.end(end)
}
function getVideoAbusesListSort (url, token, sort, end) {
const path = '/api/v1/videos/abuse'
request(url)
.get(path)
.query({ sort: sort })
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token)
.expect(200)
.expect('Content-Type', /json/)
.end(end)
}
// ---------------------------------------------------------------------------
module.exports = videosUtils