Source: lib/request.js

module.exports = Request

var debug = require('debug')('snapchat:request')
var https = require('https')
var request = require('request')
var urljoin = require('url-join')
var extend = require('xtend')
var zlib = require('zlib')

var BufferUtils = require('./buffer-utils')
var StringUtils = require('./string-utils')
var constants = require('./constants')

/**
 * Snapchat wrapper for HTTP requests
 *
 * @class
 * @param {Object} opts
 */
function Request (opts) {
  var self = this
  if (!(self instanceof Request)) return new Request(opts)
  if (!opts) opts = {}

  self.HTTPMethod = opts.method
  self.HTTPHeaders = {}
  self.opts = opts

  if (opts.method === 'POST') {
    if (opts.endpoint) {
      self._initPOST(opts)
    } else if (opts.url) {
      self._initPOSTURL(opts)
    } else {
      throw new Error('invalid request')
    }
  } else if (opts.method === 'GET') {
    self._initGET(opts)
  } else {
    throw new Error('invalid request')
  }
}

Request.prototype._initHeaderFields = function (headers) {
  var self = this

  self.HTTPHeaders[constants.headers.contentType] = 'application/x-www-form-urlencoded'
  self.HTTPHeaders[constants.headers.userAgent] = constants.core.userAgent
  self.HTTPHeaders[constants.headers.acceptLanguage] = constants.headers.values.language
  self.HTTPHeaders[constants.headers.acceptLocale] = constants.headers.values.locale

  if (headers) {
    self.HTTPHeaders = extend(self.HTTPHeaders, headers)
  }
}

/**
 * Automatically adds query parameters:
 * - timestamp
 * - req_token
 * Automatically adds HTTP header fields:
 * - User-Agent
 * - Accept-Language
 * - Accept-Locale
 * - Content-Type
 */
Request.prototype._initPOST = function (opts) {
  var self = this

  Request._applyHeaderOverrides(opts)
  opts.token = opts.token || constants.core.staticToken

  self._initHeaderFields(opts.headers)
  Request._applyOverrides(opts)

  self.URL = urljoin(constants.core.baseURL, opts.endpoint)

  if (!opts.timestamp) {
    opts.timestamp = StringUtils.timestamp()
  }

  var reqToken = 'req_token'
  if (!opts.params[reqToken]) {
    opts.params[reqToken] = StringUtils.hashSCString(opts.token, opts.timestamp)
  }

  if (!opts.params.timestamp) {
    opts.params.timestamp = +opts.timestamp
  }

  // special-case for uploading snaps
  if (opts.endpoint === constants.endpoints.snaps.upload) {
    var body = [ ]

    for (var key in opts.params) {
      if (key === 'data') {
        body.push(BufferUtils.boundaryForBuffer(key, opts.params[key]))
      } else {
        body.push(BufferUtils.boundaryForString(key, opts.params[key]))
      }
    }

    // final boundary
    body.push(new Buffer('\r\n--' + constants.core.boundary + '--\r\n'))

    // concat all buffers together to form the body
    self.HTTPBody = Buffer.concat(body)
    self.multipart = true

    // debugging...
    // var fs = require('fs')
    // fs.writeFileSync('out.txt', self.HTTPBody.toString('utf-8'))
    // console.log(self.HTTPBody.toString('utf-8'))
  } else {
    self.HTTPBody = opts.params
  }

  if (opts.endpoint === constants.endpoints.snaps.loadBlob || opts.endpoint === constants.endpoints.chat.media) {
    self.HTTPHeaders[constants.headers.timestamp] = opts.timestamp
  }
}

Request.prototype._initPOSTURL = function (opts) {
  var self = this

  self.URL = opts.url
  self.HTTPBody = opts.eventData
}

/**
 * Automatically adds HTTP header fields:
 * - User-Agent
 * - Accept-Language
 * - Accept-Locale
 * - Content-Type
 */
Request.prototype._initGET = function (opts) {
  var self = this

  Request._applyHeaderOverrides(opts)
  self._initHeaderFields(opts.headers)

  self.URL = urljoin(constants.core.baseURL, opts.endpoint)
}

/**
 * Initiates the underlying HTTP request Request.httpRequest.
 *
 * @param {function} cb
 */
Request.prototype.start = function (cb) {
  var self = this

  function wrapcb (err, response, body) {
    var contentType = response && response.headers && response.headers['content-type']
    if (contentType && contentType !== 'application/octet-stream') {
      response.body = response.body.toString('utf8')
    }
    if (err) {
      return cb(err, body)
    } else if (!err && response && (response.statusCode < 200 || response.statusCode >= 300)) {
      // catch snapchat API error codes
      debug('Snapchat Request Error: %d (%s) \nendpoint: %s \nheaders: %j \nrequest: %s',
            response.statusCode,
            response.statusMessage,
            self.opts.endpoint,
            response.request.headers,
            self.multipart ? '' : response.request.body.toString('utf-8').substr(0, 80))

      // console.log(response)
      // console.log(response.request.body.toString('base64'))
      // console.log(response.request.body.toString('utf-8'))

      return cb('Snapchat API error ' + response.statusCode + ' (' + response.statusMessage + ')', body)
    } else {
      var result = body

      // attempt to parse the body as JSON if appropriate
      if (contentType && contentType.indexOf('application/json') >= 0) {
        result = StringUtils.tryParseJSON(body)

        if (!result) {
          debug('Snapchat Request JSON Parse Error: \nresponse: "%s"', body)
          // return cb('JSON parse error', body)
        }
      }

      return cb(null, result)
    }
  }

  if (self.HTTPMethod === 'POST') {
    if (self.multipart) {
      self.HTTPHeaders['content-length'] = Buffer.byteLength(self.HTTPBody)

      // self.HTTPHeaders['X-Timestamp'] = 0

      // TODO:
      // neither objc or php versions send Content-Length header
      // don't think we can use request because its multipart/form-data uses random boundary

      var req = https.request({
        host: 'feelinsonice-hrd.appspot.com', // constants.core.baseURL,
        path: self.endpoint,
        method: 'POST',
        headers: self.HTTPHeaders,
        agent: false,
        rejectUnauthorized: false
        //, secureOptions: require('constants').SSL_OP_NO_TLSv1
      }, function (response) {
        response.request = req
        response.request.headers = req._headers

        console.log('status', response.statusCode)
        console.log('headers', JSON.stringify(response.headers))

        response.setEncoding('utf8')

        var result = ''
        response.on('data', function (chunk) {
          console.log('RESPONSE DATA', chunk)
          result += chunk
        })

        response.on('end', function () {
          console.log('RESPONSE END')
          wrapcb(null, response, result)
        })
      })

      // req.removeHeader('content-length')
      // req.removeHeader('transfer-encoding')
      // req.removeHeader('connection')

      req.on('error', function (err) {
        console.error('ERROR', err)
        wrapcb(err, null, null)
      })

      req.on('abort', function () {
        console.error('ABORT')
        wrapcb('abort', null, null)
      })

      req.write(self.HTTPBody)
      req.end()
      console.log('REQUEST END', req._headers)
    } else {
      self.httpRequest = request.post({
        url: self.URL,
        headers: self.HTTPHeaders,
        form: self.HTTPBody
      }, wrapcb)

      // debug('%s Request %s (headers %j) (body \'%s\')', self.HTTPMethod, self.URL, self.HTTPHeaders,
      //       self.httpRequest.body.toString('utf-8'))
    }
  } else {

    self.httpRequest = request({
      url: self.URL,
      headers: self.HTTPHeaders,
      encoding: null
    }, wrapcb)

    debug('%s Request %s (headers %j)', self.HTTPMethod, self.URL, self.HTTPHeaders)
  }
}

// -----------------------------------------------------------------------------
// static utility methods
// -----------------------------------------------------------------------------

/**
 * @static
 * @param {string=} endpoint Optional endpoint of the request relative to the base URL.
 * @param {Object=} params Optional parameters for the request.
 * @param {string=} gauth Optional parameter set to the X-Snapchat-Client-Auth-Token header field.
 * @param {string=} token Optional snapchat auth token returned from logging in.
 * @param {Object=} opts Optional custom options.
 * @param {function} cb
 * @return {Request} new Request wrapping this HTTP POST request.
 */
Request.post = function (endpoint, params, gauth, token, opts, cb) {
  // handle optional parameters
  if (typeof params === 'function') {
    cb = params
    params = { }
    gauth = token = opts = null
  } else if (typeof gauth === 'function') {
    cb = gauth
    gauth = token = opts = null
  } else if (typeof token === 'function') {
    cb = token
    token = opts = null
  } else if (typeof opts === 'function') {
    cb = opts
    opts = null
  }

  var headers = { }
  headers[constants.headers.clientAuthToken] = 'Bearer ' + (gauth || '')

  return Request.postCustom(endpoint, params, headers, token, opts, cb)
}

/**
 * @static
 * @param {string} endpoint endpoint of the request relative to the base URL.
 * @param {Object} params parameters for the request.
 * @param {Object} headers Custom HTTP headers for this request.
 * @param {string} token snapchat auth token returned from logging in.
 * @param {Object=} opts Optional custom options.
 * @param {function} cb
 * @return {Request} new Request wrapping this HTTP POST request.
 */
Request.postCustom = function (endpoint, params, headers, token, opts, cb) {
  // handle optional parameters
  if (typeof opts === 'function') {
    cb = opts
    opts = null
  }

  opts = opts || { }

  var request = new Request(extend({
    method: 'POST',
    endpoint: endpoint,
    token: token,
    params: params,
    headers: headers
  }, opts))

  request.start(cb)
  return request
}

/**
 * @static
 * @param {string} endpoint The endpoint of the request relative to the base URL.
 * @param {function} cb
 * @return {Request} new Request wrapping this HTTP GET request
 */
Request.get = function (endpoint, cb) {
  var request = new Request({
    method: 'GET',
    endpoint: endpoint
  })

  request.start(cb)
  return request
}

/**
 * @static
 * @param {Object} eventData
 * @param {function} cb
 * @return {Request} new Request wrapping this events HTTP POST request
 */
Request.sendEvents = function (eventData, cb) {
  var request = new Request({
    method: 'POST',
    url: constants.core.eventsURL,
    eventData: eventData
  })

  request.start(cb)
  return request
}

Request.postRaw = function (params, cb) {
  request.post(extend(params, {
    encoding: null
  }), function (err, response, body) {
    if (err) {
      return cb(err, response, body)
    }

    var contentType = response.headers['content-type']
    var encoding = response.headers['content-encoding']

    function contentTypeWrapper (err, body) {
      if (err) {
        return cb(err, response, body)
      } else if (contentType.indexOf('application/json') >= 0) {
        return cb(err, response, StringUtils.tryParseJSON(body.toString()))
      } else if (contentType.indexOf('text/plain') >= 0) {
        return cb(err, response, body.toString())
      } else {
        return cb(err, response, body)
      }
    }

    if (encoding === 'gzip') {
      zlib.gunzip(body, function (err, dezipped) {
        contentTypeWrapper(err, dezipped && dezipped.toString())
      })
    } else if (encoding === 'deflate') {
      zlib.inflate(body, function (err, decoded) {
        contentTypeWrapper(err, decoded && decoded.toString())
      })
    } else {
      contentTypeWrapper(err, body)
    }
  })
}

// -----------------------------------------------------------------------------
// override handling
// -----------------------------------------------------------------------------

Request.overrideHeaderValuesGlobally = function (headers) {
  globalHeaderOverrides = headers
}

Request.overrideHeaderValues = function (headers, endpoint) {
  scopeHeaderOverrides[endpoint] = headers
}

Request.overrideValues = function (params, endpoint) {
  scopeParamOverrides[endpoint] = params
}

Request.overrideValuesGlobally = function (params) {
  globalParamOverrides = params
}

Request.overrideEndpoints = function (endpoints) {
  endpointOverrides = endpoints
}

Request._applyOverrides = function (opts) {
  if (opts.params) {
    opts.params = extend(opts.params, globalParamOverrides)
    opts.params = extend(opts.params, scopeParamOverrides[opts.endpoint] || { })
  }

  if (opts.endpoint) {
    opts.endpoint = endpointOverrides[opts.endpoint] || opts.endpoint
  }
}

Request._applyHeaderOverrides = function (opts) {
  if (!opts.headers) return

  opts.headers = extend(opts.headers, globalHeaderOverrides)
  opts.headers = extend(opts.headers, scopeHeaderOverrides[opts.endpoint] || { })
}

var globalHeaderOverrides = { }
var globalParamOverrides = { }
var scopeParamOverrides = { }
var scopeHeaderOverrides = { }
var endpointOverrides = { }