aboutsummaryrefslogblamecommitdiffhomepage
path: root/server/lib/friends.ts
blob: 5c9baef470ab91319c46fddc146786d76ce8ff3e (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
                                  
                                      
                                    
                           
 
                                                         





                           

                




                        
                  


                   

                          
                            



                                   
                  






                        



                        

                             






                               
                     
 

                                                                  

                                                                           
 
                                               

                                                                   
 
                                
                             

                                       

 
                                                                                                   
                   
                                     
                                       


                    
                               

 
                                                                                                      
                   
                                        
                                       


                    
                               

 
                                                                                                         
                   
                                        
                                       
                      
               
   
                               

 









                                                                                                                
                                                                                                                    




























                                                                                                                           
                                                                                                                              








                                          
                                                                                                                                      
                   
                                        
                                       
                     
                                               
               
   
                               

 
                                                                                                        
                   

                               

               
                                        

 
                                                                                                            

                  
                                     
                                                                          

    
                           

 
                                                                                              
                   

                                

               
                                         

 
                                                                                                  

                  
                                           
                                                               
   
 
                           

 



                                       

 
                                              
                      

                              
                                      
 









                                                                 

 
                               
                       
                               
 










                                                          
 




                                                                
                                 
 

                                               


                                                              
 














                                                                            
 
 
                                                   
                       























                                                             

 


                                                      
 








                                                 
 




                                    

 






                                                                       
                         
                                           
                                             
                            


                           
                                     
        



                                                            
        
 



                          
 
 

                                                    




                                                                  

















                                                          
                                                





                                                        












                                                                               

 











                                           
                                                                              
 


                     
                             
                       
                          







                                     
               
                       
                     

                               
                                

                           
                         

                              
 
 
                                                                              
 


                                                                                               
 

                                                          
 

                                             
 


                                                              
 
                  

 
                                                                                        

                                                              
                     
                                    
 
                                                 
                                                           

                                                                    
     
   
 
                 

 
                                            
                                                                
                                                            
 
                                                                                     
                              
 
           
                                                               




                        

    
 
                                                                                  
                       
                               
                        
                              
 









                                                                                                
       
 










                                                                                                          
         












                                                                                             

                                                                       
                               
   
 
 
                                                                                     
                             
                              
                           



                                    




                                                                            
 
                                                                                 





                                                              
 
 

                                                                             

 

                                                                               

 
                              
                                       
 
import * as request from 'request'
import * as Sequelize from 'sequelize'
import * as Bluebird from 'bluebird'
import { join } from 'path'

import { database as db } from '../initializers/database'
import {
  API_VERSION,
  CONFIG,
  REQUESTS_IN_PARALLEL,
  REQUEST_ENDPOINTS,
  REQUEST_ENDPOINT_ACTIONS,
  REMOTE_SCHEME,
  STATIC_PATHS
} from '../initializers'
import {
  logger,
  getMyPublicCert,
  makeSecureRequest,
  makeRetryRequest
} from '../helpers'
import {
  RequestScheduler,
  RequestSchedulerOptions,

  RequestVideoQaduScheduler,
  RequestVideoQaduSchedulerOptions,

  RequestVideoEventScheduler,
  RequestVideoEventSchedulerOptions
} from './request'
import {
  PodInstance,
  VideoInstance
} from '../models'
import {
  RequestEndpoint,
  RequestVideoEventType,
  RequestVideoQaduType,
  RemoteVideoCreateData,
  RemoteVideoUpdateData,
  RemoteVideoRemoveData,
  RemoteVideoReportAbuseData,
  ResultList,
  RemoteVideoRequestType,
  Pod as FormattedPod,
  RemoteVideoChannelCreateData,
  RemoteVideoChannelUpdateData,
  RemoteVideoChannelRemoveData,
  RemoteVideoAuthorCreateData,
  RemoteVideoAuthorRemoveData
} from '../../shared'

type QaduParam = { videoId: number, type: RequestVideoQaduType }
type EventParam = { videoId: number, type: RequestVideoEventType }

const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]

const requestScheduler = new RequestScheduler()
const requestVideoQaduScheduler = new RequestVideoQaduScheduler()
const requestVideoEventScheduler = new RequestVideoEventScheduler()

function activateSchedulers () {
  requestScheduler.activate()
  requestVideoQaduScheduler.activate()
  requestVideoEventScheduler.activate()
}

function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.ADD_VIDEO,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: videoData,
    transaction
  }
  return createRequest(options)
}

function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.UPDATE_VIDEO,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: videoData,
    transaction
  }
  return createRequest(options)
}

function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction?: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.REMOVE_VIDEO,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: videoParams,
    transaction
  }
  return createRequest(options)
}

function addVideoAuthorToFriends (authorData: RemoteVideoAuthorCreateData, transaction: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.ADD_AUTHOR,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: authorData,
    transaction
  }
  return createRequest(options)
}

function removeVideoAuthorToFriends (authorData: RemoteVideoAuthorRemoveData, transaction?: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.REMOVE_AUTHOR,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: authorData,
    transaction
  }
  return createRequest(options)
}

function addVideoChannelToFriends (videoChannelData: RemoteVideoChannelCreateData, transaction: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.ADD_CHANNEL,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: videoChannelData,
    transaction
  }
  return createRequest(options)
}

function updateVideoChannelToFriends (videoChannelData: RemoteVideoChannelUpdateData, transaction: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.UPDATE_CHANNEL,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: videoChannelData,
    transaction
  }
  return createRequest(options)
}

function removeVideoChannelToFriends (videoChannelParams: RemoteVideoChannelRemoveData, transaction?: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.REMOVE_CHANNEL,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: videoChannelParams,
    transaction
  }
  return createRequest(options)
}

function reportAbuseVideoToFriend (reportData: RemoteVideoReportAbuseData, video: VideoInstance, transaction: Sequelize.Transaction) {
  const options = {
    type: ENDPOINT_ACTIONS.REPORT_ABUSE,
    endpoint: REQUEST_ENDPOINTS.VIDEOS,
    data: reportData,
    toIds: [ video.VideoChannel.Author.podId ],
    transaction
  }
  return createRequest(options)
}

function quickAndDirtyUpdateVideoToFriends (qaduParam: QaduParam, transaction?: Sequelize.Transaction) {
  const options = {
    videoId: qaduParam.videoId,
    type: qaduParam.type,
    transaction
  }
  return createVideoQaduRequest(options)
}

function quickAndDirtyUpdatesVideoToFriends (qadusParams: QaduParam[], transaction: Sequelize.Transaction) {
  const tasks = []

  qadusParams.forEach(qaduParams => {
    tasks.push(quickAndDirtyUpdateVideoToFriends(qaduParams, transaction))
  })

  return Promise.all(tasks)
}

function addEventToRemoteVideo (eventParam: EventParam, transaction?: Sequelize.Transaction) {
  const options = {
    videoId: eventParam.videoId,
    type: eventParam.type,
    transaction
  }
  return createVideoEventRequest(options)
}

function addEventsToRemoteVideo (eventsParams: EventParam[], transaction: Sequelize.Transaction) {
  const tasks = []

  for (const eventParams of eventsParams) {
    tasks.push(addEventToRemoteVideo(eventParams, transaction))
  }

  return Promise.all(tasks)
}

async function hasFriends () {
  const count = await db.Pod.countAll()

  return count !== 0
}

async function makeFriends (hosts: string[]) {
  const podsScore = {}

  logger.info('Make friends!')
  const cert = await getMyPublicCert()

  for (const host of hosts) {
    await computeForeignPodsList(host, podsScore)
  }

  logger.debug('Pods scores computed.', { podsScore: podsScore })

  const podsList = computeWinningPods(hosts, podsScore)
  logger.debug('Pods that we keep.', { podsToKeep: podsList })

  return makeRequestsToWinningPods(cert, podsList)
}

async function quitFriends () {
  // Stop pool requests
  requestScheduler.deactivate()

  try {
    await requestScheduler.flush()

    await requestVideoQaduScheduler.flush()

    const pods = await db.Pod.list()
    const requestParams = {
      method: 'POST' as 'POST',
      path: '/api/' + API_VERSION + '/remote/pods/remove',
      toPod: null
    }

    // Announce we quit them
    // We don't care if the request fails
    // The other pod will exclude us automatically after a while
    try {
      await Bluebird.map(pods, pod => {
        requestParams.toPod = pod

        return makeSecureRequest(requestParams)
      }, { concurrency: REQUESTS_IN_PARALLEL })
    } catch (err) { // Don't stop the process
      logger.error('Some errors while quitting friends.', err)
    }

    const tasks = []
    for (const pod of pods) {
      tasks.push(pod.destroy())
    }
    await Promise.all(pods)

    logger.info('Removed all remote videos.')

    requestScheduler.activate()
  } catch (err) {
    // Don't forget to re activate the scheduler, even if there was an error
    requestScheduler.activate()

    throw err
  }
}

async function sendOwnedDataToPod (podId: number) {
  // First send authors
  await sendOwnedAuthorsToPod(podId)
  await sendOwnedChannelsToPod(podId)
  await sendOwnedVideosToPod(podId)
}

async function sendOwnedChannelsToPod (podId: number) {
  const videoChannels = await db.VideoChannel.listOwned()

  const tasks: Promise<any>[] = []
  for (const videoChannel of videoChannels) {
    const remoteVideoChannel = videoChannel.toAddRemoteJSON()
    const options = {
      type: 'add-channel' as 'add-channel',
      endpoint: REQUEST_ENDPOINTS.VIDEOS,
      data: remoteVideoChannel,
      toIds: [ podId ],
      transaction: null
    }

    const p = createRequest(options)
    tasks.push(p)
  }

  await Promise.all(tasks)
}

async function sendOwnedAuthorsToPod (podId: number) {
  const authors = await db.Author.listOwned()
  const tasks: Promise<any>[] = []

  for (const author of authors) {
    const remoteAuthor = author.toAddRemoteJSON()
    const options = {
      type: 'add-author' as 'add-author',
      endpoint: REQUEST_ENDPOINTS.VIDEOS,
      data: remoteAuthor,
      toIds: [ podId ],
      transaction: null
    }

    const p = createRequest(options)
    tasks.push(p)
  }

  await Promise.all(tasks)
}

async function sendOwnedVideosToPod (podId: number) {
  const videosList = await db.Video.listOwnedAndPopulateAuthorAndTags()
  const tasks: Bluebird<any>[] = []

  for (const video of videosList) {
    const promise = video.toAddRemoteJSON()
      .then(remoteVideo => {
        const options = {
          type: 'add-video' as 'add-video',
          endpoint: REQUEST_ENDPOINTS.VIDEOS,
          data: remoteVideo,
          toIds: [ podId ],
          transaction: null
        }
        return createRequest(options)
      })
      .catch(err => {
        logger.error('Cannot convert video to remote.', err)
        // Don't break the process
        return undefined
      })

    tasks.push(promise)
  }

  await Promise.all(tasks)
}

function fetchRemotePreview (video: VideoInstance) {
  const host = video.VideoChannel.Author.Pod.host
  const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())

  return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
}

function fetchRemoteDescription (video: VideoInstance) {
  const host = video.VideoChannel.Author.Pod.host
  const path = video.getDescriptionPath()

  const requestOptions = {
    url: REMOTE_SCHEME.HTTP + '://' + host + path,
    json: true
  }

  return new Promise<string>((res, rej) => {
    request.get(requestOptions, (err, response, body) => {
      if (err) return rej(err)

      return res(body.description ? body.description : '')
    })
  })
}

async function removeFriend (pod: PodInstance) {
  const requestParams = {
    method: 'POST' as 'POST',
    path: '/api/' + API_VERSION + '/remote/pods/remove',
    toPod: pod
  }

  try {
    await makeSecureRequest(requestParams)
  } catch (err) {
    logger.warn('Cannot notify friends %s we are quitting him.', pod.host, err)
  }

  try {
    await pod.destroy()

    logger.info('Removed friend %s.', pod.host)
  } catch (err) {
    logger.error('Cannot destroy friend %s.', pod.host, err)
  }
}

function getRequestScheduler () {
  return requestScheduler
}

function getRequestVideoQaduScheduler () {
  return requestVideoQaduScheduler
}

function getRequestVideoEventScheduler () {
  return requestVideoEventScheduler
}

// ---------------------------------------------------------------------------

export {
  activateSchedulers,
  addVideoToFriends,
  removeVideoAuthorToFriends,
  updateVideoToFriends,
  addVideoAuthorToFriends,
  reportAbuseVideoToFriend,
  quickAndDirtyUpdateVideoToFriends,
  quickAndDirtyUpdatesVideoToFriends,
  addEventToRemoteVideo,
  addEventsToRemoteVideo,
  hasFriends,
  makeFriends,
  quitFriends,
  removeFriend,
  removeVideoToFriends,
  sendOwnedDataToPod,
  getRequestScheduler,
  getRequestVideoQaduScheduler,
  getRequestVideoEventScheduler,
  fetchRemotePreview,
  addVideoChannelToFriends,
  fetchRemoteDescription,
  updateVideoChannelToFriends,
  removeVideoChannelToFriends
}

// ---------------------------------------------------------------------------

async function computeForeignPodsList (host: string, podsScore: { [ host: string ]: number }) {
  const result = await getForeignPodsList(host)
  const foreignPodsList: { host: string }[] = result.data

  // Let's give 1 point to the pod we ask the friends list
  foreignPodsList.push({ host })

  for (const foreignPod of foreignPodsList) {
    const foreignPodHost = foreignPod.host

    if (podsScore[foreignPodHost]) podsScore[foreignPodHost]++
    else podsScore[foreignPodHost] = 1
  }

  return undefined
}

function computeWinningPods (hosts: string[], podsScore: { [ host: string ]: number }) {
  // Build the list of pods to add
  // Only add a pod if it exists in more than a half base pods
  const podsList = []
  const baseScore = hosts.length / 2

  for (const podHost of Object.keys(podsScore)) {
    // If the pod is not me and with a good score we add it
    if (isMe(podHost) === false && podsScore[podHost] > baseScore) {
      podsList.push({ host: podHost })
    }
  }

  return podsList
}

function getForeignPodsList (host: string) {
  return new Promise< ResultList<FormattedPod> >((res, rej) => {
    const path = '/api/' + API_VERSION + '/remote/pods/list'

    request.post(REMOTE_SCHEME.HTTP + '://' + host + path, (err, response, body) => {
      if (err) return rej(err)

      try {
        const json: ResultList<FormattedPod> = JSON.parse(body)
        return res(json)
      } catch (err) {
        return rej(err)
      }
    })
  })
}

async function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
  // Stop pool requests
  requestScheduler.deactivate()
  // Flush pool requests
  requestScheduler.forceSend()

  try {
    await Bluebird.map(podsList, async pod => {
      const params = {
        url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/remote/pods/add',
        method: 'POST' as 'POST',
        json: {
          host: CONFIG.WEBSERVER.HOST,
          email: CONFIG.ADMIN.EMAIL,
          publicKey: cert
        }
      }

      const { response, body } = await makeRetryRequest(params)
      const typedBody = body as { cert: string, email: string }

      if (response.statusCode === 200) {
        const podObj = db.Pod.build({ host: pod.host, publicKey: typedBody.cert, email: typedBody.email })

        let podCreated: PodInstance
        try {
          podCreated = await podObj.save()
        } catch (err) {
          logger.error('Cannot add friend %s pod.', pod.host, err)
        }

        // Add our videos to the request scheduler
        sendOwnedDataToPod(podCreated.id)
          .catch(err => logger.warn('Cannot send owned data to pod %d.', podCreated.id, err))
      } else {
        logger.error('Status not 200 for %s pod.', pod.host)
      }
    }, { concurrency: REQUESTS_IN_PARALLEL })

    logger.debug('makeRequestsToWinningPods finished.')

    requestScheduler.activate()
  } catch (err) {
    // Final callback, we've ended all the requests
    // Now we made new friends, we can re activate the pool of requests
    requestScheduler.activate()
  }
}

// Wrapper that populate "toIds" argument with all our friends if it is not specified
type CreateRequestOptions = {
  type: RemoteVideoRequestType
  endpoint: RequestEndpoint
  data: Object
  toIds?: number[]
  transaction: Sequelize.Transaction
}
async function createRequest (options: CreateRequestOptions) {
  if (options.toIds !== undefined) {
    await requestScheduler.createRequest(options as RequestSchedulerOptions)
    return undefined
  }

  // If the "toIds" pods is not specified, we send the request to all our friends
  const podIds = await db.Pod.listAllIds(options.transaction)

  const newOptions = Object.assign(options, { toIds: podIds })
  await requestScheduler.createRequest(newOptions)

  return undefined
}

function createVideoQaduRequest (options: RequestVideoQaduSchedulerOptions) {
  return requestVideoQaduScheduler.createRequest(options)
}

function createVideoEventRequest (options: RequestVideoEventSchedulerOptions) {
  return requestVideoEventScheduler.createRequest(options)
}

function isMe (host: string) {
  return host === CONFIG.WEBSERVER.HOST
}