1 import * as request from 'request'
2 import * as Sequelize from 'sequelize'
3 import * as Promise from 'bluebird'
4 import { join } from 'path'
6 import { database as db } from '../initializers/database'
12 REQUEST_ENDPOINT_ACTIONS,
15 } from '../initializers'
24 RequestSchedulerOptions,
26 RequestVideoQaduScheduler,
27 RequestVideoQaduSchedulerOptions,
29 RequestVideoEventScheduler,
30 RequestVideoEventSchedulerOptions
38 RequestVideoEventType,
40 RemoteVideoCreateData,
41 RemoteVideoUpdateData,
42 RemoteVideoRemoveData,
43 RemoteVideoReportAbuseData,
45 RemoteVideoRequestType,
47 RemoteVideoChannelCreateData,
48 RemoteVideoChannelUpdateData,
49 RemoteVideoChannelRemoveData,
50 RemoteVideoAuthorCreateData,
51 RemoteVideoAuthorRemoveData
54 type QaduParam = { videoId: number, type: RequestVideoQaduType }
55 type EventParam = { videoId: number, type: RequestVideoEventType }
57 const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
59 const requestScheduler = new RequestScheduler()
60 const requestVideoQaduScheduler = new RequestVideoQaduScheduler()
61 const requestVideoEventScheduler = new RequestVideoEventScheduler()
63 function activateSchedulers () {
64 requestScheduler.activate()
65 requestVideoQaduScheduler.activate()
66 requestVideoEventScheduler.activate()
69 function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Sequelize.Transaction) {
71 type: ENDPOINT_ACTIONS.ADD_VIDEO,
72 endpoint: REQUEST_ENDPOINTS.VIDEOS,
76 return createRequest(options)
79 function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Sequelize.Transaction) {
81 type: ENDPOINT_ACTIONS.UPDATE_VIDEO,
82 endpoint: REQUEST_ENDPOINTS.VIDEOS,
86 return createRequest(options)
89 function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction: Sequelize.Transaction) {
91 type: ENDPOINT_ACTIONS.REMOVE_VIDEO,
92 endpoint: REQUEST_ENDPOINTS.VIDEOS,
96 return createRequest(options)
99 function addVideoAuthorToFriends (authorData: RemoteVideoAuthorCreateData, transaction: Sequelize.Transaction) {
101 type: ENDPOINT_ACTIONS.ADD_AUTHOR,
102 endpoint: REQUEST_ENDPOINTS.VIDEOS,
106 return createRequest(options)
109 function removeVideoAuthorToFriends (authorData: RemoteVideoAuthorRemoveData, transaction: Sequelize.Transaction) {
111 type: ENDPOINT_ACTIONS.REMOVE_AUTHOR,
112 endpoint: REQUEST_ENDPOINTS.VIDEOS,
116 return createRequest(options)
119 function addVideoChannelToFriends (videoChannelData: RemoteVideoChannelCreateData, transaction: Sequelize.Transaction) {
121 type: ENDPOINT_ACTIONS.ADD_CHANNEL,
122 endpoint: REQUEST_ENDPOINTS.VIDEOS,
123 data: videoChannelData,
126 return createRequest(options)
129 function updateVideoChannelToFriends (videoChannelData: RemoteVideoChannelUpdateData, transaction: Sequelize.Transaction) {
131 type: ENDPOINT_ACTIONS.UPDATE_CHANNEL,
132 endpoint: REQUEST_ENDPOINTS.VIDEOS,
133 data: videoChannelData,
136 return createRequest(options)
139 function removeVideoChannelToFriends (videoChannelParams: RemoteVideoChannelRemoveData, transaction: Sequelize.Transaction) {
141 type: ENDPOINT_ACTIONS.REMOVE_CHANNEL,
142 endpoint: REQUEST_ENDPOINTS.VIDEOS,
143 data: videoChannelParams,
146 return createRequest(options)
149 function reportAbuseVideoToFriend (reportData: RemoteVideoReportAbuseData, video: VideoInstance, transaction: Sequelize.Transaction) {
151 type: ENDPOINT_ACTIONS.REPORT_ABUSE,
152 endpoint: REQUEST_ENDPOINTS.VIDEOS,
154 toIds: [ video.VideoChannel.Author.podId ],
157 return createRequest(options)
160 function quickAndDirtyUpdateVideoToFriends (qaduParam: QaduParam, transaction?: Sequelize.Transaction) {
162 videoId: qaduParam.videoId,
163 type: qaduParam.type,
166 return createVideoQaduRequest(options)
169 function quickAndDirtyUpdatesVideoToFriends (qadusParams: QaduParam[], transaction: Sequelize.Transaction) {
172 qadusParams.forEach(qaduParams => {
173 tasks.push(quickAndDirtyUpdateVideoToFriends(qaduParams, transaction))
176 return Promise.all(tasks)
179 function addEventToRemoteVideo (eventParam: EventParam, transaction?: Sequelize.Transaction) {
181 videoId: eventParam.videoId,
182 type: eventParam.type,
185 return createVideoEventRequest(options)
188 function addEventsToRemoteVideo (eventsParams: EventParam[], transaction: Sequelize.Transaction) {
191 eventsParams.forEach(eventParams => {
192 tasks.push(addEventToRemoteVideo(eventParams, transaction))
195 return Promise.all(tasks)
198 function hasFriends () {
199 return db.Pod.countAll().then(count => count !== 0)
202 function makeFriends (hosts: string[]) {
205 logger.info('Make friends!')
206 return getMyPublicCert()
208 return Promise.each(hosts, host => computeForeignPodsList(host, podsScore)).then(() => cert)
211 logger.debug('Pods scores computed.', { podsScore: podsScore })
212 const podsList = computeWinningPods(hosts, podsScore)
213 logger.debug('Pods that we keep.', { podsToKeep: podsList })
215 return makeRequestsToWinningPods(cert, podsList)
219 function quitFriends () {
220 // Stop pool requests
221 requestScheduler.deactivate()
223 return requestScheduler.flush()
225 return requestVideoQaduScheduler.flush()
231 const requestParams = {
232 method: 'POST' as 'POST',
233 path: '/api/' + API_VERSION + '/remote/pods/remove',
237 // Announce we quit them
238 // We don't care if the request fails
239 // The other pod will exclude us automatically after a while
240 return Promise.map(pods, pod => {
241 requestParams.toPod = pod
243 return makeSecureRequest(requestParams)
244 }, { concurrency: REQUESTS_IN_PARALLEL })
247 logger.error('Some errors while quitting friends.', err)
248 // Don't stop the process
254 pods.forEach(pod => tasks.push(pod.destroy()))
256 return Promise.all(pods)
259 logger.info('Removed all remote videos.')
260 // Don't forget to re activate the scheduler, even if there was an error
261 return requestScheduler.activate()
263 .finally(() => requestScheduler.activate())
266 function sendOwnedDataToPod (podId: number) {
267 // First send authors
268 return sendOwnedAuthorsToPod(podId)
269 .then(() => sendOwnedChannelsToPod(podId))
270 .then(() => sendOwnedVideosToPod(podId))
273 function sendOwnedChannelsToPod (podId: number) {
274 return db.VideoChannel.listOwned()
275 .then(videoChannels => {
277 videoChannels.forEach(videoChannel => {
278 const remoteVideoChannel = videoChannel.toAddRemoteJSON()
280 type: 'add-channel' as 'add-channel',
281 endpoint: REQUEST_ENDPOINTS.VIDEOS,
282 data: remoteVideoChannel,
287 const p = createRequest(options)
291 return Promise.all(tasks)
295 function sendOwnedAuthorsToPod (podId: number) {
296 return db.Author.listOwned()
299 authors.forEach(author => {
300 const remoteAuthor = author.toAddRemoteJSON()
302 type: 'add-author' as 'add-author',
303 endpoint: REQUEST_ENDPOINTS.VIDEOS,
309 const p = createRequest(options)
313 return Promise.all(tasks)
317 function sendOwnedVideosToPod (podId: number) {
318 return db.Video.listOwnedAndPopulateAuthorAndTags()
319 .then(videosList => {
321 videosList.forEach(video => {
322 const promise = video.toAddRemoteJSON()
323 .then(remoteVideo => {
325 type: 'add-video' as 'add-video',
326 endpoint: REQUEST_ENDPOINTS.VIDEOS,
331 return createRequest(options)
334 logger.error('Cannot convert video to remote.', err)
335 // Don't break the process
342 return Promise.all(tasks)
346 function fetchRemotePreview (video: VideoInstance) {
347 const host = video.VideoChannel.Author.Pod.host
348 const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
350 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
353 function removeFriend (pod: PodInstance) {
354 const requestParams = {
355 method: 'POST' as 'POST',
356 path: '/api/' + API_VERSION + '/remote/pods/remove',
360 return makeSecureRequest(requestParams)
361 .catch(err => logger.warn('Cannot notify friends %s we are quitting him.', pod.host, err))
362 .then(() => pod.destroy())
363 .then(() => logger.info('Removed friend %s.', pod.host))
364 .catch(err => logger.error('Cannot destroy friend %s.', pod.host, err))
367 function getRequestScheduler () {
368 return requestScheduler
371 function getRequestVideoQaduScheduler () {
372 return requestVideoQaduScheduler
375 function getRequestVideoEventScheduler () {
376 return requestVideoEventScheduler
379 // ---------------------------------------------------------------------------
384 removeVideoAuthorToFriends,
385 updateVideoToFriends,
386 addVideoAuthorToFriends,
387 reportAbuseVideoToFriend,
388 quickAndDirtyUpdateVideoToFriends,
389 quickAndDirtyUpdatesVideoToFriends,
390 addEventToRemoteVideo,
391 addEventsToRemoteVideo,
396 removeVideoToFriends,
399 getRequestVideoQaduScheduler,
400 getRequestVideoEventScheduler,
402 addVideoChannelToFriends,
403 updateVideoChannelToFriends,
404 removeVideoChannelToFriends
407 // ---------------------------------------------------------------------------
409 function computeForeignPodsList (host: string, podsScore: { [ host: string ]: number }) {
411 return getForeignPodsList(host).then(res => {
412 const foreignPodsList: { host: string }[] = res.data
414 // Let's give 1 point to the pod we ask the friends list
415 foreignPodsList.push({ host })
417 foreignPodsList.forEach(foreignPod => {
418 const foreignPodHost = foreignPod.host
420 if (podsScore[foreignPodHost]) podsScore[foreignPodHost]++
421 else podsScore[foreignPodHost] = 1
428 function computeWinningPods (hosts: string[], podsScore: { [ host: string ]: number }) {
429 // Build the list of pods to add
430 // Only add a pod if it exists in more than a half base pods
432 const baseScore = hosts.length / 2
434 Object.keys(podsScore).forEach(podHost => {
435 // If the pod is not me and with a good score we add it
436 if (isMe(podHost) === false && podsScore[podHost] > baseScore) {
437 podsList.push({ host: podHost })
444 function getForeignPodsList (host: string) {
445 return new Promise< ResultList<FormattedPod> >((res, rej) => {
446 const path = '/api/' + API_VERSION + '/remote/pods/list'
448 request.post(REMOTE_SCHEME.HTTP + '://' + host + path, (err, response, body) => {
449 if (err) return rej(err)
452 const json = JSON.parse(body)
461 function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
462 // Stop pool requests
463 requestScheduler.deactivate()
464 // Flush pool requests
465 requestScheduler.forceSend()
467 return Promise.map(podsList, pod => {
469 url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/remote/pods/add',
470 method: 'POST' as 'POST',
472 host: CONFIG.WEBSERVER.HOST,
473 email: CONFIG.ADMIN.EMAIL,
478 return makeRetryRequest(params)
479 .then(({ response, body }) => {
480 body = body as { cert: string, email: string }
482 if (response.statusCode === 200) {
483 const podObj = db.Pod.build({ host: pod.host, publicKey: body.cert, email: body.email })
485 .then(podCreated => {
487 // Add our videos to the request scheduler
488 sendOwnedDataToPod(podCreated.id)
491 logger.error('Cannot add friend %s pod.', pod.host, err)
494 logger.error('Status not 200 for %s pod.', pod.host)
498 logger.error('Error with adding %s pod.', pod.host, { error: err.stack })
499 // Don't break the process
501 }, { concurrency: REQUESTS_IN_PARALLEL })
502 .then(() => logger.debug('makeRequestsToWinningPods finished.'))
504 // Final callback, we've ended all the requests
505 // Now we made new friends, we can re activate the pool of requests
506 requestScheduler.activate()
510 // Wrapper that populate "toIds" argument with all our friends if it is not specified
511 type CreateRequestOptions = {
512 type: RemoteVideoRequestType
513 endpoint: RequestEndpoint
516 transaction: Sequelize.Transaction
518 function createRequest (options: CreateRequestOptions) {
519 if (options.toIds !== undefined) return requestScheduler.createRequest(options as RequestSchedulerOptions)
521 // If the "toIds" pods is not specified, we send the request to all our friends
522 return db.Pod.listAllIds(options.transaction).then(podIds => {
523 const newOptions = Object.assign(options, { toIds: podIds })
524 return requestScheduler.createRequest(newOptions)
528 function createVideoQaduRequest (options: RequestVideoQaduSchedulerOptions) {
529 return requestVideoQaduScheduler.createRequest(options)
532 function createVideoEventRequest (options: RequestVideoEventSchedulerOptions) {
533 return requestVideoEventScheduler.createRequest(options)
536 function isMe (host: string) {
537 return host === CONFIG.WEBSERVER.HOST