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