From 65fcc3119c334b75dd13bcfdebf186afdc580a8f Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 15 May 2017 22:22:03 +0200 Subject: First typescript iteration --- server/controllers/api/clients.js | 41 --- server/controllers/api/clients.ts | 41 +++ server/controllers/api/config.js | 22 -- server/controllers/api/config.ts | 22 ++ server/controllers/api/index.js | 35 -- server/controllers/api/index.ts | 33 ++ server/controllers/api/pods.js | 109 ------ server/controllers/api/pods.ts | 118 +++++++ server/controllers/api/remote/index.js | 18 - server/controllers/api/remote/index.ts | 18 + server/controllers/api/remote/pods.js | 42 --- server/controllers/api/remote/pods.ts | 40 +++ server/controllers/api/remote/videos.js | 509 ---------------------------- server/controllers/api/remote/videos.ts | 521 +++++++++++++++++++++++++++++ server/controllers/api/requests.js | 55 --- server/controllers/api/requests.ts | 57 ++++ server/controllers/api/users.js | 169 ---------- server/controllers/api/users.ts | 173 ++++++++++ server/controllers/api/videos/abuse.js | 112 ------- server/controllers/api/videos/abuse.ts | 117 +++++++ server/controllers/api/videos/blacklist.js | 43 --- server/controllers/api/videos/blacklist.ts | 43 +++ server/controllers/api/videos/index.js | 404 ---------------------- server/controllers/api/videos/index.ts | 426 +++++++++++++++++++++++ server/controllers/api/videos/rate.js | 169 ---------- server/controllers/api/videos/rate.ts | 181 ++++++++++ 26 files changed, 1790 insertions(+), 1728 deletions(-) delete mode 100644 server/controllers/api/clients.js create mode 100644 server/controllers/api/clients.ts delete mode 100644 server/controllers/api/config.js create mode 100644 server/controllers/api/config.ts delete mode 100644 server/controllers/api/index.js create mode 100644 server/controllers/api/index.ts delete mode 100644 server/controllers/api/pods.js create mode 100644 server/controllers/api/pods.ts delete mode 100644 server/controllers/api/remote/index.js create mode 100644 server/controllers/api/remote/index.ts delete mode 100644 server/controllers/api/remote/pods.js create mode 100644 server/controllers/api/remote/pods.ts delete mode 100644 server/controllers/api/remote/videos.js create mode 100644 server/controllers/api/remote/videos.ts delete mode 100644 server/controllers/api/requests.js create mode 100644 server/controllers/api/requests.ts delete mode 100644 server/controllers/api/users.js create mode 100644 server/controllers/api/users.ts delete mode 100644 server/controllers/api/videos/abuse.js create mode 100644 server/controllers/api/videos/abuse.ts delete mode 100644 server/controllers/api/videos/blacklist.js create mode 100644 server/controllers/api/videos/blacklist.ts delete mode 100644 server/controllers/api/videos/index.js create mode 100644 server/controllers/api/videos/index.ts delete mode 100644 server/controllers/api/videos/rate.js create mode 100644 server/controllers/api/videos/rate.ts (limited to 'server/controllers/api') diff --git a/server/controllers/api/clients.js b/server/controllers/api/clients.js deleted file mode 100644 index cf83cb835..000000000 --- a/server/controllers/api/clients.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict' - -const express = require('express') - -const constants = require('../../initializers/constants') -const db = require('../../initializers/database') -const logger = require('../../helpers/logger') - -const router = express.Router() - -router.get('/local', getLocalClient) - -// Get the client credentials for the PeerTube front end -function getLocalClient (req, res, next) { - const serverHostname = constants.CONFIG.WEBSERVER.HOSTNAME - const serverPort = constants.CONFIG.WEBSERVER.PORT - let headerHostShouldBe = serverHostname - if (serverPort !== 80 && serverPort !== 443) { - headerHostShouldBe += ':' + serverPort - } - - // Don't make this check if this is a test instance - if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { - logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe) - return res.type('json').status(403).end() - } - - db.OAuthClient.loadFirstClient(function (err, client) { - if (err) return next(err) - if (!client) return next(new Error('No client available.')) - - res.json({ - client_id: client.clientId, - client_secret: client.clientSecret - }) - }) -} - -// --------------------------------------------------------------------------- - -module.exports = router diff --git a/server/controllers/api/clients.ts b/server/controllers/api/clients.ts new file mode 100644 index 000000000..902f62995 --- /dev/null +++ b/server/controllers/api/clients.ts @@ -0,0 +1,41 @@ +import express = require('express') + +import { CONFIG } from '../../initializers'; +import { logger } from '../../helpers' +const db = require('../../initializers/database') + +const clientsRouter = express.Router() + +clientsRouter.get('/local', getLocalClient) + +// Get the client credentials for the PeerTube front end +function getLocalClient (req, res, next) { + const serverHostname = CONFIG.WEBSERVER.HOSTNAME + const serverPort = CONFIG.WEBSERVER.PORT + let headerHostShouldBe = serverHostname + if (serverPort !== 80 && serverPort !== 443) { + headerHostShouldBe += ':' + serverPort + } + + // Don't make this check if this is a test instance + if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { + logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe) + return res.type('json').status(403).end() + } + + db.OAuthClient.loadFirstClient(function (err, client) { + if (err) return next(err) + if (!client) return next(new Error('No client available.')) + + res.json({ + client_id: client.clientId, + client_secret: client.clientSecret + }) + }) +} + +// --------------------------------------------------------------------------- + +export { + clientsRouter +} diff --git a/server/controllers/api/config.js b/server/controllers/api/config.js deleted file mode 100644 index 8154b6ad0..000000000 --- a/server/controllers/api/config.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' - -const express = require('express') - -const constants = require('../../initializers/constants') - -const router = express.Router() - -router.get('/', getConfig) - -// Get the client credentials for the PeerTube front end -function getConfig (req, res, next) { - res.json({ - signup: { - enabled: constants.CONFIG.SIGNUP.ENABLED - } - }) -} - -// --------------------------------------------------------------------------- - -module.exports = router diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts new file mode 100644 index 000000000..8f3fa2473 --- /dev/null +++ b/server/controllers/api/config.ts @@ -0,0 +1,22 @@ +import express = require('express') + +import { CONFIG } from '../../initializers'; + +const configRouter = express.Router() + +configRouter.get('/', getConfig) + +// Get the client credentials for the PeerTube front end +function getConfig (req, res, next) { + res.json({ + signup: { + enabled: CONFIG.SIGNUP.ENABLED + } + }) +} + +// --------------------------------------------------------------------------- + +export { + configRouter +} diff --git a/server/controllers/api/index.js b/server/controllers/api/index.js deleted file mode 100644 index 6edc089f4..000000000 --- a/server/controllers/api/index.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict' - -const express = require('express') - -const utils = require('../../helpers/utils') - -const router = express.Router() - -const clientsController = require('./clients') -const configController = require('./config') -const podsController = require('./pods') -const remoteController = require('./remote') -const requestsController = require('./requests') -const usersController = require('./users') -const videosController = require('./videos') - -router.use('/clients', clientsController) -router.use('/config', configController) -router.use('/pods', podsController) -router.use('/remote', remoteController) -router.use('/requests', requestsController) -router.use('/users', usersController) -router.use('/videos', videosController) -router.use('/ping', pong) -router.use('/*', utils.badRequest) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function pong (req, res, next) { - return res.send('pong').status(200).end() -} diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts new file mode 100644 index 000000000..18bef2d3d --- /dev/null +++ b/server/controllers/api/index.ts @@ -0,0 +1,33 @@ +import express = require('express') + +import { badRequest } from '../../helpers' + +import { clientsRouter } from './clients' +import { configRouter } from './config' +import { podsRouter } from './pods' +import { remoteRouter } from './remote' +import { requestsRouter } from './requests' +import { usersRouter } from './users' +import { videosRouter } from './videos' + +const apiRouter = express.Router() + +apiRouter.use('/clients', clientsRouter) +apiRouter.use('/config', configRouter) +apiRouter.use('/pods', podsRouter) +apiRouter.use('/remote', remoteRouter) +apiRouter.use('/requests', requestsRouter) +apiRouter.use('/users', usersRouter) +apiRouter.use('/videos', videosRouter) +apiRouter.use('/ping', pong) +apiRouter.use('/*', badRequest) + +// --------------------------------------------------------------------------- + +export { apiRouter } + +// --------------------------------------------------------------------------- + +function pong (req, res, next) { + return res.send('pong').status(200).end() +} diff --git a/server/controllers/api/pods.js b/server/controllers/api/pods.js deleted file mode 100644 index ab5763cf6..000000000 --- a/server/controllers/api/pods.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict' - -const express = require('express') -const waterfall = require('async/waterfall') - -const db = require('../../initializers/database') -const constants = require('../../initializers/constants') -const logger = require('../../helpers/logger') -const peertubeCrypto = require('../../helpers/peertube-crypto') -const utils = require('../../helpers/utils') -const friends = require('../../lib/friends') -const middlewares = require('../../middlewares') -const admin = middlewares.admin -const oAuth = middlewares.oauth -const podsMiddleware = middlewares.pods -const validators = middlewares.validators.pods - -const router = express.Router() - -router.get('/', listPods) -router.post('/', - podsMiddleware.setBodyHostPort, // We need to modify the host before running the validator! - validators.podsAdd, - addPods -) -router.post('/makefriends', - oAuth.authenticate, - admin.ensureIsAdmin, - validators.makeFriends, - podsMiddleware.setBodyHostsPort, - makeFriends -) -router.get('/quitfriends', - oAuth.authenticate, - admin.ensureIsAdmin, - quitFriends -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function addPods (req, res, next) { - const informations = req.body - - waterfall([ - function addPod (callback) { - const pod = db.Pod.build(informations) - pod.save().asCallback(function (err, podCreated) { - // Be sure about the number of parameters for the callback - return callback(err, podCreated) - }) - }, - - function sendMyVideos (podCreated, callback) { - friends.sendOwnedVideosToPod(podCreated.id) - - callback(null) - }, - - function fetchMyCertificate (callback) { - peertubeCrypto.getMyPublicCert(function (err, cert) { - if (err) { - logger.error('Cannot read cert file.') - return callback(err) - } - - return callback(null, cert) - }) - } - ], function (err, cert) { - if (err) return next(err) - - return res.json({ cert: cert, email: constants.CONFIG.ADMIN.EMAIL }) - }) -} - -function listPods (req, res, next) { - db.Pod.list(function (err, podsList) { - if (err) return next(err) - - res.json(utils.getFormatedObjects(podsList, podsList.length)) - }) -} - -function makeFriends (req, res, next) { - const hosts = req.body.hosts - - friends.makeFriends(hosts, function (err) { - if (err) { - logger.error('Could not make friends.', { error: err }) - return - } - - logger.info('Made friends!') - }) - - res.type('json').status(204).end() -} - -function quitFriends (req, res, next) { - friends.quitFriends(function (err) { - if (err) return next(err) - - res.type('json').status(204).end() - }) -} diff --git a/server/controllers/api/pods.ts b/server/controllers/api/pods.ts new file mode 100644 index 000000000..06dfd8295 --- /dev/null +++ b/server/controllers/api/pods.ts @@ -0,0 +1,118 @@ +import express = require('express') +import { waterfall } from 'async' + +const db = require('../../initializers/database') +import { CONFIG } from '../../initializers' +import { + logger, + getMyPublicCert, + getFormatedObjects +} from '../../helpers' +import { + sendOwnedVideosToPod, + makeFriends, + quitFriends +} from '../../lib' +import { + podsAddValidator, + authenticate, + ensureIsAdmin, + makeFriendsValidator, + setBodyHostPort, + setBodyHostsPort +} from '../../middlewares' + +const podsRouter = express.Router() + +podsRouter.get('/', listPods) +podsRouter.post('/', + setBodyHostPort, // We need to modify the host before running the validator! + podsAddValidator, + addPods +) +podsRouter.post('/makefriends', + authenticate, + ensureIsAdmin, + makeFriendsValidator, + setBodyHostsPort, + makeFriends +) +podsRouter.get('/quitfriends', + authenticate, + ensureIsAdmin, + quitFriends +) + +// --------------------------------------------------------------------------- + +export { + podsRouter +} + +// --------------------------------------------------------------------------- + +function addPods (req, res, next) { + const informations = req.body + + waterfall([ + function addPod (callback) { + const pod = db.Pod.build(informations) + pod.save().asCallback(function (err, podCreated) { + // Be sure about the number of parameters for the callback + return callback(err, podCreated) + }) + }, + + function sendMyVideos (podCreated, callback) { + sendOwnedVideosToPod(podCreated.id) + + callback(null) + }, + + function fetchMyCertificate (callback) { + getMyPublicCert(function (err, cert) { + if (err) { + logger.error('Cannot read cert file.') + return callback(err) + } + + return callback(null, cert) + }) + } + ], function (err, cert) { + if (err) return next(err) + + return res.json({ cert: cert, email: CONFIG.ADMIN.EMAIL }) + }) +} + +function listPods (req, res, next) { + db.Pod.list(function (err, podsList) { + if (err) return next(err) + + res.json(getFormatedObjects(podsList, podsList.length)) + }) +} + +function makeFriendsController (req, res, next) { + const hosts = req.body.hosts + + makeFriends(hosts, function (err) { + if (err) { + logger.error('Could not make friends.', { error: err }) + return + } + + logger.info('Made friends!') + }) + + res.type('json').status(204).end() +} + +function quitFriendsController (req, res, next) { + quitFriends(function (err) { + if (err) return next(err) + + res.type('json').status(204).end() + }) +} diff --git a/server/controllers/api/remote/index.js b/server/controllers/api/remote/index.js deleted file mode 100644 index 6106850ab..000000000 --- a/server/controllers/api/remote/index.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' - -const express = require('express') - -const utils = require('../../../helpers/utils') - -const router = express.Router() - -const podsRemoteController = require('./pods') -const videosRemoteController = require('./videos') - -router.use('/pods', podsRemoteController) -router.use('/videos', videosRemoteController) -router.use('/*', utils.badRequest) - -// --------------------------------------------------------------------------- - -module.exports = router diff --git a/server/controllers/api/remote/index.ts b/server/controllers/api/remote/index.ts new file mode 100644 index 000000000..b11439204 --- /dev/null +++ b/server/controllers/api/remote/index.ts @@ -0,0 +1,18 @@ +import express = require('express') + +import { badRequest } from '../../../helpers' + +import { remotePodsRouter } from './pods' +import { remoteVideosRouter } from './videos' + +const remoteRouter = express.Router() + +remoteRouter.use('/pods', remotePodsRouter) +remoteRouter.use('/videos', remoteVideosRouter) +remoteRouter.use('/*', badRequest) + +// --------------------------------------------------------------------------- + +export { + remoteRouter +} diff --git a/server/controllers/api/remote/pods.js b/server/controllers/api/remote/pods.js deleted file mode 100644 index 0343bc62e..000000000 --- a/server/controllers/api/remote/pods.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' - -const express = require('express') -const waterfall = require('async/waterfall') - -const db = require('../../../initializers/database') -const middlewares = require('../../../middlewares') -const checkSignature = middlewares.secure.checkSignature -const signatureValidator = middlewares.validators.remote.signature - -const router = express.Router() - -// Post because this is a secured request -router.post('/remove', - signatureValidator.signature, - checkSignature, - removePods -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function removePods (req, res, next) { - const host = req.body.signature.host - - waterfall([ - function loadPod (callback) { - db.Pod.loadByHost(host, callback) - }, - - function deletePod (pod, callback) { - pod.destroy().asCallback(callback) - } - ], function (err) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) -} diff --git a/server/controllers/api/remote/pods.ts b/server/controllers/api/remote/pods.ts new file mode 100644 index 000000000..85ef7bb42 --- /dev/null +++ b/server/controllers/api/remote/pods.ts @@ -0,0 +1,40 @@ +import express = require('express') +import { waterfall } from 'async/waterfall' + +const db = require('../../../initializers/database') +import { checkSignature, signatureValidator } from '../../../middlewares' + +const remotePodsRouter = express.Router() + +// Post because this is a secured request +remotePodsRouter.post('/remove', + signatureValidator, + checkSignature, + removePods +) + +// --------------------------------------------------------------------------- + +export { + remotePodsRouter +} + +// --------------------------------------------------------------------------- + +function removePods (req, res, next) { + const host = req.body.signature.host + + waterfall([ + function loadPod (callback) { + db.Pod.loadByHost(host, callback) + }, + + function deletePod (pod, callback) { + pod.destroy().asCallback(callback) + } + ], function (err) { + if (err) return next(err) + + return res.type('json').status(204).end() + }) +} diff --git a/server/controllers/api/remote/videos.js b/server/controllers/api/remote/videos.js deleted file mode 100644 index e54793628..000000000 --- a/server/controllers/api/remote/videos.js +++ /dev/null @@ -1,509 +0,0 @@ -'use strict' - -const eachSeries = require('async/eachSeries') -const express = require('express') -const waterfall = require('async/waterfall') - -const db = require('../../../initializers/database') -const constants = require('../../../initializers/constants') -const middlewares = require('../../../middlewares') -const secureMiddleware = middlewares.secure -const videosValidators = middlewares.validators.remote.videos -const signatureValidators = middlewares.validators.remote.signature -const logger = require('../../../helpers/logger') -const friends = require('../../../lib/friends') -const databaseUtils = require('../../../helpers/database-utils') - -const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS] - -// Functions to call when processing a remote request -const functionsHash = {} -functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper -functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper -functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo -functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo - -const router = express.Router() - -router.post('/', - signatureValidators.signature, - secureMiddleware.checkSignature, - videosValidators.remoteVideos, - remoteVideos -) - -router.post('/qadu', - signatureValidators.signature, - secureMiddleware.checkSignature, - videosValidators.remoteQaduVideos, - remoteVideosQadu -) - -router.post('/events', - signatureValidators.signature, - secureMiddleware.checkSignature, - videosValidators.remoteEventsVideos, - remoteVideosEvents -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function remoteVideos (req, res, next) { - const requests = req.body.data - const fromPod = res.locals.secure.pod - - // We need to process in the same order to keep consistency - // TODO: optimization - eachSeries(requests, function (request, callbackEach) { - const data = request.data - - // Get the function we need to call in order to process the request - const fun = functionsHash[request.type] - if (fun === undefined) { - logger.error('Unkown remote request type %s.', request.type) - return callbackEach(null) - } - - fun.call(this, data, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) - }) - - // We don't need to keep the other pod waiting - return res.type('json').status(204).end() -} - -function remoteVideosQadu (req, res, next) { - const requests = req.body.data - const fromPod = res.locals.secure.pod - - eachSeries(requests, function (request, callbackEach) { - const videoData = request.data - - quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) - }) - - return res.type('json').status(204).end() -} - -function remoteVideosEvents (req, res, next) { - const requests = req.body.data - const fromPod = res.locals.secure.pod - - eachSeries(requests, function (request, callbackEach) { - const eventData = request.data - - processVideosEventsRetryWrapper(eventData, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) - }) - - return res.type('json').status(204).end() -} - -function processVideosEventsRetryWrapper (eventData, fromPod, finalCallback) { - const options = { - arguments: [ eventData, fromPod ], - errorMessage: 'Cannot process videos events with many retries.' - } - - databaseUtils.retryTransactionWrapper(processVideosEvents, options, finalCallback) -} - -function processVideosEvents (eventData, fromPod, finalCallback) { - waterfall([ - databaseUtils.startSerializableTransaction, - - function findVideo (t, callback) { - fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) { - return callback(err, t, videoInstance) - }) - }, - - function updateVideoIntoDB (t, videoInstance, callback) { - const options = { transaction: t } - - let columnToUpdate - let qaduType - - switch (eventData.eventType) { - case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS: - columnToUpdate = 'views' - qaduType = constants.REQUEST_VIDEO_QADU_TYPES.VIEWS - break - - case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES: - columnToUpdate = 'likes' - qaduType = constants.REQUEST_VIDEO_QADU_TYPES.LIKES - break - - case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES: - columnToUpdate = 'dislikes' - qaduType = constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES - break - - default: - return callback(new Error('Unknown video event type.')) - } - - const query = {} - query[columnToUpdate] = eventData.count - - videoInstance.increment(query, options).asCallback(function (err) { - return callback(err, t, videoInstance, qaduType) - }) - }, - - function sendQaduToFriends (t, videoInstance, qaduType, callback) { - const qadusParams = [ - { - videoId: videoInstance.id, - type: qaduType - } - ] - - friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { - return callback(err, t) - }) - }, - - databaseUtils.commitTransaction - - ], function (err, t) { - if (err) { - logger.debug('Cannot process a video event.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Remote video event processed for video %s.', eventData.remoteId) - return finalCallback(null) - }) -} - -function quickAndDirtyUpdateVideoRetryWrapper (videoData, fromPod, finalCallback) { - const options = { - arguments: [ videoData, fromPod ], - errorMessage: 'Cannot update quick and dirty the remote video with many retries.' - } - - databaseUtils.retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback) -} - -function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) { - let videoName - - waterfall([ - databaseUtils.startSerializableTransaction, - - function findVideo (t, callback) { - fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) { - return callback(err, t, videoInstance) - }) - }, - - function updateVideoIntoDB (t, videoInstance, callback) { - const options = { transaction: t } - - videoName = videoInstance.name - - if (videoData.views) { - videoInstance.set('views', videoData.views) - } - - if (videoData.likes) { - videoInstance.set('likes', videoData.likes) - } - - if (videoData.dislikes) { - videoInstance.set('dislikes', videoData.dislikes) - } - - videoInstance.save(options).asCallback(function (err) { - return callback(err, t) - }) - }, - - databaseUtils.commitTransaction - - ], function (err, t) { - if (err) { - logger.debug('Cannot quick and dirty update the remote video.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Remote video %s quick and dirty updated', videoName) - return finalCallback(null) - }) -} - -// Handle retries on fail -function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) { - const options = { - arguments: [ videoToCreateData, fromPod ], - errorMessage: 'Cannot insert the remote video with many retries.' - } - - databaseUtils.retryTransactionWrapper(addRemoteVideo, options, finalCallback) -} - -function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { - logger.debug('Adding remote video "%s".', videoToCreateData.remoteId) - - waterfall([ - - databaseUtils.startSerializableTransaction, - - function assertRemoteIdAndHostUnique (t, callback) { - db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) { - if (err) return callback(err) - - if (video) return callback(new Error('RemoteId and host pair is not unique.')) - - return callback(null, t) - }) - }, - - function findOrCreateAuthor (t, callback) { - const name = videoToCreateData.author - const podId = fromPod.id - // This author is from another pod so we do not associate a user - const userId = null - - db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { - return callback(err, t, authorInstance) - }) - }, - - function findOrCreateTags (t, author, callback) { - const tags = videoToCreateData.tags - - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, author, tagInstances) - }) - }, - - function createVideoObject (t, author, tagInstances, callback) { - const videoData = { - name: videoToCreateData.name, - remoteId: videoToCreateData.remoteId, - extname: videoToCreateData.extname, - infoHash: videoToCreateData.infoHash, - category: videoToCreateData.category, - licence: videoToCreateData.licence, - language: videoToCreateData.language, - nsfw: videoToCreateData.nsfw, - description: videoToCreateData.description, - authorId: author.id, - duration: videoToCreateData.duration, - createdAt: videoToCreateData.createdAt, - // FIXME: updatedAt does not seems to be considered by Sequelize - updatedAt: videoToCreateData.updatedAt, - views: videoToCreateData.views, - likes: videoToCreateData.likes, - dislikes: videoToCreateData.dislikes - } - - const video = db.Video.build(videoData) - - return callback(null, t, tagInstances, video) - }, - - function generateThumbnail (t, tagInstances, video, callback) { - db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) { - if (err) { - logger.error('Cannot generate thumbnail from data.', { error: err }) - return callback(err) - } - - return callback(err, t, tagInstances, video) - }) - }, - - function insertVideoIntoDB (t, tagInstances, video, callback) { - const options = { - transaction: t - } - - video.save(options).asCallback(function (err, videoCreated) { - return callback(err, t, tagInstances, videoCreated) - }) - }, - - function associateTagsToVideo (t, tagInstances, video, callback) { - const options = { - transaction: t - } - - video.setTags(tagInstances, options).asCallback(function (err) { - return callback(err, t) - }) - }, - - databaseUtils.commitTransaction - - ], function (err, t) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot insert the remote video.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Remote video %s inserted.', videoToCreateData.name) - return finalCallback(null) - }) -} - -// Handle retries on fail -function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) { - const options = { - arguments: [ videoAttributesToUpdate, fromPod ], - errorMessage: 'Cannot update the remote video with many retries' - } - - databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback) -} - -function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) { - logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId) - - waterfall([ - - databaseUtils.startSerializableTransaction, - - function findVideo (t, callback) { - fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) { - return callback(err, t, videoInstance) - }) - }, - - function findOrCreateTags (t, videoInstance, callback) { - const tags = videoAttributesToUpdate.tags - - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, videoInstance, tagInstances) - }) - }, - - function updateVideoIntoDB (t, videoInstance, tagInstances, callback) { - const options = { transaction: t } - - videoInstance.set('name', videoAttributesToUpdate.name) - videoInstance.set('category', videoAttributesToUpdate.category) - videoInstance.set('licence', videoAttributesToUpdate.licence) - videoInstance.set('language', videoAttributesToUpdate.language) - videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) - videoInstance.set('description', videoAttributesToUpdate.description) - videoInstance.set('infoHash', videoAttributesToUpdate.infoHash) - videoInstance.set('duration', videoAttributesToUpdate.duration) - videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) - videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) - videoInstance.set('extname', videoAttributesToUpdate.extname) - videoInstance.set('views', videoAttributesToUpdate.views) - videoInstance.set('likes', videoAttributesToUpdate.likes) - videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) - - videoInstance.save(options).asCallback(function (err) { - return callback(err, t, videoInstance, tagInstances) - }) - }, - - function associateTagsToVideo (t, videoInstance, tagInstances, callback) { - const options = { transaction: t } - - videoInstance.setTags(tagInstances, options).asCallback(function (err) { - return callback(err, t) - }) - }, - - databaseUtils.commitTransaction - - ], function (err, t) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot update the remote video.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Remote video %s updated', videoAttributesToUpdate.name) - return finalCallback(null) - }) -} - -function removeRemoteVideo (videoToRemoveData, fromPod, callback) { - // We need the instance because we have to remove some other stuffs (thumbnail etc) - fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) { - // Do not return the error, continue the process - if (err) return callback(null) - - logger.debug('Removing remote video %s.', video.remoteId) - video.destroy().asCallback(function (err) { - // Do not return the error, continue the process - if (err) { - logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err }) - } - - return callback(null) - }) - }) -} - -function reportAbuseRemoteVideo (reportData, fromPod, callback) { - fetchOwnedVideo(reportData.videoRemoteId, function (err, video) { - if (err || !video) { - if (!err) err = new Error('video not found') - - logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId }) - // Do not return the error, continue the process - return callback(null) - } - - 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(function (err) { - if (err) { - logger.error('Cannot create remote abuse video.', { error: err }) - } - - return callback(null) - }) - }) -} - -function fetchOwnedVideo (id, callback) { - db.Video.load(id, function (err, video) { - if (err || !video) { - if (!err) err = new Error('video not found') - - logger.error('Cannot load owned video from id.', { error: err, id }) - return callback(err) - } - - return callback(null, video) - }) -} - -function fetchRemoteVideo (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, podHost, remoteId }) - return callback(err) - } - - return callback(null, video) - }) -} diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts new file mode 100644 index 000000000..df4ba8309 --- /dev/null +++ b/server/controllers/api/remote/videos.ts @@ -0,0 +1,521 @@ +import express = require('express') +import { eachSeries, waterfall } from 'async' + +const db = require('../../../initializers/database') +import { + REQUEST_ENDPOINT_ACTIONS, + REQUEST_ENDPOINTS, + REQUEST_VIDEO_EVENT_TYPES, + REQUEST_VIDEO_QADU_TYPES +} from '../../../initializers' +import { + checkSignature, + signatureValidator, + remoteVideosValidator, + remoteQaduVideosValidator, + remoteEventsVideosValidator +} from '../../../middlewares' +import { + logger, + commitTransaction, + retryTransactionWrapper, + rollbackTransaction, + startSerializableTransaction +} from '../../../helpers' +import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib' + +const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] + +// Functions to call when processing a remote request +const functionsHash = {} +functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo +functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo + +const remoteVideosRouter = express.Router() + +remoteVideosRouter.post('/', + signatureValidator, + checkSignature, + remoteVideosValidator, + remoteVideos +) + +remoteVideosRouter.post('/qadu', + signatureValidator, + checkSignature, + remoteQaduVideosValidator, + remoteVideosQadu +) + +remoteVideosRouter.post('/events', + signatureValidator, + checkSignature, + remoteEventsVideosValidator, + remoteVideosEvents +) + +// --------------------------------------------------------------------------- + +export { + remoteVideosRouter +} + +// --------------------------------------------------------------------------- + +function remoteVideos (req, res, next) { + const requests = req.body.data + const fromPod = res.locals.secure.pod + + // We need to process in the same order to keep consistency + // TODO: optimization + eachSeries(requests, function (request: any, callbackEach) { + const data = request.data + + // Get the function we need to call in order to process the request + const fun = functionsHash[request.type] + if (fun === undefined) { + logger.error('Unkown remote request type %s.', request.type) + return callbackEach(null) + } + + fun.call(this, data, fromPod, callbackEach) + }, function (err) { + if (err) logger.error('Error managing remote videos.', { error: err }) + }) + + // We don't need to keep the other pod waiting + return res.type('json').status(204).end() +} + +function remoteVideosQadu (req, res, next) { + const requests = req.body.data + const fromPod = res.locals.secure.pod + + eachSeries(requests, function (request: any, callbackEach) { + const videoData = request.data + + quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach) + }, function (err) { + if (err) logger.error('Error managing remote videos.', { error: err }) + }) + + return res.type('json').status(204).end() +} + +function remoteVideosEvents (req, res, next) { + const requests = req.body.data + const fromPod = res.locals.secure.pod + + eachSeries(requests, function (request: any, callbackEach) { + const eventData = request.data + + processVideosEventsRetryWrapper(eventData, fromPod, callbackEach) + }, function (err) { + if (err) logger.error('Error managing remote videos.', { error: err }) + }) + + return res.type('json').status(204).end() +} + +function processVideosEventsRetryWrapper (eventData, fromPod, finalCallback) { + const options = { + arguments: [ eventData, fromPod ], + errorMessage: 'Cannot process videos events with many retries.' + } + + retryTransactionWrapper(processVideosEvents, options, finalCallback) +} + +function processVideosEvents (eventData, fromPod, finalCallback) { + waterfall([ + startSerializableTransaction, + + function findVideo (t, callback) { + fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) { + return callback(err, t, videoInstance) + }) + }, + + function updateVideoIntoDB (t, videoInstance, callback) { + const options = { transaction: t } + + let columnToUpdate + let qaduType + + switch (eventData.eventType) { + case REQUEST_VIDEO_EVENT_TYPES.VIEWS: + columnToUpdate = 'views' + qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS + break + + case REQUEST_VIDEO_EVENT_TYPES.LIKES: + columnToUpdate = 'likes' + qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES + break + + case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: + columnToUpdate = 'dislikes' + qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES + break + + default: + return callback(new Error('Unknown video event type.')) + } + + const query = {} + query[columnToUpdate] = eventData.count + + videoInstance.increment(query, options).asCallback(function (err) { + return callback(err, t, videoInstance, qaduType) + }) + }, + + function sendQaduToFriends (t, videoInstance, qaduType, callback) { + const qadusParams = [ + { + videoId: videoInstance.id, + type: qaduType + } + ] + + quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { + return callback(err, t) + }) + }, + + commitTransaction + + ], function (err, t) { + if (err) { + logger.debug('Cannot process a video event.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Remote video event processed for video %s.', eventData.remoteId) + return finalCallback(null) + }) +} + +function quickAndDirtyUpdateVideoRetryWrapper (videoData, fromPod, finalCallback) { + const options = { + arguments: [ videoData, fromPod ], + errorMessage: 'Cannot update quick and dirty the remote video with many retries.' + } + + retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback) +} + +function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) { + let videoName + + waterfall([ + startSerializableTransaction, + + function findVideo (t, callback) { + fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) { + return callback(err, t, videoInstance) + }) + }, + + function updateVideoIntoDB (t, videoInstance, callback) { + const options = { transaction: t } + + videoName = videoInstance.name + + if (videoData.views) { + videoInstance.set('views', videoData.views) + } + + if (videoData.likes) { + videoInstance.set('likes', videoData.likes) + } + + if (videoData.dislikes) { + videoInstance.set('dislikes', videoData.dislikes) + } + + videoInstance.save(options).asCallback(function (err) { + return callback(err, t) + }) + }, + + commitTransaction + + ], function (err, t) { + if (err) { + logger.debug('Cannot quick and dirty update the remote video.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Remote video %s quick and dirty updated', videoName) + return finalCallback(null) + }) +} + +// Handle retries on fail +function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) { + const options = { + arguments: [ videoToCreateData, fromPod ], + errorMessage: 'Cannot insert the remote video with many retries.' + } + + retryTransactionWrapper(addRemoteVideo, options, finalCallback) +} + +function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { + logger.debug('Adding remote video "%s".', videoToCreateData.remoteId) + + waterfall([ + + startSerializableTransaction, + + function assertRemoteIdAndHostUnique (t, callback) { + db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) { + if (err) return callback(err) + + if (video) return callback(new Error('RemoteId and host pair is not unique.')) + + return callback(null, t) + }) + }, + + function findOrCreateAuthor (t, callback) { + const name = videoToCreateData.author + const podId = fromPod.id + // This author is from another pod so we do not associate a user + const userId = null + + db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { + return callback(err, t, authorInstance) + }) + }, + + function findOrCreateTags (t, author, callback) { + const tags = videoToCreateData.tags + + db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { + return callback(err, t, author, tagInstances) + }) + }, + + function createVideoObject (t, author, tagInstances, callback) { + const videoData = { + name: videoToCreateData.name, + remoteId: videoToCreateData.remoteId, + extname: videoToCreateData.extname, + infoHash: videoToCreateData.infoHash, + category: videoToCreateData.category, + licence: videoToCreateData.licence, + language: videoToCreateData.language, + nsfw: videoToCreateData.nsfw, + description: videoToCreateData.description, + authorId: author.id, + duration: videoToCreateData.duration, + createdAt: videoToCreateData.createdAt, + // FIXME: updatedAt does not seems to be considered by Sequelize + updatedAt: videoToCreateData.updatedAt, + views: videoToCreateData.views, + likes: videoToCreateData.likes, + dislikes: videoToCreateData.dislikes + } + + const video = db.Video.build(videoData) + + return callback(null, t, tagInstances, video) + }, + + function generateThumbnail (t, tagInstances, video, callback) { + db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) { + if (err) { + logger.error('Cannot generate thumbnail from data.', { error: err }) + return callback(err) + } + + return callback(err, t, tagInstances, video) + }) + }, + + function insertVideoIntoDB (t, tagInstances, video, callback) { + const options = { + transaction: t + } + + video.save(options).asCallback(function (err, videoCreated) { + return callback(err, t, tagInstances, videoCreated) + }) + }, + + function associateTagsToVideo (t, tagInstances, video, callback) { + const options = { + transaction: t + } + + video.setTags(tagInstances, options).asCallback(function (err) { + return callback(err, t) + }) + }, + + commitTransaction + + ], function (err, t) { + if (err) { + // This is just a debug because we will retry the insert + logger.debug('Cannot insert the remote video.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Remote video %s inserted.', videoToCreateData.name) + return finalCallback(null) + }) +} + +// Handle retries on fail +function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) { + const options = { + arguments: [ videoAttributesToUpdate, fromPod ], + errorMessage: 'Cannot update the remote video with many retries' + } + + retryTransactionWrapper(updateRemoteVideo, options, finalCallback) +} + +function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) { + logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId) + + waterfall([ + + startSerializableTransaction, + + function findVideo (t, callback) { + fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) { + return callback(err, t, videoInstance) + }) + }, + + function findOrCreateTags (t, videoInstance, callback) { + const tags = videoAttributesToUpdate.tags + + db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { + return callback(err, t, videoInstance, tagInstances) + }) + }, + + function updateVideoIntoDB (t, videoInstance, tagInstances, callback) { + const options = { transaction: t } + + videoInstance.set('name', videoAttributesToUpdate.name) + videoInstance.set('category', videoAttributesToUpdate.category) + videoInstance.set('licence', videoAttributesToUpdate.licence) + videoInstance.set('language', videoAttributesToUpdate.language) + videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) + videoInstance.set('description', videoAttributesToUpdate.description) + videoInstance.set('infoHash', videoAttributesToUpdate.infoHash) + videoInstance.set('duration', videoAttributesToUpdate.duration) + videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) + videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) + videoInstance.set('extname', videoAttributesToUpdate.extname) + videoInstance.set('views', videoAttributesToUpdate.views) + videoInstance.set('likes', videoAttributesToUpdate.likes) + videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) + + videoInstance.save(options).asCallback(function (err) { + return callback(err, t, videoInstance, tagInstances) + }) + }, + + function associateTagsToVideo (t, videoInstance, tagInstances, callback) { + const options = { transaction: t } + + videoInstance.setTags(tagInstances, options).asCallback(function (err) { + return callback(err, t) + }) + }, + + commitTransaction + + ], function (err, t) { + if (err) { + // This is just a debug because we will retry the insert + logger.debug('Cannot update the remote video.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Remote video %s updated', videoAttributesToUpdate.name) + return finalCallback(null) + }) +} + +function removeRemoteVideo (videoToRemoveData, fromPod, callback) { + // We need the instance because we have to remove some other stuffs (thumbnail etc) + fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) { + // Do not return the error, continue the process + if (err) return callback(null) + + logger.debug('Removing remote video %s.', video.remoteId) + video.destroy().asCallback(function (err) { + // Do not return the error, continue the process + if (err) { + logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err }) + } + + return callback(null) + }) + }) +} + +function reportAbuseRemoteVideo (reportData, fromPod, callback) { + fetchOwnedVideo(reportData.videoRemoteId, function (err, video) { + if (err || !video) { + if (!err) err = new Error('video not found') + + logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId }) + // Do not return the error, continue the process + return callback(null) + } + + 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(function (err) { + if (err) { + logger.error('Cannot create remote abuse video.', { error: err }) + } + + return callback(null) + }) + }) +} + +function fetchOwnedVideo (id, callback) { + db.Video.load(id, function (err, video) { + if (err || !video) { + if (!err) err = new Error('video not found') + + logger.error('Cannot load owned video from id.', { error: err, id }) + return callback(err) + } + + return callback(null, video) + }) +} + +function fetchRemoteVideo (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, podHost, remoteId }) + return callback(err) + } + + return callback(null, video) + }) +} diff --git a/server/controllers/api/requests.js b/server/controllers/api/requests.js deleted file mode 100644 index 6fd5753ac..000000000 --- a/server/controllers/api/requests.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict' - -const express = require('express') -const parallel = require('async/parallel') - -const friends = require('../../lib/friends') -const middlewares = require('../../middlewares') -const admin = middlewares.admin -const oAuth = middlewares.oauth - -const router = express.Router() - -router.get('/stats', - oAuth.authenticate, - admin.ensureIsAdmin, - getStatsRequests -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function getStatsRequests (req, res, next) { - parallel({ - requestScheduler: buildRequestSchedulerFunction(friends.getRequestScheduler()), - requestVideoQaduScheduler: buildRequestSchedulerFunction(friends.getRequestVideoQaduScheduler()), - requestVideoEventScheduler: buildRequestSchedulerFunction(friends.getRequestVideoEventScheduler()) - }, function (err, result) { - if (err) return next(err) - - return res.json(result) - }) -} - -// --------------------------------------------------------------------------- - -function buildRequestSchedulerFunction (requestScheduler) { - return function (callback) { - requestScheduler.remainingRequestsCount(function (err, count) { - if (err) return callback(err) - - const result = { - totalRequests: count, - requestsLimitPods: requestScheduler.limitPods, - requestsLimitPerPod: requestScheduler.limitPerPod, - remainingMilliSeconds: requestScheduler.remainingMilliSeconds(), - milliSecondsInterval: requestScheduler.requestInterval - } - - return callback(null, result) - }) - } -} diff --git a/server/controllers/api/requests.ts b/server/controllers/api/requests.ts new file mode 100644 index 000000000..304499a4f --- /dev/null +++ b/server/controllers/api/requests.ts @@ -0,0 +1,57 @@ +import express = require('express') +import { parallel } from 'async' + +import { + getRequestScheduler, + getRequestVideoQaduScheduler, + getRequestVideoEventScheduler +} from '../../lib' +import { authenticate, ensureIsAdmin } from '../../middlewares' + +const requestsRouter = express.Router() + +requestsRouter.get('/stats', + authenticate, + ensureIsAdmin, + getStatsRequests +) + +// --------------------------------------------------------------------------- + +export { + requestsRouter +} + +// --------------------------------------------------------------------------- + +function getStatsRequests (req, res, next) { + parallel({ + requestScheduler: buildRequestSchedulerFunction(getRequestScheduler()), + requestVideoQaduScheduler: buildRequestSchedulerFunction(getRequestVideoQaduScheduler()), + requestVideoEventScheduler: buildRequestSchedulerFunction(getRequestVideoEventScheduler()) + }, function (err, result) { + if (err) return next(err) + + return res.json(result) + }) +} + +// --------------------------------------------------------------------------- + +function buildRequestSchedulerFunction (requestScheduler) { + return function (callback) { + requestScheduler.remainingRequestsCount(function (err, count) { + if (err) return callback(err) + + const result = { + totalRequests: count, + requestsLimitPods: requestScheduler.limitPods, + requestsLimitPerPod: requestScheduler.limitPerPod, + remainingMilliSeconds: requestScheduler.remainingMilliSeconds(), + milliSecondsInterval: requestScheduler.requestInterval + } + + return callback(null, result) + }) + } +} diff --git a/server/controllers/api/users.js b/server/controllers/api/users.js deleted file mode 100644 index c7fe7bf85..000000000 --- a/server/controllers/api/users.js +++ /dev/null @@ -1,169 +0,0 @@ -'use strict' - -const express = require('express') -const waterfall = require('async/waterfall') - -const constants = require('../../initializers/constants') -const db = require('../../initializers/database') -const logger = require('../../helpers/logger') -const utils = require('../../helpers/utils') -const middlewares = require('../../middlewares') -const admin = middlewares.admin -const oAuth = middlewares.oauth -const pagination = middlewares.pagination -const sort = middlewares.sort -const validatorsPagination = middlewares.validators.pagination -const validatorsSort = middlewares.validators.sort -const validatorsUsers = middlewares.validators.users - -const router = express.Router() - -router.get('/me', - oAuth.authenticate, - getUserInformation -) - -router.get('/me/videos/:videoId/rating', - oAuth.authenticate, - validatorsUsers.usersVideoRating, - getUserVideoRating -) - -router.get('/', - validatorsPagination.pagination, - validatorsSort.usersSort, - sort.setUsersSort, - pagination.setPagination, - listUsers -) - -router.post('/', - oAuth.authenticate, - admin.ensureIsAdmin, - validatorsUsers.usersAdd, - createUser -) - -router.post('/register', - ensureRegistrationEnabled, - validatorsUsers.usersAdd, - createUser -) - -router.put('/:id', - oAuth.authenticate, - validatorsUsers.usersUpdate, - updateUser -) - -router.delete('/:id', - oAuth.authenticate, - admin.ensureIsAdmin, - validatorsUsers.usersRemove, - removeUser -) - -router.post('/token', oAuth.token, success) -// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function ensureRegistrationEnabled (req, res, next) { - const registrationEnabled = constants.CONFIG.SIGNUP.ENABLED - - if (registrationEnabled === true) { - return next() - } - - return res.status(400).send('User registration is not enabled.') -} - -function createUser (req, res, next) { - const user = db.User.build({ - username: req.body.username, - password: req.body.password, - email: req.body.email, - displayNSFW: false, - role: constants.USER_ROLES.USER - }) - - user.save().asCallback(function (err, createdUser) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) -} - -function getUserInformation (req, res, next) { - db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { - if (err) return next(err) - - return res.json(user.toFormatedJSON()) - }) -} - -function getUserVideoRating (req, res, next) { - const videoId = req.params.videoId - const userId = res.locals.oauth.token.User.id - - db.UserVideoRate.load(userId, videoId, function (err, ratingObj) { - if (err) return next(err) - - const rating = ratingObj ? ratingObj.type : 'none' - - res.json({ - videoId, - rating - }) - }) -} - -function listUsers (req, res, next) { - db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { - if (err) return next(err) - - res.json(utils.getFormatedObjects(usersList, usersTotal)) - }) -} - -function removeUser (req, res, next) { - waterfall([ - function loadUser (callback) { - db.User.loadById(req.params.id, callback) - }, - - function deleteUser (user, callback) { - user.destroy().asCallback(callback) - } - ], function andFinally (err) { - if (err) { - logger.error('Errors when removed the user.', { error: err }) - return next(err) - } - - return res.sendStatus(204) - }) -} - -function updateUser (req, res, next) { - db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { - if (err) return next(err) - - if (req.body.password) user.password = req.body.password - if (req.body.displayNSFW !== undefined) user.displayNSFW = req.body.displayNSFW - - user.save().asCallback(function (err) { - if (err) return next(err) - - return res.sendStatus(204) - }) - }) -} - -function success (req, res, next) { - res.end() -} diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts new file mode 100644 index 000000000..981a4706a --- /dev/null +++ b/server/controllers/api/users.ts @@ -0,0 +1,173 @@ +import express = require('express') +import { waterfall } from 'async' + +const db = require('../../initializers/database') +import { CONFIG, USER_ROLES } from '../../initializers' +import { logger, getFormatedObjects } from '../../helpers' +import { + authenticate, + ensureIsAdmin, + usersAddValidator, + usersUpdateValidator, + usersRemoveValidator, + usersVideoRatingValidator, + paginationValidator, + setPagination, + usersSortValidator, + setUsersSort, + token +} from '../../middlewares' + +const usersRouter = express.Router() + +usersRouter.get('/me', + authenticate, + getUserInformation +) + +usersRouter.get('/me/videos/:videoId/rating', + authenticate, + usersVideoRatingValidator, + getUserVideoRating +) + +usersRouter.get('/', + paginationValidator, + usersSortValidator, + setUsersSort, + setPagination, + listUsers +) + +usersRouter.post('/', + authenticate, + ensureIsAdmin, + usersAddValidator, + createUser +) + +usersRouter.post('/register', + ensureRegistrationEnabled, + usersAddValidator, + createUser +) + +usersRouter.put('/:id', + authenticate, + usersUpdateValidator, + updateUser +) + +usersRouter.delete('/:id', + authenticate, + ensureIsAdmin, + usersRemoveValidator, + removeUser +) + +usersRouter.post('/token', token, success) +// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route + +// --------------------------------------------------------------------------- + +export { + usersRouter +} + +// --------------------------------------------------------------------------- + +function ensureRegistrationEnabled (req, res, next) { + const registrationEnabled = CONFIG.SIGNUP.ENABLED + + if (registrationEnabled === true) { + return next() + } + + return res.status(400).send('User registration is not enabled.') +} + +function createUser (req, res, next) { + const user = db.User.build({ + username: req.body.username, + password: req.body.password, + email: req.body.email, + displayNSFW: false, + role: USER_ROLES.USER + }) + + user.save().asCallback(function (err, createdUser) { + if (err) return next(err) + + return res.type('json').status(204).end() + }) +} + +function getUserInformation (req, res, next) { + db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { + if (err) return next(err) + + return res.json(user.toFormatedJSON()) + }) +} + +function getUserVideoRating (req, res, next) { + const videoId = req.params.videoId + const userId = res.locals.oauth.token.User.id + + db.UserVideoRate.load(userId, videoId, function (err, ratingObj) { + if (err) return next(err) + + const rating = ratingObj ? ratingObj.type : 'none' + + res.json({ + videoId, + rating + }) + }) +} + +function listUsers (req, res, next) { + db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { + if (err) return next(err) + + res.json(getFormatedObjects(usersList, usersTotal)) + }) +} + +function removeUser (req, res, next) { + waterfall([ + function loadUser (callback) { + db.User.loadById(req.params.id, callback) + }, + + function deleteUser (user, callback) { + user.destroy().asCallback(callback) + } + ], function andFinally (err) { + if (err) { + logger.error('Errors when removed the user.', { error: err }) + return next(err) + } + + return res.sendStatus(204) + }) +} + +function updateUser (req, res, next) { + db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { + if (err) return next(err) + + if (req.body.password) user.password = req.body.password + if (req.body.displayNSFW !== undefined) user.displayNSFW = req.body.displayNSFW + + user.save().asCallback(function (err) { + if (err) return next(err) + + return res.sendStatus(204) + }) + }) +} + +function success (req, res, next) { + res.end() +} diff --git a/server/controllers/api/videos/abuse.js b/server/controllers/api/videos/abuse.js deleted file mode 100644 index 0fb44bb14..000000000 --- a/server/controllers/api/videos/abuse.js +++ /dev/null @@ -1,112 +0,0 @@ -'use strict' - -const express = require('express') -const waterfall = require('async/waterfall') - -const db = require('../../../initializers/database') -const logger = require('../../../helpers/logger') -const friends = require('../../../lib/friends') -const middlewares = require('../../../middlewares') -const admin = middlewares.admin -const oAuth = middlewares.oauth -const pagination = middlewares.pagination -const validators = middlewares.validators -const validatorsPagination = validators.pagination -const validatorsSort = validators.sort -const validatorsVideos = validators.videos -const sort = middlewares.sort -const databaseUtils = require('../../../helpers/database-utils') -const utils = require('../../../helpers/utils') - -const router = express.Router() - -router.get('/abuse', - oAuth.authenticate, - admin.ensureIsAdmin, - validatorsPagination.pagination, - validatorsSort.videoAbusesSort, - sort.setVideoAbusesSort, - pagination.setPagination, - listVideoAbuses -) -router.post('/:id/abuse', - oAuth.authenticate, - validatorsVideos.videoAbuseReport, - reportVideoAbuseRetryWrapper -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -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) - - res.json(utils.getFormatedObjects(abusesList, abusesTotal)) - }) -} - -function reportVideoAbuseRetryWrapper (req, res, next) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot report abuse to the video with many retries.' - } - - databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) -} - -function reportVideoAbuse (req, res, finalCallback) { - const videoInstance = res.locals.video - 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 - } - - waterfall([ - - databaseUtils.startSerializableTransaction, - - function createAbuse (t, callback) { - db.VideoAbuse.create(abuse).asCallback(function (err, abuse) { - return callback(err, t, abuse) - }) - }, - - function sendToFriendsIfNeeded (t, abuse, callback) { - // 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 callback(null, t) - }, - - databaseUtils.commitTransaction - - ], function andFinally (err, t) { - if (err) { - logger.debug('Cannot update the video.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Abuse report for video %s created.', videoInstance.name) - return finalCallback(null) - }) -} diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts new file mode 100644 index 000000000..88204120f --- /dev/null +++ b/server/controllers/api/videos/abuse.ts @@ -0,0 +1,117 @@ +import express = require('express') +import { waterfall } from 'async' + +const db = require('../../../initializers/database') +import friends = require('../../../lib/friends') +import { + logger, + getFormatedObjects, + retryTransactionWrapper, + startSerializableTransaction, + commitTransaction, + rollbackTransaction +} from '../../../helpers' +import { + authenticate, + ensureIsAdmin, + paginationValidator, + videoAbuseReportValidator, + videoAbusesSortValidator, + setVideoAbusesSort, + setPagination +} from '../../../middlewares' + +const abuseVideoRouter = express.Router() + +abuseVideoRouter.get('/abuse', + authenticate, + ensureIsAdmin, + paginationValidator, + videoAbusesSortValidator, + setVideoAbusesSort, + setPagination, + listVideoAbuses +) +abuseVideoRouter.post('/:id/abuse', + authenticate, + videoAbuseReportValidator, + reportVideoAbuseRetryWrapper +) + +// --------------------------------------------------------------------------- + +export { + abuseVideoRouter +} + +// --------------------------------------------------------------------------- + +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) + + res.json(getFormatedObjects(abusesList, abusesTotal)) + }) +} + +function reportVideoAbuseRetryWrapper (req, res, next) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot report abuse to the video with many retries.' + } + + retryTransactionWrapper(reportVideoAbuse, options, function (err) { + if (err) return next(err) + + return res.type('json').status(204).end() + }) +} + +function reportVideoAbuse (req, res, finalCallback) { + const videoInstance = res.locals.video + 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 + } + + waterfall([ + + startSerializableTransaction, + + function createAbuse (t, callback) { + db.VideoAbuse.create(abuse).asCallback(function (err, abuse) { + return callback(err, t, abuse) + }) + }, + + function sendToFriendsIfNeeded (t, abuse, callback) { + // 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 callback(null, t) + }, + + commitTransaction + + ], function andFinally (err, t) { + if (err) { + logger.debug('Cannot update the video.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Abuse report for video %s created.', videoInstance.name) + return finalCallback(null) + }) +} diff --git a/server/controllers/api/videos/blacklist.js b/server/controllers/api/videos/blacklist.js deleted file mode 100644 index 8c3e2a69d..000000000 --- a/server/controllers/api/videos/blacklist.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' - -const express = require('express') - -const db = require('../../../initializers/database') -const logger = require('../../../helpers/logger') -const middlewares = require('../../../middlewares') -const admin = middlewares.admin -const oAuth = middlewares.oauth -const validators = middlewares.validators -const validatorsVideos = validators.videos - -const router = express.Router() - -router.post('/:id/blacklist', - oAuth.authenticate, - admin.ensureIsAdmin, - validatorsVideos.videosBlacklist, - addVideoToBlacklist -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function addVideoToBlacklist (req, res, next) { - const videoInstance = res.locals.video - - const toCreate = { - videoId: videoInstance.id - } - - db.BlacklistedVideo.create(toCreate).asCallback(function (err) { - if (err) { - logger.error('Errors when blacklisting video ', { error: err }) - return next(err) - } - - return res.type('json').status(204).end() - }) -} diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts new file mode 100644 index 000000000..db6d95e73 --- /dev/null +++ b/server/controllers/api/videos/blacklist.ts @@ -0,0 +1,43 @@ +import express = require('express') + +const db = require('../../../initializers/database') +import { logger } from '../../../helpers' +import { + authenticate, + ensureIsAdmin, + videosBlacklistValidator +} from '../../../middlewares' + +const blacklistRouter = express.Router() + +blacklistRouter.post('/:id/blacklist', + authenticate, + ensureIsAdmin, + videosBlacklistValidator, + addVideoToBlacklist +) + +// --------------------------------------------------------------------------- + +export { + blacklistRouter +} + +// --------------------------------------------------------------------------- + +function addVideoToBlacklist (req, res, next) { + const videoInstance = res.locals.video + + const toCreate = { + videoId: videoInstance.id + } + + db.BlacklistedVideo.create(toCreate).asCallback(function (err) { + if (err) { + logger.error('Errors when blacklisting video ', { error: err }) + return next(err) + } + + return res.type('json').status(204).end() + }) +} diff --git a/server/controllers/api/videos/index.js b/server/controllers/api/videos/index.js deleted file mode 100644 index 8de44d5ac..000000000 --- a/server/controllers/api/videos/index.js +++ /dev/null @@ -1,404 +0,0 @@ -'use strict' - -const express = require('express') -const fs = require('fs') -const multer = require('multer') -const path = require('path') -const waterfall = require('async/waterfall') - -const constants = require('../../../initializers/constants') -const db = require('../../../initializers/database') -const logger = require('../../../helpers/logger') -const friends = require('../../../lib/friends') -const middlewares = require('../../../middlewares') -const oAuth = middlewares.oauth -const pagination = middlewares.pagination -const validators = middlewares.validators -const validatorsPagination = validators.pagination -const validatorsSort = validators.sort -const validatorsVideos = validators.videos -const search = middlewares.search -const sort = middlewares.sort -const databaseUtils = require('../../../helpers/database-utils') -const utils = require('../../../helpers/utils') - -const abuseController = require('./abuse') -const blacklistController = require('./blacklist') -const rateController = require('./rate') - -const router = express.Router() - -// multer configuration -const storage = multer.diskStorage({ - destination: function (req, file, cb) { - cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR) - }, - - filename: function (req, file, cb) { - let extension = '' - if (file.mimetype === 'video/webm') extension = 'webm' - else if (file.mimetype === 'video/mp4') extension = 'mp4' - else if (file.mimetype === 'video/ogg') extension = 'ogv' - utils.generateRandomString(16, function (err, randomString) { - const fieldname = err ? undefined : randomString - cb(null, fieldname + '.' + extension) - }) - } -}) - -const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) - -router.use('/', abuseController) -router.use('/', blacklistController) -router.use('/', rateController) - -router.get('/categories', listVideoCategories) -router.get('/licences', listVideoLicences) -router.get('/languages', listVideoLanguages) - -router.get('/', - validatorsPagination.pagination, - validatorsSort.videosSort, - sort.setVideosSort, - pagination.setPagination, - listVideos -) -router.put('/:id', - oAuth.authenticate, - reqFiles, - validatorsVideos.videosUpdate, - updateVideoRetryWrapper -) -router.post('/', - oAuth.authenticate, - reqFiles, - validatorsVideos.videosAdd, - addVideoRetryWrapper -) -router.get('/:id', - validatorsVideos.videosGet, - getVideo -) - -router.delete('/:id', - oAuth.authenticate, - validatorsVideos.videosRemove, - removeVideo -) - -router.get('/search/:value', - validatorsVideos.videosSearch, - validatorsPagination.pagination, - validatorsSort.videosSort, - sort.setVideosSort, - pagination.setPagination, - search.setVideosSearch, - searchVideos -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function listVideoCategories (req, res, next) { - res.json(constants.VIDEO_CATEGORIES) -} - -function listVideoLicences (req, res, next) { - res.json(constants.VIDEO_LICENCES) -} - -function listVideoLanguages (req, res, next) { - res.json(constants.VIDEO_LANGUAGES) -} - -// Wrapper to video add that retry the function if there is a database error -// We need this because we run the transaction in SERIALIZABLE isolation that can fail -function addVideoRetryWrapper (req, res, next) { - const options = { - arguments: [ req, res, req.files.videofile[0] ], - errorMessage: 'Cannot insert the video with many retries.' - } - - databaseUtils.retryTransactionWrapper(addVideo, options, function (err) { - if (err) return next(err) - - // TODO : include Location of the new video -> 201 - return res.type('json').status(204).end() - }) -} - -function addVideo (req, res, videoFile, finalCallback) { - const videoInfos = req.body - - waterfall([ - - databaseUtils.startSerializableTransaction, - - function findOrCreateAuthor (t, callback) { - const user = res.locals.oauth.token.User - - const name = user.username - // null because it is OUR pod - const podId = null - const userId = user.id - - db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { - return callback(err, t, authorInstance) - }) - }, - - function findOrCreateTags (t, author, callback) { - const tags = videoInfos.tags - - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, author, tagInstances) - }) - }, - - function createVideoObject (t, author, tagInstances, callback) { - const videoData = { - name: videoInfos.name, - remoteId: null, - extname: path.extname(videoFile.filename), - category: videoInfos.category, - licence: videoInfos.licence, - language: videoInfos.language, - nsfw: videoInfos.nsfw, - description: videoInfos.description, - duration: videoFile.duration, - authorId: author.id - } - - const video = db.Video.build(videoData) - - return callback(null, t, author, tagInstances, video) - }, - - // Set the videoname the same as the id - function renameVideoFile (t, author, tagInstances, video, callback) { - const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR - const source = path.join(videoDir, videoFile.filename) - const destination = path.join(videoDir, video.getVideoFilename()) - - fs.rename(source, destination, function (err) { - if (err) return callback(err) - - // This is important in case if there is another attempt - videoFile.filename = video.getVideoFilename() - return callback(null, t, author, tagInstances, video) - }) - }, - - function insertVideoIntoDB (t, author, tagInstances, video, callback) { - const options = { transaction: t } - - // Add tags association - video.save(options).asCallback(function (err, videoCreated) { - if (err) return callback(err) - - // Do not forget to add Author informations to the created video - videoCreated.Author = author - - return callback(err, t, tagInstances, videoCreated) - }) - }, - - function associateTagsToVideo (t, tagInstances, video, callback) { - const options = { transaction: t } - - video.setTags(tagInstances, options).asCallback(function (err) { - video.Tags = tagInstances - - return callback(err, t, video) - }) - }, - - function sendToFriends (t, video, callback) { - // Let transcoding job send the video to friends because the videofile extension might change - if (constants.CONFIG.TRANSCODING.ENABLED === true) return callback(null, t) - - video.toAddRemoteJSON(function (err, remoteVideo) { - if (err) return callback(err) - - // Now we'll add the video's meta data to our friends - friends.addVideoToFriends(remoteVideo, t, function (err) { - return callback(err, t) - }) - }) - }, - - databaseUtils.commitTransaction - - ], function andFinally (err, t) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot insert the video.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Video with name %s created.', videoInfos.name) - return finalCallback(null) - }) -} - -function updateVideoRetryWrapper (req, res, next) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot update the video with many retries.' - } - - databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) { - if (err) return next(err) - - // TODO : include Location of the new video -> 201 - return res.type('json').status(204).end() - }) -} - -function updateVideo (req, res, finalCallback) { - const videoInstance = res.locals.video - const videoFieldsSave = videoInstance.toJSON() - const videoInfosToUpdate = req.body - - waterfall([ - - databaseUtils.startSerializableTransaction, - - function findOrCreateTags (t, callback) { - if (videoInfosToUpdate.tags) { - db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) { - return callback(err, t, tagInstances) - }) - } else { - return callback(null, t, null) - } - }, - - function updateVideoIntoDB (t, tagInstances, callback) { - const options = { - transaction: t - } - - if (videoInfosToUpdate.name !== undefined) videoInstance.set('name', videoInfosToUpdate.name) - if (videoInfosToUpdate.category !== undefined) videoInstance.set('category', videoInfosToUpdate.category) - if (videoInfosToUpdate.licence !== undefined) videoInstance.set('licence', videoInfosToUpdate.licence) - if (videoInfosToUpdate.language !== undefined) videoInstance.set('language', videoInfosToUpdate.language) - if (videoInfosToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfosToUpdate.nsfw) - if (videoInfosToUpdate.description !== undefined) videoInstance.set('description', videoInfosToUpdate.description) - - videoInstance.save(options).asCallback(function (err) { - return callback(err, t, tagInstances) - }) - }, - - function associateTagsToVideo (t, tagInstances, callback) { - if (tagInstances) { - const options = { transaction: t } - - videoInstance.setTags(tagInstances, options).asCallback(function (err) { - videoInstance.Tags = tagInstances - - return callback(err, t) - }) - } else { - return callback(null, t) - } - }, - - function sendToFriends (t, callback) { - const json = videoInstance.toUpdateRemoteJSON() - - // Now we'll update the video's meta data to our friends - friends.updateVideoToFriends(json, t, function (err) { - return callback(err, t) - }) - }, - - databaseUtils.commitTransaction - - ], function andFinally (err, t) { - if (err) { - logger.debug('Cannot update the video.', { error: err }) - - // Force fields we want to update - // If the transaction is retried, sequelize will think the object has not changed - // So it will skip the SQL request, even if the last one was ROLLBACKed! - Object.keys(videoFieldsSave).forEach(function (key) { - const value = videoFieldsSave[key] - videoInstance.set(key, value) - }) - - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Video with name %s updated.', videoInfosToUpdate.name) - return finalCallback(null) - }) -} - -function getVideo (req, res, next) { - const videoInstance = res.locals.video - - if (videoInstance.isOwned()) { - // The increment is done directly in the database, not using the instance value - videoInstance.increment('views').asCallback(function (err) { - if (err) { - logger.error('Cannot add view to video %d.', videoInstance.id) - return - } - - // FIXME: make a real view system - // For example, only add a view when a user watch a video during 30s etc - const qaduParams = { - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS - } - friends.quickAndDirtyUpdateVideoToFriends(qaduParams) - }) - } else { - // Just send the event to our friends - const eventParams = { - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS - } - friends.addEventToRemoteVideo(eventParams) - } - - // Do not wait the view system - res.json(videoInstance.toFormatedJSON()) -} - -function listVideos (req, res, next) { - db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { - if (err) return next(err) - - res.json(utils.getFormatedObjects(videosList, videosTotal)) - }) -} - -function removeVideo (req, res, next) { - const videoInstance = res.locals.video - - videoInstance.destroy().asCallback(function (err) { - if (err) { - logger.error('Errors when removed the video.', { error: err }) - return next(err) - } - - return res.type('json').status(204).end() - }) -} - -function searchVideos (req, res, next) { - db.Video.searchAndPopulateAuthorAndPodAndTags( - req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort, - function (err, videosList, videosTotal) { - if (err) return next(err) - - res.json(utils.getFormatedObjects(videosList, videosTotal)) - } - ) -} diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts new file mode 100644 index 000000000..5fbf03676 --- /dev/null +++ b/server/controllers/api/videos/index.ts @@ -0,0 +1,426 @@ +import express = require('express') +import fs = require('fs') +import multer = require('multer') +import path = require('path') +import { waterfall } from 'async' + +const db = require('../../../initializers/database') +import { + CONFIG, + REQUEST_VIDEO_QADU_TYPES, + REQUEST_VIDEO_EVENT_TYPES, + VIDEO_CATEGORIES, + VIDEO_LICENCES, + VIDEO_LANGUAGES +} from '../../../initializers' +import { + addEventToRemoteVideo, + quickAndDirtyUpdateVideoToFriends, + addVideoToFriends, + updateVideoToFriends +} from '../../../lib' +import { + authenticate, + paginationValidator, + videosSortValidator, + setVideosSort, + setPagination, + setVideosSearch, + videosUpdateValidator, + videosSearchValidator, + videosAddValidator, + videosGetValidator, + videosRemoveValidator +} from '../../../middlewares' +import { + logger, + commitTransaction, + retryTransactionWrapper, + rollbackTransaction, + startSerializableTransaction, + generateRandomString, + getFormatedObjects +} from '../../../helpers' + +import { abuseVideoRouter } from './abuse' +import { blacklistRouter } from './blacklist' +import { rateVideoRouter } from './rate' + +const videosRouter = express.Router() + +// multer configuration +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, CONFIG.STORAGE.VIDEOS_DIR) + }, + + filename: function (req, file, cb) { + let extension = '' + if (file.mimetype === 'video/webm') extension = 'webm' + else if (file.mimetype === 'video/mp4') extension = 'mp4' + else if (file.mimetype === 'video/ogg') extension = 'ogv' + generateRandomString(16, function (err, randomString) { + const fieldname = err ? undefined : randomString + cb(null, fieldname + '.' + extension) + }) + } +}) + +const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) + +videosRouter.use('/', abuseVideoRouter) +videosRouter.use('/', blacklistRouter) +videosRouter.use('/', rateVideoRouter) + +videosRouter.get('/categories', listVideoCategories) +videosRouter.get('/licences', listVideoLicences) +videosRouter.get('/languages', listVideoLanguages) + +videosRouter.get('/', + paginationValidator, + videosSortValidator, + setVideosSort, + setPagination, + listVideos +) +videosRouter.put('/:id', + authenticate, + reqFiles, + videosUpdateValidator, + updateVideoRetryWrapper +) +videosRouter.post('/', + authenticate, + reqFiles, + videosAddValidator, + addVideoRetryWrapper +) +videosRouter.get('/:id', + videosGetValidator, + getVideo +) + +videosRouter.delete('/:id', + authenticate, + videosRemoveValidator, + removeVideo +) + +videosRouter.get('/search/:value', + videosSearchValidator, + paginationValidator, + videosSortValidator, + setVideosSort, + setPagination, + setVideosSearch, + searchVideos +) + +// --------------------------------------------------------------------------- + +export { + videosRouter +} + +// --------------------------------------------------------------------------- + +function listVideoCategories (req, res, next) { + res.json(VIDEO_CATEGORIES) +} + +function listVideoLicences (req, res, next) { + res.json(VIDEO_LICENCES) +} + +function listVideoLanguages (req, res, next) { + res.json(VIDEO_LANGUAGES) +} + +// Wrapper to video add that retry the function if there is a database error +// We need this because we run the transaction in SERIALIZABLE isolation that can fail +function addVideoRetryWrapper (req, res, next) { + const options = { + arguments: [ req, res, req.files.videofile[0] ], + errorMessage: 'Cannot insert the video with many retries.' + } + + retryTransactionWrapper(addVideo, options, function (err) { + if (err) return next(err) + + // TODO : include Location of the new video -> 201 + return res.type('json').status(204).end() + }) +} + +function addVideo (req, res, videoFile, finalCallback) { + const videoInfos = req.body + + waterfall([ + + startSerializableTransaction, + + function findOrCreateAuthor (t, callback) { + const user = res.locals.oauth.token.User + + const name = user.username + // null because it is OUR pod + const podId = null + const userId = user.id + + db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { + return callback(err, t, authorInstance) + }) + }, + + function findOrCreateTags (t, author, callback) { + const tags = videoInfos.tags + + db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { + return callback(err, t, author, tagInstances) + }) + }, + + function createVideoObject (t, author, tagInstances, callback) { + const videoData = { + name: videoInfos.name, + remoteId: null, + extname: path.extname(videoFile.filename), + category: videoInfos.category, + licence: videoInfos.licence, + language: videoInfos.language, + nsfw: videoInfos.nsfw, + description: videoInfos.description, + duration: videoFile.duration, + authorId: author.id + } + + const video = db.Video.build(videoData) + + return callback(null, t, author, tagInstances, video) + }, + + // Set the videoname the same as the id + function renameVideoFile (t, author, tagInstances, video, callback) { + const videoDir = CONFIG.STORAGE.VIDEOS_DIR + const source = path.join(videoDir, videoFile.filename) + const destination = path.join(videoDir, video.getVideoFilename()) + + fs.rename(source, destination, function (err) { + if (err) return callback(err) + + // This is important in case if there is another attempt + videoFile.filename = video.getVideoFilename() + return callback(null, t, author, tagInstances, video) + }) + }, + + function insertVideoIntoDB (t, author, tagInstances, video, callback) { + const options = { transaction: t } + + // Add tags association + video.save(options).asCallback(function (err, videoCreated) { + if (err) return callback(err) + + // Do not forget to add Author informations to the created video + videoCreated.Author = author + + return callback(err, t, tagInstances, videoCreated) + }) + }, + + function associateTagsToVideo (t, tagInstances, video, callback) { + const options = { transaction: t } + + video.setTags(tagInstances, options).asCallback(function (err) { + video.Tags = tagInstances + + return callback(err, t, video) + }) + }, + + function sendToFriends (t, video, callback) { + // Let transcoding job send the video to friends because the videofile extension might change + if (CONFIG.TRANSCODING.ENABLED === true) return callback(null, t) + + video.toAddRemoteJSON(function (err, remoteVideo) { + if (err) return callback(err) + + // Now we'll add the video's meta data to our friends + addVideoToFriends(remoteVideo, t, function (err) { + return callback(err, t) + }) + }) + }, + + commitTransaction + + ], function andFinally (err, t) { + if (err) { + // This is just a debug because we will retry the insert + logger.debug('Cannot insert the video.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Video with name %s created.', videoInfos.name) + return finalCallback(null) + }) +} + +function updateVideoRetryWrapper (req, res, next) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot update the video with many retries.' + } + + retryTransactionWrapper(updateVideo, options, function (err) { + if (err) return next(err) + + // TODO : include Location of the new video -> 201 + return res.type('json').status(204).end() + }) +} + +function updateVideo (req, res, finalCallback) { + const videoInstance = res.locals.video + const videoFieldsSave = videoInstance.toJSON() + const videoInfosToUpdate = req.body + + waterfall([ + + startSerializableTransaction, + + function findOrCreateTags (t, callback) { + if (videoInfosToUpdate.tags) { + db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) { + return callback(err, t, tagInstances) + }) + } else { + return callback(null, t, null) + } + }, + + function updateVideoIntoDB (t, tagInstances, callback) { + const options = { + transaction: t + } + + if (videoInfosToUpdate.name !== undefined) videoInstance.set('name', videoInfosToUpdate.name) + if (videoInfosToUpdate.category !== undefined) videoInstance.set('category', videoInfosToUpdate.category) + if (videoInfosToUpdate.licence !== undefined) videoInstance.set('licence', videoInfosToUpdate.licence) + if (videoInfosToUpdate.language !== undefined) videoInstance.set('language', videoInfosToUpdate.language) + if (videoInfosToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfosToUpdate.nsfw) + if (videoInfosToUpdate.description !== undefined) videoInstance.set('description', videoInfosToUpdate.description) + + videoInstance.save(options).asCallback(function (err) { + return callback(err, t, tagInstances) + }) + }, + + function associateTagsToVideo (t, tagInstances, callback) { + if (tagInstances) { + const options = { transaction: t } + + videoInstance.setTags(tagInstances, options).asCallback(function (err) { + videoInstance.Tags = tagInstances + + return callback(err, t) + }) + } else { + return callback(null, t) + } + }, + + function sendToFriends (t, callback) { + const json = videoInstance.toUpdateRemoteJSON() + + // Now we'll update the video's meta data to our friends + updateVideoToFriends(json, t, function (err) { + return callback(err, t) + }) + }, + + commitTransaction + + ], function andFinally (err, t) { + if (err) { + logger.debug('Cannot update the video.', { error: err }) + + // Force fields we want to update + // If the transaction is retried, sequelize will think the object has not changed + // So it will skip the SQL request, even if the last one was ROLLBACKed! + Object.keys(videoFieldsSave).forEach(function (key) { + const value = videoFieldsSave[key] + videoInstance.set(key, value) + }) + + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Video with name %s updated.', videoInfosToUpdate.name) + return finalCallback(null) + }) +} + +function getVideo (req, res, next) { + const videoInstance = res.locals.video + + if (videoInstance.isOwned()) { + // The increment is done directly in the database, not using the instance value + videoInstance.increment('views').asCallback(function (err) { + if (err) { + logger.error('Cannot add view to video %d.', videoInstance.id) + return + } + + // FIXME: make a real view system + // For example, only add a view when a user watch a video during 30s etc + const qaduParams = { + videoId: videoInstance.id, + type: REQUEST_VIDEO_QADU_TYPES.VIEWS + } + quickAndDirtyUpdateVideoToFriends(qaduParams) + }) + } else { + // Just send the event to our friends + const eventParams = { + videoId: videoInstance.id, + type: REQUEST_VIDEO_EVENT_TYPES.VIEWS + } + addEventToRemoteVideo(eventParams) + } + + // Do not wait the view system + res.json(videoInstance.toFormatedJSON()) +} + +function listVideos (req, res, next) { + db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { + if (err) return next(err) + + res.json(getFormatedObjects(videosList, videosTotal)) + }) +} + +function removeVideo (req, res, next) { + const videoInstance = res.locals.video + + videoInstance.destroy().asCallback(function (err) { + if (err) { + logger.error('Errors when removed the video.', { error: err }) + return next(err) + } + + return res.type('json').status(204).end() + }) +} + +function searchVideos (req, res, next) { + db.Video.searchAndPopulateAuthorAndPodAndTags( + req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort, + function (err, videosList, videosTotal) { + if (err) return next(err) + + res.json(getFormatedObjects(videosList, videosTotal)) + } + ) +} diff --git a/server/controllers/api/videos/rate.js b/server/controllers/api/videos/rate.js deleted file mode 100644 index df8a69a1d..000000000 --- a/server/controllers/api/videos/rate.js +++ /dev/null @@ -1,169 +0,0 @@ -'use strict' - -const express = require('express') -const waterfall = require('async/waterfall') - -const constants = require('../../../initializers/constants') -const db = require('../../../initializers/database') -const logger = require('../../../helpers/logger') -const friends = require('../../../lib/friends') -const middlewares = require('../../../middlewares') -const oAuth = middlewares.oauth -const validators = middlewares.validators -const validatorsVideos = validators.videos -const databaseUtils = require('../../../helpers/database-utils') - -const router = express.Router() - -router.put('/:id/rate', - oAuth.authenticate, - validatorsVideos.videoRate, - rateVideoRetryWrapper -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function rateVideoRetryWrapper (req, res, next) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot update the user video rate.' - } - - databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) -} - -function rateVideo (req, res, finalCallback) { - const rateType = req.body.rating - const videoInstance = res.locals.video - const userInstance = res.locals.oauth.token.User - - waterfall([ - databaseUtils.startSerializableTransaction, - - function findPreviousRate (t, callback) { - db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) { - return callback(err, t, previousRate) - }) - }, - - function insertUserRateIntoDB (t, previousRate, callback) { - const options = { transaction: t } - - let likesToIncrement = 0 - let dislikesToIncrement = 0 - - if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++ - else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ - - // There was a previous rate, update it - if (previousRate) { - // We will remove the previous rate, so we will need to remove it from the video attribute - if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement-- - else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- - - previousRate.type = rateType - - previousRate.save(options).asCallback(function (err) { - return callback(err, t, likesToIncrement, dislikesToIncrement) - }) - } else { // There was not a previous rate, insert a new one - const query = { - userId: userInstance.id, - videoId: videoInstance.id, - type: rateType - } - - db.UserVideoRate.create(query, options).asCallback(function (err) { - return callback(err, t, likesToIncrement, dislikesToIncrement) - }) - } - }, - - function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) { - const options = { transaction: t } - const incrementQuery = { - likes: likesToIncrement, - dislikes: dislikesToIncrement - } - - // Even if we do not own the video we increment the attributes - // It is usefull for the user to have a feedback - videoInstance.increment(incrementQuery, options).asCallback(function (err) { - return callback(err, t, likesToIncrement, dislikesToIncrement) - }) - }, - - function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { - // No need for an event type, we own the video - if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement) - - const eventsParams = [] - - if (likesToIncrement !== 0) { - eventsParams.push({ - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES, - count: likesToIncrement - }) - } - - if (dislikesToIncrement !== 0) { - eventsParams.push({ - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES, - count: dislikesToIncrement - }) - } - - friends.addEventsToRemoteVideo(eventsParams, t, function (err) { - return callback(err, t, likesToIncrement, dislikesToIncrement) - }) - }, - - function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { - // We do not own the video, there is no need to send a quick and dirty update to friends - // Our rate was already sent by the addEvent function - if (videoInstance.isOwned() === false) return callback(null, t) - - const qadusParams = [] - - if (likesToIncrement !== 0) { - qadusParams.push({ - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES - }) - } - - if (dislikesToIncrement !== 0) { - qadusParams.push({ - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES - }) - } - - friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { - return callback(err, t) - }) - }, - - databaseUtils.commitTransaction - - ], function (err, t) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot add the user video rate.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username) - return finalCallback(null) - }) -} diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts new file mode 100644 index 000000000..21053792a --- /dev/null +++ b/server/controllers/api/videos/rate.ts @@ -0,0 +1,181 @@ +import express = require('express') +import { waterfall } from 'async' + +const db = require('../../../initializers/database') +import { + logger, + retryTransactionWrapper, + startSerializableTransaction, + commitTransaction, + rollbackTransaction +} from '../../../helpers' +import { + VIDEO_RATE_TYPES, + REQUEST_VIDEO_EVENT_TYPES, + REQUEST_VIDEO_QADU_TYPES +} from '../../../initializers' +import { + addEventsToRemoteVideo, + quickAndDirtyUpdatesVideoToFriends +} from '../../../lib' +import { + authenticate, + videoRateValidator +} from '../../../middlewares' + +const rateVideoRouter = express.Router() + +rateVideoRouter.put('/:id/rate', + authenticate, + videoRateValidator, + rateVideoRetryWrapper +) + +// --------------------------------------------------------------------------- + +export { + rateVideoRouter +} + +// --------------------------------------------------------------------------- + +function rateVideoRetryWrapper (req, res, next) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot update the user video rate.' + } + + retryTransactionWrapper(rateVideo, options, function (err) { + if (err) return next(err) + + return res.type('json').status(204).end() + }) +} + +function rateVideo (req, res, finalCallback) { + const rateType = req.body.rating + const videoInstance = res.locals.video + const userInstance = res.locals.oauth.token.User + + waterfall([ + startSerializableTransaction, + + function findPreviousRate (t, callback) { + db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) { + return callback(err, t, previousRate) + }) + }, + + function insertUserRateIntoDB (t, previousRate, callback) { + const options = { transaction: t } + + let likesToIncrement = 0 + let dislikesToIncrement = 0 + + if (rateType === VIDEO_RATE_TYPES.LIKE) likesToIncrement++ + else if (rateType === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ + + // There was a previous rate, update it + if (previousRate) { + // We will remove the previous rate, so we will need to remove it from the video attribute + if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement-- + else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- + + previousRate.type = rateType + + previousRate.save(options).asCallback(function (err) { + return callback(err, t, likesToIncrement, dislikesToIncrement) + }) + } else { // There was not a previous rate, insert a new one + const query = { + userId: userInstance.id, + videoId: videoInstance.id, + type: rateType + } + + db.UserVideoRate.create(query, options).asCallback(function (err) { + return callback(err, t, likesToIncrement, dislikesToIncrement) + }) + } + }, + + function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) { + const options = { transaction: t } + const incrementQuery = { + likes: likesToIncrement, + dislikes: dislikesToIncrement + } + + // Even if we do not own the video we increment the attributes + // It is usefull for the user to have a feedback + videoInstance.increment(incrementQuery, options).asCallback(function (err) { + return callback(err, t, likesToIncrement, dislikesToIncrement) + }) + }, + + function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { + // No need for an event type, we own the video + if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement) + + const eventsParams = [] + + if (likesToIncrement !== 0) { + eventsParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_EVENT_TYPES.LIKES, + count: likesToIncrement + }) + } + + if (dislikesToIncrement !== 0) { + eventsParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_EVENT_TYPES.DISLIKES, + count: dislikesToIncrement + }) + } + + addEventsToRemoteVideo(eventsParams, t, function (err) { + return callback(err, t, likesToIncrement, dislikesToIncrement) + }) + }, + + function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { + // We do not own the video, there is no need to send a quick and dirty update to friends + // Our rate was already sent by the addEvent function + if (videoInstance.isOwned() === false) return callback(null, t) + + const qadusParams = [] + + if (likesToIncrement !== 0) { + qadusParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_QADU_TYPES.LIKES + }) + } + + if (dislikesToIncrement !== 0) { + qadusParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_QADU_TYPES.DISLIKES + }) + } + + quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { + return callback(err, t) + }) + }, + + commitTransaction + + ], function (err, t) { + if (err) { + // This is just a debug because we will retry the insert + logger.debug('Cannot add the user video rate.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username) + return finalCallback(null) + }) +} -- cgit v1.2.3