f-hj/slsk-client

View on GitHub
lib/slsk-client.js

Summary

Maintainability
A
3 hrs
Test Coverage
const EventEmitter = require('events')
const crypto = require('crypto')
const stream = require('stream')
const net = require('net')

const debug = require('debug')('slsk:i')

const Server = require('./server.js')
const DefaultPeer = require('./peer/default-peer.js')
const DistributedPeer = require('./peer/distributed-peer.js')
const downloadPeerFile = require('./peer/download-peer-file.js')
const Listen = require('./listen.js')
const Shared = require('./share/shared.js')
let stack = require('./stack')

let server
let listen
let peers = {}
let shared

class SlskClient extends EventEmitter {
  constructor (serverAddress, sharedFolders) {
    super()
    this.serverAddress = serverAddress
    this.sharedFolders = sharedFolders
  }

  init (cb) {
    debug('Init client')
    server = new Server(this.serverAddress, cb)

    server.on('connect-to-peer', peer => {
      this.connectToPeer(peer)
    })

    server.on('get-peer-address', peer => {
      if (peers[peer.user]) {
        peers[peer.user].setAddress(peer.host, peer.port)
      } else {
        peers[peer.user] = new DefaultPeer(net.createConnection({
          host: peer.host,
          port: peer.port
        }), peer)
        if (stack.peerSearchMatches[peer.user]) {
          Object.keys(stack.peerSearchMatches[peer.user]).forEach(ticket => {
            peers[peer.user].fileSearchResult(stack.peerSearchMatches[peer.user][ticket], ticket, stack.currentLogin)
          })
          stack.peerSearchMatches[peer.user] = {}
        }
      }
    })

    shared = new Shared()
    this.sharedFolders.forEach(folder => shared.scanFolder(folder))
  }

  connectToPeer (peer) {
    debug(`connectToPeer ${peer.user} ${peer.host} ${peer.port} ${peer.token} ${peer.type}`)

    switch (peer.type) {
      case 'F': {
        downloadPeerFile(peer.host, peer.port, peer.token, peer.user, false)
        break
      }
      case 'D': {
        peers[peer.user] = new DistributedPeer(net.createConnection({
          host: peer.host,
          port: peer.port
        }), peer)
        peers[peer.user].on('search', search => {
          let matched = shared.search(search.query)
          if (matched.length > 0) {
            if (!peers[search.user]) {
              server.getPeerAddress(search.user)
              if (!stack.peerSearchMatches[search.user]) {
                stack.peerSearchMatches[search.user] = {
                  [search.ticket]: matched
                }
              } else {
                stack.peerSearchMatches[search.user][search.ticket] = matched
              }
            } else {
              peers[search.user].fileSearchResult(matched, search.ticket, stack.currentLogin)
            }
            debug(`Search from peer ${search.user}, query: ${search.query}. Matched: ${matched.length} files`)
          }
        })
        peers[peer.user].on('disconnect', () => {
          delete peers[peer.user]
        })
        break
      }
      default: {
        peers[peer.user] = new DefaultPeer(net.createConnection({
          host: peer.host,
          port: peer.port
        }), peer)
        peers[peer.user].on('disconnect', () => {
          delete peers[peer.user]
        })
      }
    }
  }

  login (credentials, cb) {
    stack.currentLogin = credentials.user
    server.login(credentials)
    let incomingPort = credentials.incomingPort || 2234
    listen = new Listen(incomingPort)
    listen.on('new-peer', evt => {
      let peer = evt.peer
      if (peers[peer.user]) {
        debug(`Already connected to ${peer.user}`)
      } else {
        server.getPeerAddress(peer.user)
        debug(`new Peer connected ${peer.user} token ${peer.token}`)
        peers[peer.user] = new DefaultPeer(evt.socket, peer)
        peers[peer.user].on('disconnect', () => {
          delete peers[peer.user]
        })
      }
    })
    server.setWaitPort(incomingPort)
    stack.login = cb
  }

  search (obj, cb) {
    if (typeof cb !== 'function') {
      throw new Error('2nd argument must be callback function')
    }

    let token = crypto.randomBytes(4).toString('hex')
    let timeout = obj.timeout || 4000
    let results = []
    setTimeout(() => {
      delete stack.search[token]
      cb(null, results)
    }, timeout)
    stack.search[token] = {
      cb: res => {
        this.emit('found', res)
        this.emit(`found:${obj.req}`, res)
        results.push(res)
      },
      query: obj.req
    }

    server.fileSearch(obj.req, token)
  }

  download (obj, cb, stream) {
    debug(`launch download ${obj.file.user} ${obj.file.file}`)
    if (typeof cb !== 'function' && !stream) {
      throw new Error('2nd argument must be callback function')
    }
    if (typeof obj.file === 'undefined') {
      throw new Error('You must specify file')
    }
    if (!peers[obj.file.user]) {
      return cb(new Error('User not exist'))
    }

    let token = crypto.randomBytes(4).toString('hex')

    stack.downloadTokens[token] = {
      user: obj.file.user,
      file: obj.file.file,
      size: obj.file.size
    }

    stack.download[obj.file.user + '_' + obj.file.file] = {
      cb,
      path: obj.path,
      stream
    }
    peers[obj.file.user].transferRequest(obj.file.file, token)
  }

  downloadStream (obj, cb) {
    let s = new stream.Readable()
    s._read = () => {}
    cb(null, s)
    this.download(obj, null, s)
  }

  destroy () {
    if (server) server.destroy()
    if (listen) listen.destroy()

    Object.keys(peers).forEach(peer => {
      peers[peer].destroy()
    })
    delete stack.login
  }
}

module.exports = SlskClient