]>
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, 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.', 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.', 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.', 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.', 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.', 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.', 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.', 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.stack }) | |
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.', 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.stack, 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.stack, podHost, remoteId }) | |
400 | throw err | |
401 | }) | |
402 | } |