]>
Commit | Line | Data |
---|---|---|
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 | } |