]>
Commit | Line | Data |
---|---|---|
4d4e5cd4 | 1 | import * as express from 'express' |
6fcd19ba | 2 | import * as Promise from 'bluebird' |
528a9efa | 3 | |
e02643f3 | 4 | import { database as db } from '../../../initializers/database' |
65fcc311 C |
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' | |
6fcd19ba | 18 | import { logger, retryTransactionWrapper } from '../../../helpers' |
65fcc311 | 19 | import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib' |
556ddc31 | 20 | import { PodInstance } from '../../../models' |
4771e000 C |
21 | import { |
22 | RemoteVideoRequest, | |
23 | RemoteVideoCreateData, | |
24 | RemoteVideoUpdateData, | |
25 | RemoteVideoRemoveData, | |
26 | RemoteVideoReportAbuseData, | |
27 | RemoteQaduVideoRequest, | |
28 | RemoteQaduVideoData, | |
29 | RemoteVideoEventRequest, | |
30 | RemoteVideoEventData | |
31 | } from '../../../../shared' | |
65fcc311 C |
32 | |
33 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] | |
62f4ef41 C |
34 | |
35 | // Functions to call when processing a remote request | |
6fcd19ba | 36 | const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} |
62f4ef41 C |
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 | ||
65fcc311 | 42 | const remoteVideosRouter = express.Router() |
528a9efa | 43 | |
65fcc311 C |
44 | remoteVideosRouter.post('/', |
45 | signatureValidator, | |
46 | checkSignature, | |
47 | remoteVideosValidator, | |
528a9efa C |
48 | remoteVideos |
49 | ) | |
50 | ||
65fcc311 C |
51 | remoteVideosRouter.post('/qadu', |
52 | signatureValidator, | |
53 | checkSignature, | |
54 | remoteQaduVideosValidator, | |
9e167724 C |
55 | remoteVideosQadu |
56 | ) | |
57 | ||
65fcc311 C |
58 | remoteVideosRouter.post('/events', |
59 | signatureValidator, | |
60 | checkSignature, | |
61 | remoteEventsVideosValidator, | |
e4c87ec2 C |
62 | remoteVideosEvents |
63 | ) | |
64 | ||
528a9efa C |
65 | // --------------------------------------------------------------------------- |
66 | ||
65fcc311 C |
67 | export { |
68 | remoteVideosRouter | |
69 | } | |
528a9efa C |
70 | |
71 | // --------------------------------------------------------------------------- | |
72 | ||
69818c93 | 73 | function 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 | 96 | function 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 | 110 | function 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 | 124 | function 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 | 133 | function 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 | }) |
0a6658fd | 179 | .then(() => logger.info('Remote video event processed for video %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 | 186 | function 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 | 195 | function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) { |
f148e5ed C |
196 | let videoName |
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 | |
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 | 225 | function 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 | 234 | function 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 | }) | |
318 | .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name)) | |
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 | 326 | function 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 | 335 | function 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) | |
354 | videoInstance.set('infoHash', videoAttributesToUpdate.infoHash) | |
355 | videoInstance.set('duration', videoAttributesToUpdate.duration) | |
356 | videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) | |
357 | videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) | |
358 | videoInstance.set('extname', videoAttributesToUpdate.extname) | |
359 | videoInstance.set('views', videoAttributesToUpdate.views) | |
360 | videoInstance.set('likes', videoAttributesToUpdate.likes) | |
361 | videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) | |
362 | ||
363 | return videoInstance.save(options).then(() => ({ videoInstance, tagInstances })) | |
3d118fb5 | 364 | }) |
93e1258c C |
365 | .then(({ tagInstances, videoInstance }) => { |
366 | const tasks = [] | |
367 | const options = { | |
368 | transaction: t | |
369 | } | |
370 | ||
371 | videoAttributesToUpdate.files.forEach(fileData => { | |
372 | const videoFileInstance = db.VideoFile.build({ | |
373 | extname: fileData.extname, | |
374 | infoHash: fileData.infoHash, | |
375 | resolution: fileData.resolution, | |
376 | size: fileData.size, | |
377 | videoId: videoInstance.id | |
378 | }) | |
379 | ||
380 | tasks.push(videoFileInstance.save(options)) | |
381 | }) | |
382 | ||
383 | return Promise.all(tasks).then(() => ({ tagInstances, videoInstance })) | |
384 | }) | |
6fcd19ba C |
385 | .then(({ videoInstance, tagInstances }) => { |
386 | const options = { transaction: t } | |
3d118fb5 | 387 | |
6fcd19ba | 388 | return videoInstance.setTags(tagInstances, options) |
3d118fb5 | 389 | }) |
6fcd19ba C |
390 | }) |
391 | .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name)) | |
392 | .catch(err => { | |
393 | // This is just a debug because we will retry the insert | |
ad0997ad | 394 | logger.debug('Cannot update the remote video.', err) |
6fcd19ba | 395 | throw err |
3d118fb5 C |
396 | }) |
397 | } | |
398 | ||
4771e000 | 399 | function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { |
3d118fb5 | 400 | // We need the instance because we have to remove some other stuffs (thumbnail etc) |
0a6658fd | 401 | return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid) |
6fcd19ba | 402 | .then(video => { |
0a6658fd | 403 | logger.debug('Removing remote video %s.', video.uuid) |
6fcd19ba C |
404 | return video.destroy() |
405 | }) | |
406 | .catch(err => { | |
0a6658fd | 407 | logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack }) |
d8cc063e | 408 | }) |
55fa55a9 C |
409 | } |
410 | ||
4771e000 | 411 | function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { |
0a6658fd | 412 | return fetchVideoByUUID(reportData.videoUUID) |
6fcd19ba C |
413 | .then(video => { |
414 | logger.debug('Reporting remote abuse for video %s.', video.id) | |
55fa55a9 | 415 | |
6fcd19ba C |
416 | const videoAbuseData = { |
417 | reporterUsername: reportData.reporterUsername, | |
418 | reason: reportData.reportReason, | |
419 | reporterPodId: fromPod.id, | |
420 | videoId: video.id | |
d8cc063e C |
421 | } |
422 | ||
6fcd19ba | 423 | return db.VideoAbuse.create(videoAbuseData) |
d8cc063e | 424 | }) |
ad0997ad | 425 | .catch(err => logger.error('Cannot create remote abuse video.', err)) |
55fa55a9 C |
426 | } |
427 | ||
0a6658fd C |
428 | function fetchVideoByUUID (id: string) { |
429 | return db.Video.loadByUUID(id) | |
6fcd19ba C |
430 | .then(video => { |
431 | if (!video) throw new Error('Video not found') | |
e4c87ec2 | 432 | |
6fcd19ba C |
433 | return video |
434 | }) | |
435 | .catch(err => { | |
ad0997ad | 436 | logger.error('Cannot load owned video from id.', { error: err.stack, id }) |
6fcd19ba C |
437 | throw err |
438 | }) | |
e4c87ec2 C |
439 | } |
440 | ||
0a6658fd C |
441 | function fetchVideoByHostAndUUID (podHost: string, uuid: string) { |
442 | return db.Video.loadByHostAndUUID(podHost, uuid) | |
6fcd19ba C |
443 | .then(video => { |
444 | if (!video) throw new Error('Video not found') | |
55fa55a9 | 445 | |
6fcd19ba C |
446 | return video |
447 | }) | |
448 | .catch(err => { | |
0a6658fd | 449 | logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid }) |
6fcd19ba C |
450 | throw err |
451 | }) | |
528a9efa | 452 | } |