]>
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' |
69818c93 | 20 | import { PodInstance, VideoInstance } from '../../../models' |
65fcc311 C |
21 | |
22 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] | |
62f4ef41 C |
23 | |
24 | // Functions to call when processing a remote request | |
6fcd19ba | 25 | const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} |
62f4ef41 C |
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 | ||
65fcc311 | 31 | const remoteVideosRouter = express.Router() |
528a9efa | 32 | |
65fcc311 C |
33 | remoteVideosRouter.post('/', |
34 | signatureValidator, | |
35 | checkSignature, | |
36 | remoteVideosValidator, | |
528a9efa C |
37 | remoteVideos |
38 | ) | |
39 | ||
65fcc311 C |
40 | remoteVideosRouter.post('/qadu', |
41 | signatureValidator, | |
42 | checkSignature, | |
43 | remoteQaduVideosValidator, | |
9e167724 C |
44 | remoteVideosQadu |
45 | ) | |
46 | ||
65fcc311 C |
47 | remoteVideosRouter.post('/events', |
48 | signatureValidator, | |
49 | checkSignature, | |
50 | remoteEventsVideosValidator, | |
e4c87ec2 C |
51 | remoteVideosEvents |
52 | ) | |
53 | ||
528a9efa C |
54 | // --------------------------------------------------------------------------- |
55 | ||
65fcc311 C |
56 | export { |
57 | remoteVideosRouter | |
58 | } | |
528a9efa C |
59 | |
60 | // --------------------------------------------------------------------------- | |
61 | ||
69818c93 | 62 | function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
528a9efa | 63 | const requests = req.body.data |
4ff0d862 | 64 | const fromPod = res.locals.secure.pod |
528a9efa C |
65 | |
66 | // We need to process in the same order to keep consistency | |
709756b8 | 67 | Promise.each(requests, (request: any) => { |
55fa55a9 | 68 | const data = request.data |
528a9efa | 69 | |
62f4ef41 C |
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) | |
6fcd19ba | 74 | return |
528a9efa | 75 | } |
62f4ef41 | 76 | |
6fcd19ba | 77 | return fun.call(this, data, fromPod) |
528a9efa | 78 | }) |
6fcd19ba | 79 | .catch(err => logger.error('Error managing remote videos.', { error: err })) |
528a9efa | 80 | |
709756b8 | 81 | // Don't block the other pod |
528a9efa C |
82 | return res.type('json').status(204).end() |
83 | } | |
84 | ||
69818c93 | 85 | function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) { |
9e167724 C |
86 | const requests = req.body.data |
87 | const fromPod = res.locals.secure.pod | |
88 | ||
709756b8 | 89 | Promise.each(requests, (request: any) => { |
9e167724 C |
90 | const videoData = request.data |
91 | ||
6fcd19ba | 92 | return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod) |
9e167724 | 93 | }) |
6fcd19ba | 94 | .catch(err => logger.error('Error managing remote videos.', { error: err })) |
9e167724 C |
95 | |
96 | return res.type('json').status(204).end() | |
97 | } | |
98 | ||
69818c93 | 99 | function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) { |
e4c87ec2 C |
100 | const requests = req.body.data |
101 | const fromPod = res.locals.secure.pod | |
102 | ||
709756b8 | 103 | Promise.each(requests, (request: any) => { |
e4c87ec2 C |
104 | const eventData = request.data |
105 | ||
6fcd19ba | 106 | return processVideosEventsRetryWrapper(eventData, fromPod) |
e4c87ec2 | 107 | }) |
6fcd19ba | 108 | .catch(err => logger.error('Error managing remote videos.', { error: err })) |
e4c87ec2 C |
109 | |
110 | return res.type('json').status(204).end() | |
111 | } | |
112 | ||
6fcd19ba | 113 | function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance) { |
e4c87ec2 C |
114 | const options = { |
115 | arguments: [ eventData, fromPod ], | |
116 | errorMessage: 'Cannot process videos events with many retries.' | |
117 | } | |
118 | ||
6fcd19ba | 119 | return retryTransactionWrapper(processVideosEvents, options) |
e4c87ec2 C |
120 | } |
121 | ||
6fcd19ba | 122 | function processVideosEvents (eventData: any, fromPod: PodInstance) { |
e4c87ec2 | 123 | |
6fcd19ba C |
124 | return db.sequelize.transaction(t => { |
125 | return fetchOwnedVideo(eventData.remoteId) | |
126 | .then(videoInstance => { | |
127 | const options = { transaction: t } | |
e4c87ec2 | 128 | |
6fcd19ba C |
129 | let columnToUpdate |
130 | let qaduType | |
e4c87ec2 | 131 | |
6fcd19ba C |
132 | switch (eventData.eventType) { |
133 | case REQUEST_VIDEO_EVENT_TYPES.VIEWS: | |
134 | columnToUpdate = 'views' | |
135 | qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS | |
136 | break | |
e4c87ec2 | 137 | |
6fcd19ba C |
138 | case REQUEST_VIDEO_EVENT_TYPES.LIKES: |
139 | columnToUpdate = 'likes' | |
140 | qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES | |
141 | break | |
e4c87ec2 | 142 | |
6fcd19ba C |
143 | case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: |
144 | columnToUpdate = 'dislikes' | |
145 | qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES | |
146 | break | |
e4c87ec2 | 147 | |
6fcd19ba C |
148 | default: |
149 | throw new Error('Unknown video event type.') | |
150 | } | |
e4c87ec2 | 151 | |
6fcd19ba C |
152 | const query = {} |
153 | query[columnToUpdate] = eventData.count | |
e4c87ec2 | 154 | |
6fcd19ba | 155 | return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType })) |
d38b8281 | 156 | }) |
6fcd19ba C |
157 | .then(({ videoInstance, qaduType }) => { |
158 | const qadusParams = [ | |
159 | { | |
160 | videoId: videoInstance.id, | |
161 | type: qaduType | |
162 | } | |
163 | ] | |
164 | ||
165 | return quickAndDirtyUpdatesVideoToFriends(qadusParams, t) | |
e4c87ec2 | 166 | }) |
6fcd19ba C |
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 | |
e4c87ec2 C |
172 | }) |
173 | } | |
174 | ||
6fcd19ba | 175 | function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance) { |
9e167724 C |
176 | const options = { |
177 | arguments: [ videoData, fromPod ], | |
178 | errorMessage: 'Cannot update quick and dirty the remote video with many retries.' | |
179 | } | |
180 | ||
6fcd19ba | 181 | return retryTransactionWrapper(quickAndDirtyUpdateVideo, options) |
9e167724 C |
182 | } |
183 | ||
6fcd19ba | 184 | function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance) { |
f148e5ed C |
185 | let videoName |
186 | ||
6fcd19ba C |
187 | return db.sequelize.transaction(t => { |
188 | return fetchRemoteVideo(fromPod.host, videoData.remoteId) | |
189 | .then(videoInstance => { | |
190 | const options = { transaction: t } | |
9e167724 | 191 | |
6fcd19ba | 192 | videoName = videoInstance.name |
f148e5ed | 193 | |
6fcd19ba C |
194 | if (videoData.views) { |
195 | videoInstance.set('views', videoData.views) | |
196 | } | |
9e167724 | 197 | |
6fcd19ba C |
198 | if (videoData.likes) { |
199 | videoInstance.set('likes', videoData.likes) | |
200 | } | |
9e167724 | 201 | |
6fcd19ba C |
202 | if (videoData.dislikes) { |
203 | videoInstance.set('dislikes', videoData.dislikes) | |
204 | } | |
9e167724 | 205 | |
6fcd19ba | 206 | return videoInstance.save(options) |
9e167724 | 207 | }) |
9e167724 | 208 | }) |
6fcd19ba C |
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 })) | |
9e167724 C |
211 | } |
212 | ||
ed04d94f | 213 | // Handle retries on fail |
6fcd19ba | 214 | function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance) { |
d6a5b018 C |
215 | const options = { |
216 | arguments: [ videoToCreateData, fromPod ], | |
217 | errorMessage: 'Cannot insert the remote video with many retries.' | |
218 | } | |
ed04d94f | 219 | |
6fcd19ba | 220 | return retryTransactionWrapper(addRemoteVideo, options) |
ed04d94f C |
221 | } |
222 | ||
6fcd19ba | 223 | function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance) { |
ed04d94f | 224 | logger.debug('Adding remote video "%s".', videoToCreateData.remoteId) |
6666aad4 | 225 | |
6fcd19ba C |
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.') | |
cddadde8 | 230 | |
6fcd19ba | 231 | return undefined |
cddadde8 | 232 | }) |
6fcd19ba C |
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 | |
feb4bdfd | 238 | |
6fcd19ba | 239 | return db.Author.findOrCreateAuthor(name, podId, userId, t) |
feb4bdfd | 240 | }) |
6fcd19ba C |
241 | .then(author => { |
242 | const tags = videoToCreateData.tags | |
feb4bdfd | 243 | |
6fcd19ba | 244 | return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances })) |
7920c273 | 245 | }) |
6fcd19ba C |
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 | |
feb4bdfd C |
265 | } |
266 | ||
6fcd19ba C |
267 | const video = db.Video.build(videoData) |
268 | return { tagInstances, video } | |
feb4bdfd | 269 | }) |
6fcd19ba C |
270 | .then(({ tagInstances, video }) => { |
271 | return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video })) | |
7920c273 | 272 | }) |
6fcd19ba C |
273 | .then(({ tagInstances, video }) => { |
274 | const options = { | |
275 | transaction: t | |
276 | } | |
7920c273 | 277 | |
6fcd19ba | 278 | return video.save(options).then(videoCreated => ({ tagInstances, videoCreated })) |
7920c273 | 279 | }) |
6fcd19ba C |
280 | .then(({ tagInstances, videoCreated }) => { |
281 | const options = { | |
282 | transaction: t | |
283 | } | |
7920c273 | 284 | |
6fcd19ba C |
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 | |
7920c273 | 292 | }) |
528a9efa C |
293 | } |
294 | ||
ed04d94f | 295 | // Handle retries on fail |
6fcd19ba | 296 | function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance) { |
d6a5b018 | 297 | const options = { |
fbc22d79 | 298 | arguments: [ videoAttributesToUpdate, fromPod ], |
d6a5b018 C |
299 | errorMessage: 'Cannot update the remote video with many retries' |
300 | } | |
ed04d94f | 301 | |
6fcd19ba | 302 | return retryTransactionWrapper(updateRemoteVideo, options) |
ed04d94f C |
303 | } |
304 | ||
6fcd19ba | 305 | function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance) { |
ed04d94f | 306 | logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId) |
feb4bdfd | 307 | |
6fcd19ba C |
308 | return db.sequelize.transaction(t => { |
309 | return fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId) | |
310 | .then(videoInstance => { | |
311 | const tags = videoAttributesToUpdate.tags | |
3d118fb5 | 312 | |
6fcd19ba | 313 | return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances })) |
3d118fb5 | 314 | }) |
6fcd19ba C |
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 })) | |
3d118fb5 | 334 | }) |
6fcd19ba C |
335 | .then(({ videoInstance, tagInstances }) => { |
336 | const options = { transaction: t } | |
3d118fb5 | 337 | |
6fcd19ba | 338 | return videoInstance.setTags(tagInstances, options) |
3d118fb5 | 339 | }) |
6fcd19ba C |
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 | |
3d118fb5 C |
346 | }) |
347 | } | |
348 | ||
6fcd19ba | 349 | function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance) { |
3d118fb5 | 350 | // We need the instance because we have to remove some other stuffs (thumbnail etc) |
6fcd19ba C |
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 }) | |
d8cc063e | 358 | }) |
55fa55a9 C |
359 | } |
360 | ||
6fcd19ba C |
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) | |
55fa55a9 | 365 | |
6fcd19ba C |
366 | const videoAbuseData = { |
367 | reporterUsername: reportData.reporterUsername, | |
368 | reason: reportData.reportReason, | |
369 | reporterPodId: fromPod.id, | |
370 | videoId: video.id | |
d8cc063e C |
371 | } |
372 | ||
6fcd19ba | 373 | return db.VideoAbuse.create(videoAbuseData) |
d8cc063e | 374 | }) |
6fcd19ba | 375 | .catch(err => logger.error('Cannot create remote abuse video.', { error: err })) |
55fa55a9 C |
376 | } |
377 | ||
6fcd19ba C |
378 | function fetchOwnedVideo (id: string) { |
379 | return db.Video.load(id) | |
380 | .then(video => { | |
381 | if (!video) throw new Error('Video not found') | |
e4c87ec2 | 382 | |
6fcd19ba C |
383 | return video |
384 | }) | |
385 | .catch(err => { | |
e4c87ec2 | 386 | logger.error('Cannot load owned video from id.', { error: err, id }) |
6fcd19ba C |
387 | throw err |
388 | }) | |
e4c87ec2 C |
389 | } |
390 | ||
6fcd19ba C |
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') | |
55fa55a9 | 395 | |
6fcd19ba C |
396 | return video |
397 | }) | |
398 | .catch(err => { | |
ed04d94f | 399 | logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId }) |
6fcd19ba C |
400 | throw err |
401 | }) | |
528a9efa | 402 | } |