]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/remote/videos.js
Server: add nsfw attribute
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.js
1 'use strict'
2
3 const eachSeries = require('async/eachSeries')
4 const express = require('express')
5 const waterfall = require('async/waterfall')
6
7 const db = require('../../../initializers/database')
8 const constants = require('../../../initializers/constants')
9 const middlewares = require('../../../middlewares')
10 const secureMiddleware = middlewares.secure
11 const videosValidators = middlewares.validators.remote.videos
12 const signatureValidators = middlewares.validators.remote.signature
13 const logger = require('../../../helpers/logger')
14 const friends = require('../../../lib/friends')
15 const databaseUtils = require('../../../helpers/database-utils')
16
17 const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS]
18
19 // Functions to call when processing a remote request
20 const functionsHash = {}
21 functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
22 functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
23 functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
24 functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
25
26 const router = express.Router()
27
28 router.post('/',
29 signatureValidators.signature,
30 secureMiddleware.checkSignature,
31 videosValidators.remoteVideos,
32 remoteVideos
33 )
34
35 router.post('/qadu',
36 signatureValidators.signature,
37 secureMiddleware.checkSignature,
38 videosValidators.remoteQaduVideos,
39 remoteVideosQadu
40 )
41
42 router.post('/events',
43 signatureValidators.signature,
44 secureMiddleware.checkSignature,
45 videosValidators.remoteEventsVideos,
46 remoteVideosEvents
47 )
48
49 // ---------------------------------------------------------------------------
50
51 module.exports = router
52
53 // ---------------------------------------------------------------------------
54
55 function remoteVideos (req, res, next) {
56 const requests = req.body.data
57 const fromPod = res.locals.secure.pod
58
59 // We need to process in the same order to keep consistency
60 // TODO: optimization
61 eachSeries(requests, function (request, callbackEach) {
62 const data = request.data
63
64 // Get the function we need to call in order to process the request
65 const fun = functionsHash[request.type]
66 if (fun === undefined) {
67 logger.error('Unkown remote request type %s.', request.type)
68 return callbackEach(null)
69 }
70
71 fun.call(this, data, fromPod, callbackEach)
72 }, function (err) {
73 if (err) logger.error('Error managing remote videos.', { error: err })
74 })
75
76 // We don't need to keep the other pod waiting
77 return res.type('json').status(204).end()
78 }
79
80 function remoteVideosQadu (req, res, next) {
81 const requests = req.body.data
82 const fromPod = res.locals.secure.pod
83
84 eachSeries(requests, function (request, callbackEach) {
85 const videoData = request.data
86
87 quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach)
88 }, function (err) {
89 if (err) logger.error('Error managing remote videos.', { error: err })
90 })
91
92 return res.type('json').status(204).end()
93 }
94
95 function remoteVideosEvents (req, res, next) {
96 const requests = req.body.data
97 const fromPod = res.locals.secure.pod
98
99 eachSeries(requests, function (request, callbackEach) {
100 const eventData = request.data
101
102 processVideosEventsRetryWrapper(eventData, fromPod, callbackEach)
103 }, function (err) {
104 if (err) logger.error('Error managing remote videos.', { error: err })
105 })
106
107 return res.type('json').status(204).end()
108 }
109
110 function processVideosEventsRetryWrapper (eventData, fromPod, finalCallback) {
111 const options = {
112 arguments: [ eventData, fromPod ],
113 errorMessage: 'Cannot process videos events with many retries.'
114 }
115
116 databaseUtils.retryTransactionWrapper(processVideosEvents, options, finalCallback)
117 }
118
119 function processVideosEvents (eventData, fromPod, finalCallback) {
120 waterfall([
121 databaseUtils.startSerializableTransaction,
122
123 function findVideo (t, callback) {
124 fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) {
125 return callback(err, t, videoInstance)
126 })
127 },
128
129 function updateVideoIntoDB (t, videoInstance, callback) {
130 const options = { transaction: t }
131
132 let columnToUpdate
133 let qaduType
134
135 switch (eventData.eventType) {
136 case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS:
137 columnToUpdate = 'views'
138 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
139 break
140
141 case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES:
142 columnToUpdate = 'likes'
143 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.LIKES
144 break
145
146 case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
147 columnToUpdate = 'dislikes'
148 qaduType = constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
149 break
150
151 default:
152 return callback(new Error('Unknown video event type.'))
153 }
154
155 const query = {}
156 query[columnToUpdate] = eventData.count
157
158 videoInstance.increment(query, options).asCallback(function (err) {
159 return callback(err, t, videoInstance, qaduType)
160 })
161 },
162
163 function sendQaduToFriends (t, videoInstance, qaduType, callback) {
164 const qadusParams = [
165 {
166 videoId: videoInstance.id,
167 type: qaduType
168 }
169 ]
170
171 friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
172 return callback(err, t)
173 })
174 },
175
176 databaseUtils.commitTransaction
177
178 ], function (err, t) {
179 if (err) {
180 logger.debug('Cannot process a video event.', { error: err })
181 return databaseUtils.rollbackTransaction(err, t, finalCallback)
182 }
183
184 logger.info('Remote video event processed for video %s.', eventData.remoteId)
185 return finalCallback(null)
186 })
187 }
188
189 function quickAndDirtyUpdateVideoRetryWrapper (videoData, fromPod, finalCallback) {
190 const options = {
191 arguments: [ videoData, fromPod ],
192 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
193 }
194
195 databaseUtils.retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback)
196 }
197
198 function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) {
199 let videoName
200
201 waterfall([
202 databaseUtils.startSerializableTransaction,
203
204 function findVideo (t, callback) {
205 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
206 return callback(err, t, videoInstance)
207 })
208 },
209
210 function updateVideoIntoDB (t, videoInstance, callback) {
211 const options = { transaction: t }
212
213 videoName = videoInstance.name
214
215 if (videoData.views) {
216 videoInstance.set('views', videoData.views)
217 }
218
219 if (videoData.likes) {
220 videoInstance.set('likes', videoData.likes)
221 }
222
223 if (videoData.dislikes) {
224 videoInstance.set('dislikes', videoData.dislikes)
225 }
226
227 videoInstance.save(options).asCallback(function (err) {
228 return callback(err, t)
229 })
230 },
231
232 databaseUtils.commitTransaction
233
234 ], function (err, t) {
235 if (err) {
236 logger.debug('Cannot quick and dirty update the remote video.', { error: err })
237 return databaseUtils.rollbackTransaction(err, t, finalCallback)
238 }
239
240 logger.info('Remote video %s quick and dirty updated', videoName)
241 return finalCallback(null)
242 })
243 }
244
245 // Handle retries on fail
246 function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
247 const options = {
248 arguments: [ videoToCreateData, fromPod ],
249 errorMessage: 'Cannot insert the remote video with many retries.'
250 }
251
252 databaseUtils.retryTransactionWrapper(addRemoteVideo, options, finalCallback)
253 }
254
255 function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
256 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
257
258 waterfall([
259
260 databaseUtils.startSerializableTransaction,
261
262 function assertRemoteIdAndHostUnique (t, callback) {
263 db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
264 if (err) return callback(err)
265
266 if (video) return callback(new Error('RemoteId and host pair is not unique.'))
267
268 return callback(null, t)
269 })
270 },
271
272 function findOrCreateAuthor (t, callback) {
273 const name = videoToCreateData.author
274 const podId = fromPod.id
275 // This author is from another pod so we do not associate a user
276 const userId = null
277
278 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
279 return callback(err, t, authorInstance)
280 })
281 },
282
283 function findOrCreateTags (t, author, callback) {
284 const tags = videoToCreateData.tags
285
286 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
287 return callback(err, t, author, tagInstances)
288 })
289 },
290
291 function createVideoObject (t, author, tagInstances, callback) {
292 const videoData = {
293 name: videoToCreateData.name,
294 remoteId: videoToCreateData.remoteId,
295 extname: videoToCreateData.extname,
296 infoHash: videoToCreateData.infoHash,
297 category: videoToCreateData.category,
298 licence: videoToCreateData.licence,
299 nsfw: videoToCreateData.nsfw,
300 description: videoToCreateData.description,
301 authorId: author.id,
302 duration: videoToCreateData.duration,
303 createdAt: videoToCreateData.createdAt,
304 // FIXME: updatedAt does not seems to be considered by Sequelize
305 updatedAt: videoToCreateData.updatedAt,
306 views: videoToCreateData.views,
307 likes: videoToCreateData.likes,
308 dislikes: videoToCreateData.dislikes
309 }
310
311 const video = db.Video.build(videoData)
312
313 return callback(null, t, tagInstances, video)
314 },
315
316 function generateThumbnail (t, tagInstances, video, callback) {
317 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
318 if (err) {
319 logger.error('Cannot generate thumbnail from data.', { error: err })
320 return callback(err)
321 }
322
323 return callback(err, t, tagInstances, video)
324 })
325 },
326
327 function insertVideoIntoDB (t, tagInstances, video, callback) {
328 const options = {
329 transaction: t
330 }
331
332 video.save(options).asCallback(function (err, videoCreated) {
333 return callback(err, t, tagInstances, videoCreated)
334 })
335 },
336
337 function associateTagsToVideo (t, tagInstances, video, callback) {
338 const options = {
339 transaction: t
340 }
341
342 video.setTags(tagInstances, options).asCallback(function (err) {
343 return callback(err, t)
344 })
345 },
346
347 databaseUtils.commitTransaction
348
349 ], function (err, t) {
350 if (err) {
351 // This is just a debug because we will retry the insert
352 logger.debug('Cannot insert the remote video.', { error: err })
353 return databaseUtils.rollbackTransaction(err, t, finalCallback)
354 }
355
356 logger.info('Remote video %s inserted.', videoToCreateData.name)
357 return finalCallback(null)
358 })
359 }
360
361 // Handle retries on fail
362 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
363 const options = {
364 arguments: [ videoAttributesToUpdate, fromPod ],
365 errorMessage: 'Cannot update the remote video with many retries'
366 }
367
368 databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
369 }
370
371 function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
372 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
373
374 waterfall([
375
376 databaseUtils.startSerializableTransaction,
377
378 function findVideo (t, callback) {
379 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
380 return callback(err, t, videoInstance)
381 })
382 },
383
384 function findOrCreateTags (t, videoInstance, callback) {
385 const tags = videoAttributesToUpdate.tags
386
387 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
388 return callback(err, t, videoInstance, tagInstances)
389 })
390 },
391
392 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
393 const options = { transaction: t }
394
395 videoInstance.set('name', videoAttributesToUpdate.name)
396 videoInstance.set('category', videoAttributesToUpdate.category)
397 videoInstance.set('licence', videoAttributesToUpdate.licence)
398 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
399 videoInstance.set('description', videoAttributesToUpdate.description)
400 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
401 videoInstance.set('duration', videoAttributesToUpdate.duration)
402 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
403 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
404 videoInstance.set('extname', videoAttributesToUpdate.extname)
405 videoInstance.set('views', videoAttributesToUpdate.views)
406 videoInstance.set('likes', videoAttributesToUpdate.likes)
407 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
408
409 videoInstance.save(options).asCallback(function (err) {
410 return callback(err, t, videoInstance, tagInstances)
411 })
412 },
413
414 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
415 const options = { transaction: t }
416
417 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
418 return callback(err, t)
419 })
420 },
421
422 databaseUtils.commitTransaction
423
424 ], function (err, t) {
425 if (err) {
426 // This is just a debug because we will retry the insert
427 logger.debug('Cannot update the remote video.', { error: err })
428 return databaseUtils.rollbackTransaction(err, t, finalCallback)
429 }
430
431 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
432 return finalCallback(null)
433 })
434 }
435
436 function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
437 // We need the instance because we have to remove some other stuffs (thumbnail etc)
438 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
439 // Do not return the error, continue the process
440 if (err) return callback(null)
441
442 logger.debug('Removing remote video %s.', video.remoteId)
443 video.destroy().asCallback(function (err) {
444 // Do not return the error, continue the process
445 if (err) {
446 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
447 }
448
449 return callback(null)
450 })
451 })
452 }
453
454 function reportAbuseRemoteVideo (reportData, fromPod, callback) {
455 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
456 if (err || !video) {
457 if (!err) err = new Error('video not found')
458
459 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
460 // Do not return the error, continue the process
461 return callback(null)
462 }
463
464 logger.debug('Reporting remote abuse for video %s.', video.id)
465
466 const videoAbuseData = {
467 reporterUsername: reportData.reporterUsername,
468 reason: reportData.reportReason,
469 reporterPodId: fromPod.id,
470 videoId: video.id
471 }
472
473 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
474 if (err) {
475 logger.error('Cannot create remote abuse video.', { error: err })
476 }
477
478 return callback(null)
479 })
480 })
481 }
482
483 function fetchOwnedVideo (id, callback) {
484 db.Video.load(id, function (err, video) {
485 if (err || !video) {
486 if (!err) err = new Error('video not found')
487
488 logger.error('Cannot load owned video from id.', { error: err, id })
489 return callback(err)
490 }
491
492 return callback(null, video)
493 })
494 }
495
496 function fetchRemoteVideo (podHost, remoteId, callback) {
497 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
498 if (err || !video) {
499 if (!err) err = new Error('video not found')
500
501 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
502 return callback(err)
503 }
504
505 return callback(null, video)
506 })
507 }