]>
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' |
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 C |
143 | switch (eventData.eventType) { |
144 | case REQUEST_VIDEO_EVENT_TYPES.VIEWS: | |
145 | columnToUpdate = 'views' | |
146 | qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS | |
147 | break | |
e4c87ec2 | 148 | |
6fcd19ba C |
149 | case REQUEST_VIDEO_EVENT_TYPES.LIKES: |
150 | columnToUpdate = 'likes' | |
151 | qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES | |
152 | break | |
e4c87ec2 | 153 | |
6fcd19ba C |
154 | case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: |
155 | columnToUpdate = 'dislikes' | |
156 | qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES | |
157 | break | |
e4c87ec2 | 158 | |
6fcd19ba C |
159 | default: |
160 | throw new Error('Unknown video event type.') | |
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 | extname: videoToCreateData.extname, |
262 | infoHash: videoToCreateData.infoHash, | |
263 | category: videoToCreateData.category, | |
264 | licence: videoToCreateData.licence, | |
265 | language: videoToCreateData.language, | |
266 | nsfw: videoToCreateData.nsfw, | |
267 | description: videoToCreateData.description, | |
268 | authorId: author.id, | |
269 | duration: videoToCreateData.duration, | |
270 | createdAt: videoToCreateData.createdAt, | |
271 | // FIXME: updatedAt does not seems to be considered by Sequelize | |
272 | updatedAt: videoToCreateData.updatedAt, | |
273 | views: videoToCreateData.views, | |
274 | likes: videoToCreateData.likes, | |
0a6658fd C |
275 | dislikes: videoToCreateData.dislikes, |
276 | remote: true | |
feb4bdfd C |
277 | } |
278 | ||
6fcd19ba C |
279 | const video = db.Video.build(videoData) |
280 | return { tagInstances, video } | |
feb4bdfd | 281 | }) |
6fcd19ba C |
282 | .then(({ tagInstances, video }) => { |
283 | return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video })) | |
7920c273 | 284 | }) |
6fcd19ba C |
285 | .then(({ tagInstances, video }) => { |
286 | const options = { | |
287 | transaction: t | |
288 | } | |
7920c273 | 289 | |
6fcd19ba | 290 | return video.save(options).then(videoCreated => ({ tagInstances, videoCreated })) |
7920c273 | 291 | }) |
6fcd19ba C |
292 | .then(({ tagInstances, videoCreated }) => { |
293 | const options = { | |
294 | transaction: t | |
295 | } | |
7920c273 | 296 | |
6fcd19ba C |
297 | return videoCreated.setTags(tagInstances, options) |
298 | }) | |
299 | }) | |
300 | .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name)) | |
301 | .catch(err => { | |
ad0997ad | 302 | logger.debug('Cannot insert the remote video.', err) |
6fcd19ba | 303 | throw err |
7920c273 | 304 | }) |
528a9efa C |
305 | } |
306 | ||
ed04d94f | 307 | // Handle retries on fail |
4771e000 | 308 | function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { |
d6a5b018 | 309 | const options = { |
fbc22d79 | 310 | arguments: [ videoAttributesToUpdate, fromPod ], |
d6a5b018 C |
311 | errorMessage: 'Cannot update the remote video with many retries' |
312 | } | |
ed04d94f | 313 | |
6fcd19ba | 314 | return retryTransactionWrapper(updateRemoteVideo, options) |
ed04d94f C |
315 | } |
316 | ||
4771e000 | 317 | function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { |
0a6658fd | 318 | logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) |
feb4bdfd | 319 | |
6fcd19ba | 320 | return db.sequelize.transaction(t => { |
0a6658fd | 321 | return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid) |
6fcd19ba C |
322 | .then(videoInstance => { |
323 | const tags = videoAttributesToUpdate.tags | |
3d118fb5 | 324 | |
6fcd19ba | 325 | return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances })) |
3d118fb5 | 326 | }) |
6fcd19ba C |
327 | .then(({ videoInstance, tagInstances }) => { |
328 | const options = { transaction: t } | |
329 | ||
330 | videoInstance.set('name', videoAttributesToUpdate.name) | |
331 | videoInstance.set('category', videoAttributesToUpdate.category) | |
332 | videoInstance.set('licence', videoAttributesToUpdate.licence) | |
333 | videoInstance.set('language', videoAttributesToUpdate.language) | |
334 | videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) | |
335 | videoInstance.set('description', videoAttributesToUpdate.description) | |
336 | videoInstance.set('infoHash', videoAttributesToUpdate.infoHash) | |
337 | videoInstance.set('duration', videoAttributesToUpdate.duration) | |
338 | videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) | |
339 | videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) | |
340 | videoInstance.set('extname', videoAttributesToUpdate.extname) | |
341 | videoInstance.set('views', videoAttributesToUpdate.views) | |
342 | videoInstance.set('likes', videoAttributesToUpdate.likes) | |
343 | videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) | |
344 | ||
345 | return videoInstance.save(options).then(() => ({ videoInstance, tagInstances })) | |
3d118fb5 | 346 | }) |
6fcd19ba C |
347 | .then(({ videoInstance, tagInstances }) => { |
348 | const options = { transaction: t } | |
3d118fb5 | 349 | |
6fcd19ba | 350 | return videoInstance.setTags(tagInstances, options) |
3d118fb5 | 351 | }) |
6fcd19ba C |
352 | }) |
353 | .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name)) | |
354 | .catch(err => { | |
355 | // This is just a debug because we will retry the insert | |
ad0997ad | 356 | logger.debug('Cannot update the remote video.', err) |
6fcd19ba | 357 | throw err |
3d118fb5 C |
358 | }) |
359 | } | |
360 | ||
4771e000 | 361 | function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { |
3d118fb5 | 362 | // We need the instance because we have to remove some other stuffs (thumbnail etc) |
0a6658fd | 363 | return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid) |
6fcd19ba | 364 | .then(video => { |
0a6658fd | 365 | logger.debug('Removing remote video %s.', video.uuid) |
6fcd19ba C |
366 | return video.destroy() |
367 | }) | |
368 | .catch(err => { | |
0a6658fd | 369 | logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack }) |
d8cc063e | 370 | }) |
55fa55a9 C |
371 | } |
372 | ||
4771e000 | 373 | function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { |
0a6658fd | 374 | return fetchVideoByUUID(reportData.videoUUID) |
6fcd19ba C |
375 | .then(video => { |
376 | logger.debug('Reporting remote abuse for video %s.', video.id) | |
55fa55a9 | 377 | |
6fcd19ba C |
378 | const videoAbuseData = { |
379 | reporterUsername: reportData.reporterUsername, | |
380 | reason: reportData.reportReason, | |
381 | reporterPodId: fromPod.id, | |
382 | videoId: video.id | |
d8cc063e C |
383 | } |
384 | ||
6fcd19ba | 385 | return db.VideoAbuse.create(videoAbuseData) |
d8cc063e | 386 | }) |
ad0997ad | 387 | .catch(err => logger.error('Cannot create remote abuse video.', err)) |
55fa55a9 C |
388 | } |
389 | ||
0a6658fd C |
390 | function fetchVideoByUUID (id: string) { |
391 | return db.Video.loadByUUID(id) | |
6fcd19ba C |
392 | .then(video => { |
393 | if (!video) throw new Error('Video not found') | |
e4c87ec2 | 394 | |
6fcd19ba C |
395 | return video |
396 | }) | |
397 | .catch(err => { | |
ad0997ad | 398 | logger.error('Cannot load owned video from id.', { error: err.stack, id }) |
6fcd19ba C |
399 | throw err |
400 | }) | |
e4c87ec2 C |
401 | } |
402 | ||
0a6658fd C |
403 | function fetchVideoByHostAndUUID (podHost: string, uuid: string) { |
404 | return db.Video.loadByHostAndUUID(podHost, uuid) | |
6fcd19ba C |
405 | .then(video => { |
406 | if (!video) throw new Error('Video not found') | |
55fa55a9 | 407 | |
6fcd19ba C |
408 | return video |
409 | }) | |
410 | .catch(err => { | |
0a6658fd | 411 | logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid }) |
6fcd19ba C |
412 | throw err |
413 | }) | |
528a9efa | 414 | } |