]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/remote/videos.ts
hide error message in https too (#108)
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.ts
CommitLineData
4d4e5cd4 1import * as express from 'express'
6fcd19ba 2import * as Promise from 'bluebird'
528a9efa 3
e02643f3 4import { database as db } from '../../../initializers/database'
65fcc311
C
5import {
6 REQUEST_ENDPOINT_ACTIONS,
7 REQUEST_ENDPOINTS,
8 REQUEST_VIDEO_EVENT_TYPES,
9 REQUEST_VIDEO_QADU_TYPES
10} from '../../../initializers'
11import {
12 checkSignature,
13 signatureValidator,
14 remoteVideosValidator,
15 remoteQaduVideosValidator,
16 remoteEventsVideosValidator
17} from '../../../middlewares'
6fcd19ba 18import { logger, retryTransactionWrapper } from '../../../helpers'
65fcc311 19import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
6d33593a 20import { PodInstance, VideoFileInstance } from '../../../models'
4771e000
C
21import {
22 RemoteVideoRequest,
23 RemoteVideoCreateData,
24 RemoteVideoUpdateData,
25 RemoteVideoRemoveData,
26 RemoteVideoReportAbuseData,
27 RemoteQaduVideoRequest,
28 RemoteQaduVideoData,
29 RemoteVideoEventRequest,
30 RemoteVideoEventData
31} from '../../../../shared'
65fcc311
C
32
33const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
62f4ef41
C
34
35// Functions to call when processing a remote request
6fcd19ba 36const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
62f4ef41
C
37functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
38functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
39functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
40functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo
41
65fcc311 42const remoteVideosRouter = express.Router()
528a9efa 43
65fcc311
C
44remoteVideosRouter.post('/',
45 signatureValidator,
46 checkSignature,
47 remoteVideosValidator,
528a9efa
C
48 remoteVideos
49)
50
65fcc311
C
51remoteVideosRouter.post('/qadu',
52 signatureValidator,
53 checkSignature,
54 remoteQaduVideosValidator,
9e167724
C
55 remoteVideosQadu
56)
57
65fcc311
C
58remoteVideosRouter.post('/events',
59 signatureValidator,
60 checkSignature,
61 remoteEventsVideosValidator,
e4c87ec2
C
62 remoteVideosEvents
63)
64
528a9efa
C
65// ---------------------------------------------------------------------------
66
65fcc311
C
67export {
68 remoteVideosRouter
69}
528a9efa
C
70
71// ---------------------------------------------------------------------------
72
69818c93 73function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
4771e000 74 const requests: RemoteVideoRequest[] = req.body.data
4ff0d862 75 const fromPod = res.locals.secure.pod
528a9efa
C
76
77 // We need to process in the same order to keep consistency
4771e000 78 Promise.each(requests, request => {
55fa55a9 79 const data = request.data
528a9efa 80
62f4ef41
C
81 // Get the function we need to call in order to process the request
82 const fun = functionsHash[request.type]
83 if (fun === undefined) {
6d33593a 84 logger.error('Unknown remote request type %s.', request.type)
6fcd19ba 85 return
528a9efa 86 }
62f4ef41 87
6fcd19ba 88 return fun.call(this, data, fromPod)
528a9efa 89 })
ad0997ad 90 .catch(err => logger.error('Error managing remote videos.', err))
528a9efa 91
709756b8 92 // Don't block the other pod
528a9efa
C
93 return res.type('json').status(204).end()
94}
95
69818c93 96function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
4771e000 97 const requests: RemoteQaduVideoRequest[] = req.body.data
9e167724
C
98 const fromPod = res.locals.secure.pod
99
4771e000 100 Promise.each(requests, request => {
9e167724
C
101 const videoData = request.data
102
6fcd19ba 103 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
9e167724 104 })
ad0997ad 105 .catch(err => logger.error('Error managing remote videos.', err))
9e167724
C
106
107 return res.type('json').status(204).end()
108}
109
69818c93 110function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
4771e000 111 const requests: RemoteVideoEventRequest[] = req.body.data
e4c87ec2
C
112 const fromPod = res.locals.secure.pod
113
4771e000 114 Promise.each(requests, request => {
e4c87ec2
C
115 const eventData = request.data
116
6fcd19ba 117 return processVideosEventsRetryWrapper(eventData, fromPod)
e4c87ec2 118 })
ad0997ad 119 .catch(err => logger.error('Error managing remote videos.', err))
e4c87ec2
C
120
121 return res.type('json').status(204).end()
122}
123
4771e000 124function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) {
e4c87ec2
C
125 const options = {
126 arguments: [ eventData, fromPod ],
127 errorMessage: 'Cannot process videos events with many retries.'
128 }
129
6fcd19ba 130 return retryTransactionWrapper(processVideosEvents, options)
e4c87ec2
C
131}
132
4771e000 133function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
e4c87ec2 134
6fcd19ba 135 return db.sequelize.transaction(t => {
0a6658fd 136 return fetchVideoByUUID(eventData.uuid)
6fcd19ba
C
137 .then(videoInstance => {
138 const options = { transaction: t }
e4c87ec2 139
6fcd19ba
C
140 let columnToUpdate
141 let qaduType
e4c87ec2 142
6fcd19ba 143 switch (eventData.eventType) {
980246ea
C
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.')
6fcd19ba 161 }
e4c87ec2 162
6fcd19ba
C
163 const query = {}
164 query[columnToUpdate] = eventData.count
e4c87ec2 165
6fcd19ba 166 return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType }))
d38b8281 167 })
6fcd19ba
C
168 .then(({ videoInstance, qaduType }) => {
169 const qadusParams = [
170 {
171 videoId: videoInstance.id,
172 type: qaduType
173 }
174 ]
175
176 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
e4c87ec2 177 })
6fcd19ba 178 })
6d33593a 179 .then(() => logger.info('Remote video event processed for video with uuid %s.', eventData.uuid))
6fcd19ba 180 .catch(err => {
ad0997ad 181 logger.debug('Cannot process a video event.', err)
6fcd19ba 182 throw err
e4c87ec2
C
183 })
184}
185
4771e000 186function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
9e167724
C
187 const options = {
188 arguments: [ videoData, fromPod ],
189 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
190 }
191
6fcd19ba 192 return retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
9e167724
C
193}
194
4771e000 195function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
6d33593a 196 let videoUUID = ''
f148e5ed 197
6fcd19ba 198 return db.sequelize.transaction(t => {
0a6658fd 199 return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid)
6fcd19ba
C
200 .then(videoInstance => {
201 const options = { transaction: t }
9e167724 202
6d33593a 203 videoUUID = videoInstance.uuid
f148e5ed 204
6fcd19ba
C
205 if (videoData.views) {
206 videoInstance.set('views', videoData.views)
207 }
9e167724 208
6fcd19ba
C
209 if (videoData.likes) {
210 videoInstance.set('likes', videoData.likes)
211 }
9e167724 212
6fcd19ba
C
213 if (videoData.dislikes) {
214 videoInstance.set('dislikes', videoData.dislikes)
215 }
9e167724 216
6fcd19ba 217 return videoInstance.save(options)
9e167724 218 })
9e167724 219 })
6d33593a 220 .then(() => logger.info('Remote video with uuid %s quick and dirty updated', videoUUID))
ad0997ad 221 .catch(err => logger.debug('Cannot quick and dirty update the remote video.', err))
9e167724
C
222}
223
ed04d94f 224// Handle retries on fail
4771e000 225function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
d6a5b018
C
226 const options = {
227 arguments: [ videoToCreateData, fromPod ],
228 errorMessage: 'Cannot insert the remote video with many retries.'
229 }
ed04d94f 230
6fcd19ba 231 return retryTransactionWrapper(addRemoteVideo, options)
ed04d94f
C
232}
233
4771e000 234function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
0a6658fd 235 logger.debug('Adding remote video "%s".', videoToCreateData.uuid)
6666aad4 236
6fcd19ba 237 return db.sequelize.transaction(t => {
0a6658fd 238 return db.Video.loadByUUID(videoToCreateData.uuid)
6fcd19ba 239 .then(video => {
0a6658fd 240 if (video) throw new Error('UUID already exists.')
cddadde8 241
6fcd19ba 242 return undefined
cddadde8 243 })
6fcd19ba
C
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
feb4bdfd 249
6fcd19ba 250 return db.Author.findOrCreateAuthor(name, podId, userId, t)
feb4bdfd 251 })
6fcd19ba
C
252 .then(author => {
253 const tags = videoToCreateData.tags
feb4bdfd 254
6fcd19ba 255 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
7920c273 256 })
6fcd19ba
C
257 .then(({ author, tagInstances }) => {
258 const videoData = {
259 name: videoToCreateData.name,
0a6658fd 260 uuid: videoToCreateData.uuid,
6fcd19ba
C
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,
0a6658fd
C
273 dislikes: videoToCreateData.dislikes,
274 remote: true
feb4bdfd
C
275 }
276
6fcd19ba
C
277 const video = db.Video.build(videoData)
278 return { tagInstances, video }
feb4bdfd 279 })
6fcd19ba
C
280 .then(({ tagInstances, video }) => {
281 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
7920c273 282 })
6fcd19ba
C
283 .then(({ tagInstances, video }) => {
284 const options = {
285 transaction: t
286 }
7920c273 287
6fcd19ba 288 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
7920c273 289 })
93e1258c
C
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 })
6fcd19ba
C
310 .then(({ tagInstances, videoCreated }) => {
311 const options = {
312 transaction: t
313 }
7920c273 314
6fcd19ba
C
315 return videoCreated.setTags(tagInstances, options)
316 })
317 })
6d33593a 318 .then(() => logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid))
6fcd19ba 319 .catch(err => {
ad0997ad 320 logger.debug('Cannot insert the remote video.', err)
6fcd19ba 321 throw err
7920c273 322 })
528a9efa
C
323}
324
ed04d94f 325// Handle retries on fail
4771e000 326function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
d6a5b018 327 const options = {
fbc22d79 328 arguments: [ videoAttributesToUpdate, fromPod ],
d6a5b018
C
329 errorMessage: 'Cannot update the remote video with many retries'
330 }
ed04d94f 331
6fcd19ba 332 return retryTransactionWrapper(updateRemoteVideo, options)
ed04d94f
C
333}
334
4771e000 335function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
0a6658fd 336 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
feb4bdfd 337
6fcd19ba 338 return db.sequelize.transaction(t => {
0a6658fd 339 return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid)
6fcd19ba
C
340 .then(videoInstance => {
341 const tags = videoAttributesToUpdate.tags
3d118fb5 342
6fcd19ba 343 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
3d118fb5 344 })
6fcd19ba
C
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)
6fcd19ba
C
354 videoInstance.set('duration', videoAttributesToUpdate.duration)
355 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
356 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
6fcd19ba
C
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 }))
3d118fb5 362 })
93e1258c 363 .then(({ tagInstances, videoInstance }) => {
6d33593a
C
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>[] = []
93e1258c
C
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 })
6fcd19ba
C
393 .then(({ videoInstance, tagInstances }) => {
394 const options = { transaction: t }
3d118fb5 395
6fcd19ba 396 return videoInstance.setTags(tagInstances, options)
3d118fb5 397 })
6fcd19ba 398 })
6d33593a 399 .then(() => logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid))
6fcd19ba
C
400 .catch(err => {
401 // This is just a debug because we will retry the insert
ad0997ad 402 logger.debug('Cannot update the remote video.', err)
6fcd19ba 403 throw err
3d118fb5
C
404 })
405}
406
4771e000 407function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
3d118fb5 408 // We need the instance because we have to remove some other stuffs (thumbnail etc)
0a6658fd 409 return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid)
6fcd19ba 410 .then(video => {
6d33593a 411 logger.debug('Removing remote video with uuid %s.', video.uuid)
6fcd19ba
C
412 return video.destroy()
413 })
414 .catch(err => {
0a6658fd 415 logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack })
d8cc063e 416 })
55fa55a9
C
417}
418
4771e000 419function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
0a6658fd 420 return fetchVideoByUUID(reportData.videoUUID)
6fcd19ba
C
421 .then(video => {
422 logger.debug('Reporting remote abuse for video %s.', video.id)
55fa55a9 423
6fcd19ba
C
424 const videoAbuseData = {
425 reporterUsername: reportData.reporterUsername,
426 reason: reportData.reportReason,
427 reporterPodId: fromPod.id,
428 videoId: video.id
d8cc063e
C
429 }
430
6fcd19ba 431 return db.VideoAbuse.create(videoAbuseData)
d8cc063e 432 })
ad0997ad 433 .catch(err => logger.error('Cannot create remote abuse video.', err))
55fa55a9
C
434}
435
0a6658fd
C
436function fetchVideoByUUID (id: string) {
437 return db.Video.loadByUUID(id)
6fcd19ba
C
438 .then(video => {
439 if (!video) throw new Error('Video not found')
e4c87ec2 440
6fcd19ba
C
441 return video
442 })
443 .catch(err => {
ad0997ad 444 logger.error('Cannot load owned video from id.', { error: err.stack, id })
6fcd19ba
C
445 throw err
446 })
e4c87ec2
C
447}
448
0a6658fd
C
449function fetchVideoByHostAndUUID (podHost: string, uuid: string) {
450 return db.Video.loadByHostAndUUID(podHost, uuid)
6fcd19ba
C
451 .then(video => {
452 if (!video) throw new Error('Video not found')
55fa55a9 453
6fcd19ba
C
454 return video
455 })
456 .catch(err => {
0a6658fd 457 logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
6fcd19ba
C
458 throw err
459 })
528a9efa 460}