]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/remote/videos.ts
Better typescript typing for a better world
[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'
69818c93 20import { PodInstance, VideoInstance } 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) {
84 logger.error('Unkown 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
C
135 return db.sequelize.transaction(t => {
136 return fetchOwnedVideo(eventData.remoteId)
137 .then(videoInstance => {
138 const options = { transaction: t }
e4c87ec2 139
6fcd19ba
C
140 let columnToUpdate
141 let qaduType
e4c87ec2 142
6fcd19ba
C
143 switch (eventData.eventType) {
144 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
145 columnToUpdate = 'views'
146 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
147 break
e4c87ec2 148
6fcd19ba
C
149 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
150 columnToUpdate = 'likes'
151 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
152 break
e4c87ec2 153
6fcd19ba
C
154 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
155 columnToUpdate = 'dislikes'
156 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
157 break
e4c87ec2 158
6fcd19ba
C
159 default:
160 throw new Error('Unknown video event type.')
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
C
178 })
179 .then(() => logger.info('Remote video event processed for video %s.', eventData.remoteId))
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) {
f148e5ed
C
196 let videoName
197
6fcd19ba
C
198 return db.sequelize.transaction(t => {
199 return fetchRemoteVideo(fromPod.host, videoData.remoteId)
200 .then(videoInstance => {
201 const options = { transaction: t }
9e167724 202
6fcd19ba 203 videoName = videoInstance.name
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 })
6fcd19ba 220 .then(() => logger.info('Remote video %s quick and dirty updated', videoName))
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) {
ed04d94f 235 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
6666aad4 236
6fcd19ba
C
237 return db.sequelize.transaction(t => {
238 return db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId)
239 .then(video => {
240 if (video) throw new Error('RemoteId and host pair is not unique.')
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,
260 remoteId: videoToCreateData.remoteId,
261 extname: videoToCreateData.extname,
262 infoHash: videoToCreateData.infoHash,
263 category: videoToCreateData.category,
264 licence: videoToCreateData.licence,
265 language: videoToCreateData.language,
266 nsfw: videoToCreateData.nsfw,
267 description: videoToCreateData.description,
268 authorId: author.id,
269 duration: videoToCreateData.duration,
270 createdAt: videoToCreateData.createdAt,
271 // FIXME: updatedAt does not seems to be considered by Sequelize
272 updatedAt: videoToCreateData.updatedAt,
273 views: videoToCreateData.views,
274 likes: videoToCreateData.likes,
275 dislikes: videoToCreateData.dislikes
feb4bdfd
C
276 }
277
6fcd19ba
C
278 const video = db.Video.build(videoData)
279 return { tagInstances, video }
feb4bdfd 280 })
6fcd19ba
C
281 .then(({ tagInstances, video }) => {
282 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
7920c273 283 })
6fcd19ba
C
284 .then(({ tagInstances, video }) => {
285 const options = {
286 transaction: t
287 }
7920c273 288
6fcd19ba 289 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
7920c273 290 })
6fcd19ba
C
291 .then(({ tagInstances, videoCreated }) => {
292 const options = {
293 transaction: t
294 }
7920c273 295
6fcd19ba
C
296 return videoCreated.setTags(tagInstances, options)
297 })
298 })
299 .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name))
300 .catch(err => {
ad0997ad 301 logger.debug('Cannot insert the remote video.', err)
6fcd19ba 302 throw err
7920c273 303 })
528a9efa
C
304}
305
ed04d94f 306// Handle retries on fail
4771e000 307function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
d6a5b018 308 const options = {
fbc22d79 309 arguments: [ videoAttributesToUpdate, fromPod ],
d6a5b018
C
310 errorMessage: 'Cannot update the remote video with many retries'
311 }
ed04d94f 312
6fcd19ba 313 return retryTransactionWrapper(updateRemoteVideo, options)
ed04d94f
C
314}
315
4771e000 316function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
ed04d94f 317 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
feb4bdfd 318
6fcd19ba
C
319 return db.sequelize.transaction(t => {
320 return fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId)
321 .then(videoInstance => {
322 const tags = videoAttributesToUpdate.tags
3d118fb5 323
6fcd19ba 324 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
3d118fb5 325 })
6fcd19ba
C
326 .then(({ videoInstance, tagInstances }) => {
327 const options = { transaction: t }
328
329 videoInstance.set('name', videoAttributesToUpdate.name)
330 videoInstance.set('category', videoAttributesToUpdate.category)
331 videoInstance.set('licence', videoAttributesToUpdate.licence)
332 videoInstance.set('language', videoAttributesToUpdate.language)
333 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
334 videoInstance.set('description', videoAttributesToUpdate.description)
335 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
336 videoInstance.set('duration', videoAttributesToUpdate.duration)
337 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
338 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
339 videoInstance.set('extname', videoAttributesToUpdate.extname)
340 videoInstance.set('views', videoAttributesToUpdate.views)
341 videoInstance.set('likes', videoAttributesToUpdate.likes)
342 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
343
344 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
3d118fb5 345 })
6fcd19ba
C
346 .then(({ videoInstance, tagInstances }) => {
347 const options = { transaction: t }
3d118fb5 348
6fcd19ba 349 return videoInstance.setTags(tagInstances, options)
3d118fb5 350 })
6fcd19ba
C
351 })
352 .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name))
353 .catch(err => {
354 // This is just a debug because we will retry the insert
ad0997ad 355 logger.debug('Cannot update the remote video.', err)
6fcd19ba 356 throw err
3d118fb5
C
357 })
358}
359
4771e000 360function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
3d118fb5 361 // We need the instance because we have to remove some other stuffs (thumbnail etc)
6fcd19ba
C
362 return fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId)
363 .then(video => {
364 logger.debug('Removing remote video %s.', video.remoteId)
365 return video.destroy()
366 })
367 .catch(err => {
ad0997ad 368 logger.debug('Could not fetch remote video.', { host: fromPod.host, remoteId: videoToRemoveData.remoteId, error: err.stack })
d8cc063e 369 })
55fa55a9
C
370}
371
4771e000 372function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
6fcd19ba
C
373 return fetchOwnedVideo(reportData.videoRemoteId)
374 .then(video => {
375 logger.debug('Reporting remote abuse for video %s.', video.id)
55fa55a9 376
6fcd19ba
C
377 const videoAbuseData = {
378 reporterUsername: reportData.reporterUsername,
379 reason: reportData.reportReason,
380 reporterPodId: fromPod.id,
381 videoId: video.id
d8cc063e
C
382 }
383
6fcd19ba 384 return db.VideoAbuse.create(videoAbuseData)
d8cc063e 385 })
ad0997ad 386 .catch(err => logger.error('Cannot create remote abuse video.', err))
55fa55a9
C
387}
388
6fcd19ba
C
389function fetchOwnedVideo (id: string) {
390 return db.Video.load(id)
391 .then(video => {
392 if (!video) throw new Error('Video not found')
e4c87ec2 393
6fcd19ba
C
394 return video
395 })
396 .catch(err => {
ad0997ad 397 logger.error('Cannot load owned video from id.', { error: err.stack, id })
6fcd19ba
C
398 throw err
399 })
e4c87ec2
C
400}
401
6fcd19ba
C
402function fetchRemoteVideo (podHost: string, remoteId: string) {
403 return db.Video.loadByHostAndRemoteId(podHost, remoteId)
404 .then(video => {
405 if (!video) throw new Error('Video not found')
55fa55a9 406
6fcd19ba
C
407 return video
408 })
409 .catch(err => {
ad0997ad 410 logger.error('Cannot load video from host and remote id.', { error: err.stack, podHost, remoteId })
6fcd19ba
C
411 throw err
412 })
528a9efa 413}