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