]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/remote/videos.js
Add ability for an administrator to remove any video (#61)
[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 language: videoToCreateData.language,
300 nsfw: videoToCreateData.nsfw,
301 description: videoToCreateData.description,
302 authorId: author.id,
303 duration: videoToCreateData.duration,
304 createdAt: videoToCreateData.createdAt,
305 // FIXME: updatedAt does not seems to be considered by Sequelize
306 updatedAt: videoToCreateData.updatedAt,
307 views: videoToCreateData.views,
308 likes: videoToCreateData.likes,
309 dislikes: videoToCreateData.dislikes
310 }
311
312 const video = db.Video.build(videoData)
313
314 return callback(null, t, tagInstances, video)
315 },
316
317 function generateThumbnail (t, tagInstances, video, callback) {
318 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
319 if (err) {
320 logger.error('Cannot generate thumbnail from data.', { error: err })
321 return callback(err)
322 }
323
324 return callback(err, t, tagInstances, video)
325 })
326 },
327
328 function insertVideoIntoDB (t, tagInstances, video, callback) {
329 const options = {
330 transaction: t
331 }
332
333 video.save(options).asCallback(function (err, videoCreated) {
334 return callback(err, t, tagInstances, videoCreated)
335 })
336 },
337
338 function associateTagsToVideo (t, tagInstances, video, callback) {
339 const options = {
340 transaction: t
341 }
342
343 video.setTags(tagInstances, options).asCallback(function (err) {
344 return callback(err, t)
345 })
346 },
347
348 databaseUtils.commitTransaction
349
350 ], function (err, t) {
351 if (err) {
352 // This is just a debug because we will retry the insert
353 logger.debug('Cannot insert the remote video.', { error: err })
354 return databaseUtils.rollbackTransaction(err, t, finalCallback)
355 }
356
357 logger.info('Remote video %s inserted.', videoToCreateData.name)
358 return finalCallback(null)
359 })
360 }
361
362 // Handle retries on fail
363 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
364 const options = {
365 arguments: [ videoAttributesToUpdate, fromPod ],
366 errorMessage: 'Cannot update the remote video with many retries'
367 }
368
369 databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
370 }
371
372 function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
373 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
374
375 waterfall([
376
377 databaseUtils.startSerializableTransaction,
378
379 function findVideo (t, callback) {
380 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
381 return callback(err, t, videoInstance)
382 })
383 },
384
385 function findOrCreateTags (t, videoInstance, callback) {
386 const tags = videoAttributesToUpdate.tags
387
388 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
389 return callback(err, t, videoInstance, tagInstances)
390 })
391 },
392
393 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
394 const options = { transaction: t }
395
396 videoInstance.set('name', videoAttributesToUpdate.name)
397 videoInstance.set('category', videoAttributesToUpdate.category)
398 videoInstance.set('licence', videoAttributesToUpdate.licence)
399 videoInstance.set('language', videoAttributesToUpdate.language)
400 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
401 videoInstance.set('description', videoAttributesToUpdate.description)
402 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
403 videoInstance.set('duration', videoAttributesToUpdate.duration)
404 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
405 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
406 videoInstance.set('extname', videoAttributesToUpdate.extname)
407 videoInstance.set('views', videoAttributesToUpdate.views)
408 videoInstance.set('likes', videoAttributesToUpdate.likes)
409 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
410
411 videoInstance.save(options).asCallback(function (err) {
412 return callback(err, t, videoInstance, tagInstances)
413 })
414 },
415
416 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
417 const options = { transaction: t }
418
419 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
420 return callback(err, t)
421 })
422 },
423
424 databaseUtils.commitTransaction
425
426 ], function (err, t) {
427 if (err) {
428 // This is just a debug because we will retry the insert
429 logger.debug('Cannot update the remote video.', { error: err })
430 return databaseUtils.rollbackTransaction(err, t, finalCallback)
431 }
432
433 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
434 return finalCallback(null)
435 })
436 }
437
438 function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
439 // We need the instance because we have to remove some other stuffs (thumbnail etc)
440 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
441 // Do not return the error, continue the process
442 if (err) return callback(null)
443
444 logger.debug('Removing remote video %s.', video.remoteId)
445 video.destroy().asCallback(function (err) {
446 // Do not return the error, continue the process
447 if (err) {
448 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
449 }
450
451 return callback(null)
452 })
453 })
454 }
455
456 function reportAbuseRemoteVideo (reportData, fromPod, callback) {
457 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
458 if (err || !video) {
459 if (!err) err = new Error('video not found')
460
461 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
462 // Do not return the error, continue the process
463 return callback(null)
464 }
465
466 logger.debug('Reporting remote abuse for video %s.', video.id)
467
468 const videoAbuseData = {
469 reporterUsername: reportData.reporterUsername,
470 reason: reportData.reportReason,
471 reporterPodId: fromPod.id,
472 videoId: video.id
473 }
474
475 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
476 if (err) {
477 logger.error('Cannot create remote abuse video.', { error: err })
478 }
479
480 return callback(null)
481 })
482 })
483 }
484
485 function fetchOwnedVideo (id, callback) {
486 db.Video.load(id, function (err, video) {
487 if (err || !video) {
488 if (!err) err = new Error('video not found')
489
490 logger.error('Cannot load owned video from id.', { error: err, id })
491 return callback(err)
492 }
493
494 return callback(null, video)
495 })
496 }
497
498 function fetchRemoteVideo (podHost, remoteId, callback) {
499 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
500 if (err || !video) {
501 if (!err) err = new Error('video not found')
502
503 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
504 return callback(err)
505 }
506
507 return callback(null, video)
508 })
509 }