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