Source: index.js

module.exports = Snapchat

// external
var debug = require('debug')('snapchat')
var phone = require('phone')
var zlib = require('zlib')
var Promise = require('bluebird')

// utilities
var constants = require('./lib/constants')
var Request = require('./lib/request')
var StringUtils = require('./lib/string-utils')

// routes
var Account = require('./routes/account')
var Chat = require('./routes/chat')
var Device = require('./routes/device')
var Friends = require('./routes/friends')
var Snaps = require('./routes/snaps')
var Stories = require('./routes/stories')

// models
var Session = require('./models/session')

/**
 * Snapchat Client
 *
 * @class
 * @param {Object} opts (currently unused)
 */
function Snapchat (opts) {
  var self = this
  if (!(self instanceof Snapchat)) return new Snapchat(opts)
  if (!opts) opts = {}

  debug('new snapchat client')

  self._account = new Account(self, opts)
  self._chat = new Chat(self, opts)
  self._device = new Device(self, opts)
  self._friends = new Friends(self, opts)
  self._snaps = new Snaps(self, opts)
  self._stories = new Stories(self, opts)
}

/**
 * Account routes.
 *
 * @name Snapchat#account
 * @property {Account}
 * @readonly
 */
Object.defineProperty(Snapchat.prototype, 'account', {
  get: function () { return this._account }
})

/**
 * Chat routes.
 *
 * @name Snapchat#chat
 * @property {Chat}
 * @readonly
 */
Object.defineProperty(Snapchat.prototype, 'chat', {
  get: function () { return this._chat }
})

/**
 * Device routes.
 *
 * @name Snapchat#device
 * @property {Device}
 * @readonly
 */
Object.defineProperty(Snapchat.prototype, 'device', {
  get: function () { return this._device }
})

/**
 * Friend routes.
 *
 * @name Snapchat#friends
 * @property {Friends}
 * @readonly
 */
Object.defineProperty(Snapchat.prototype, 'friends', {
  get: function () { return this._friends }
})

/**
 * Snap routes.
 *
 * @name Snapchat#snaps
 * @property {Snaps}
 * @readonly
 */
Object.defineProperty(Snapchat.prototype, 'snaps', {
  get: function () { return this._snaps }
})

/**
 * Story routes.
 *
 * @name Snapchat#stories
 * @property {Stories}
 * @readonly
 */
Object.defineProperty(Snapchat.prototype, 'stories', {
  get: function () { return this._stories }
})

/**
 * The username of the currently signed in (or not yet singed in) user.
 * (Always lowercase)
 *
 * @name Snapchat#username
 * @property {string}
 */
Object.defineProperty(Snapchat.prototype, 'username', {
  get: function () {
    var self = this
    return self._username
  }
})

/**
 * The username of the currently signed in (or not yet singed in) user.
 * @note Always lowercase.
 *
 * @name Snapchat#session
 * @property {Session}
 */
Object.defineProperty(Snapchat.prototype, 'session', {
  get: function () {
    var self = this
    return self._session
  },

  set: function (session) {
    var self = this
    self._session = session

    if (session) {
      self._username = session.username
      self._authToken = session.authToken
    } else {
      self._username = null
      self._authToken = null
    }
  }
})

/**
 * The size of your device's screen.
 *
 * @name Snapchat#screenSize
 * @property {Object}
 */
Object.defineProperty(Snapchat.prototype, 'screenSize', {
  get: function () {
    var self = this
    return self._screenSize
  }
})

/**
 * The maximum size to load videos in.
 *
 * @name Snapchat#maxVideoSize
 * @property {Object}
 */
Object.defineProperty(Snapchat.prototype, 'maxVideoSize', {
  get: function () {
    var self = this
    return self._maxVideoSize
  }
})

/**
 * Whether or not this client is signed in.
 *
 * @name Snapchat#isSignedIn
 * @property {boolean}
 */
Object.defineProperty(Snapchat.prototype, 'isSignedIn', {
  get: function () {
    var self = this
    return self._googleAuthToken && self._authToken && self._username
  }
})

/**
 * Used internally to sign in.
 *
 * @name Snapchat#authToken
 * @property {string}
 */
Object.defineProperty(Snapchat.prototype, 'authToken', {
  get: function () {
    var self = this
    return self._authToken
  }
})

/**
 * Used internally to sign in.
 *
 * @name Snapchat#googleAuthToken
 * @property {string}
 */
Object.defineProperty(Snapchat.prototype, 'googleAuthToken', {
  get: function () {
    var self = this
    return self._googleAuthToken
  }
})

/**
 * Used internally.
 *
 * @name Snapchat#deviceToken1i
 * @property {string}
 */
Object.defineProperty(Snapchat.prototype, 'deviceToken1i', {
  get: function () {
    var self = this
    return self._deviceToken1i
  }
})

/**
 * Used internally.
 *
 * @name Snapchat#deviceToken1v
 * @property {string}
 */
Object.defineProperty(Snapchat.prototype, 'deviceToken1v', {
  get: function () {
    var self = this
    return self._deviceToken1v
  }
})

/**
 * Used internally to sign in and trick Snapchat into thinking we're using the first party client.
 *
 * @name Snapchat#googleAttestation
 * @property {string}
 */
Object.defineProperty(Snapchat.prototype, 'googleAttestation', {
  get: function () {
    var self = this
    return self._googleAttestation
  }
})

/**
 * Signs into Snapchat.
 *
 * A valid GMail account is necessary to trick Snapchat into thinking we're using the first party client.
 *
 * Note that username, password, gmailEmail, and gmailPassword are all optional only if
 * their environment variable equivalents exist. E.g.,
 *
 * SNAPCHAT_USERNAME
 * SNAPCHAT_PASSWORD
 * SNAPCHAT_GMAIL_EMAIL
 * SNAPCHAT_GMAIL_PASSWORD
 *
 * @param {string} Optional username The Snapchat username to sign in with.
 * @param {string} Optional password The password to the Snapchat account to sign in with.
 * @param {string} Optional gmailEmail A valid GMail address.
 * @param {string} Optional gmailPassword The password associated with gmailEmail.
 * @param {function} cb
 */
Snapchat.prototype.signIn = function (username, password, gmailEmail, gmailPassword, cb) {
  var self = this

  if (typeof username === 'function') {
    cb = username
    username = process.env.SNAPCHAT_USERNAME
    password = process.env.SNAPCHAT_PASSWORD
    gmailEmail = process.env.SNAPCHAT_GMAIL_EMAIL
    gmailPassword = process.env.SNAPCHAT_GMAIL_PASSWORD
  }

  return new Promise(function (resolve, reject) {
    if (!(username && password && gmailEmail && gmailPassword)) {
      return reject(new Error('missing required login credentials'))
    }

    debug('Snapchat.signIn (username %s)', username)

    self._getGoogleAuthToken(gmailEmail, gmailPassword, function (err, googleAuthToken) {
      if (err) {
        debug('error getting google auth token')
        return reject(err)
      }

      var timestamp = StringUtils.timestamp()

      self._getAttestation(username, password, timestamp, function (err, attestation) {
        if (err) {
          debug('error getting attestation')
          return reject(err)
        }

        self._getGoogleCloudMessagingIdentifier(function (err, ptoken) {
          if (err) {
            debug('error getting google cloud messaging identifier')
            return reject(err)
          }

          self._getDeviceTokens(function (err, deviceTokens) {
            if (err) {
              debug('error getting device token')
              return reject(err)
            }

            // NOTE: this is a temporary requirement which unfortunately sends
            // the username and password via plaintext and relies on casper's
            // servers until we figure out a workaround for generating
            // X-Snapchat-Client-Auth
            if (true) {
              self._getClientAuthToken(username, password, timestamp, function (err, clientAuthToken) {
                if (err) {
                  debug('error generating client auth token')
                  return reject(err)
                } else {
                  self.signInWithData(self._makeSignInData(googleAuthToken, attestation, ptoken, clientAuthToken, deviceTokens, timestamp), username, password)
                  .then(resolve)
                  .catch(reject)
                }
              })
            } else {
              self.signInWithData(self._makeSignInData(googleAuthToken, attestation, ptoken, '', deviceTokens, timestamp), username, password)
              .then(resolve)
              .catch(reject)
            }
          })
        })
      })
    })
  }).nodeify(cb)
}

Snapchat.prototype._makeSignInData = function (googleAuthToken, attestation, ptoken, clientAuthToken, deviceTokens, timestamp) {
  return {
    googleAuthToken: googleAuthToken,
    attestation: attestation,
    pushToken: ptoken,
    clientAuthToken: clientAuthToken,
    deviceTokens: deviceTokens,
    timestamp: timestamp
  }
}

Snapchat.prototype.signInWithData = function (data, username, password, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    var googleAuthToken = data.googleAuthToken
    var attestation = data.attestation
    var ptoken = data.pushToken
    var clientAuthToken = data.clientAuthToken
    var deviceTokens = data.deviceTokens
    var timestamp = data.timestamp

    self._googleAuthToken = googleAuthToken
    self._googleAttestation = attestation
    self._deviceToken1i = deviceTokens[constants.core.deviceToken1i]
    self._deviceToken1v = deviceTokens[constants.core.deviceToken1v]

    var reqToken = StringUtils.hashSCString(constants.core.staticToken, timestamp)
    var preHash = StringUtils.getSCPreHashString(username, password, timestamp, reqToken)
    var deviceHash = StringUtils.hashHMacToBase64(preHash, self._deviceToken1v).substr(0, 20)

    var params = {
      'username': username,
      'password': password,
      'width': constants.screen.width,
      'height': constants.screen.height,
      'max_video_width': constants.screen.maxVideoWidth,
      'max_video_height': constants.screen.maxVideoHeight,
      'application_id': 'com.snapchat.android',
      'is_two_fa': 'false',
      'ptoken': ptoken,
      'pre_auth': '',
      'sflag': 1,
      'dsig': deviceHash,
      'dtoken1i': self._deviceToken1i,
      'attestation': self._googleAttestation,
      'timestamp': timestamp
    }

    var headers = { }
    headers[constants.headers.clientAuthToken] = 'Bearer ' + self._googleAuthToken
    headers[constants.headers.clientAuth] = clientAuthToken

    var opts = {
      'timestamp': timestamp
    }

    Request.postCustom(constants.endpoints.account.login, params, headers, null, opts, function (err, result) {
      if (err) {
        debug('Snapchat.signIn error %s', err)
        return reject(err)
      } else if (result) {
        self.session = new Session(self, result)
        return resolve(self.session)
      }

      err = new Error('Snapchat.signIn parse error')
      err.data = result
      return reject(err)
    })
  }).nodeify(cb)
}

/**
 * Use this to restore a session that ended within the last hour. The google auth token must be re-generated every hour.
 *
 * @param {string} username Your Snapchat username.
 * @param {string} authToken Your Snapchat auth token. Can be retrieved from the authToken property.
 * @param {string} googleAuthToken Your Google auth token. Can be retrieved from the googleAuthToken property.
 * @param {function} cb
 */
Snapchat.prototype.restoreSession = function (username, authToken, googleAuthToken, cb) {
  var self = this
  debug('Snapchat.restoreSession (username %s)', username)

  self._username = username
  self._authToken = authToken
  self._googleAuthToken = googleAuthToken

  return self.updateSession(cb)
}

/**
 * Signs out.
 *
 * @param {function} cb
 */
Snapchat.prototype.signOut = function (cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.signOut (%s)', self.username)

    if (!self.isSignedIn) {
      return reject(new Error('signin required'))
    }

    self.post(constants.endpoints.account.logout, {
      'username': self._username
    }, function (err, result) {
      if (err) {
        debug('Snapchat.signOut error %s', err)
        return reject(err)
      } else if (result && result.length === 0) {
        self._session = null
        self._username = null
        self._authToken = null
        self._googleAuthToken = null
        self._googleAttestation = null
        self._deviceToken1i = null
        self._deviceToken1v = null
        return resolve()
      }
      return reject(new Error('Snapchat.signOut parse error'))
    })
  }).nodeify(cb)
}

/**
 * Updates all information in the session property.
 *
 * @param {function} cb
 */
Snapchat.prototype.updateSession = function (cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.updateSession')

    if (!self.isSignedIn) {
      return reject(new Error('signin required'))
    }

    self.post(constants.endpoints.update.all, {
      'username': self._username,
      'width': constants.screen.width,
      'height': constants.screen.height,
      'max_video_width': constants.screen.maxVideoWidth,
      'max_video_height': constants.screen.maxVideoHeight,
      'include_client_settings': 'true'
    }, function (err, result) {
      if (err) {
        debug('updateSession error %s', err)
        return reject(err)
      } else if (result) {
        self.session = new Session(self, result)
        return resolve(self.session)
      }

      return reject(new Error('updateSession error'))
    })
  }).nodeify(cb)
}

/**
 * The first step in creating a new Snapchat account.
 * Registers an email, password, and birthday in preparation for creating a new account.
 *
 * The result passed to cb has the following keys:
 *  - email: the email you registered with.
 *  - snapchat_phone_number: a number you can use to verify your phone number later.
 *  - username_suggestions: an array of available usernames for the next step.
 *
 * @param {string} email The email address to be associated with the account.
 * @param {string} password The password of the account to be created.
 * @param {string} birthday Your birthday, in the format YYYY-MM-DD.
 * @param {function} cb
 */
Snapchat.prototype.registerEmail = function (email, password, birthday, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.registerEmail (email %s)', email)

    self.post(constants.endpoints.account.registration.start, {
      'email': email,
      'password': password,
      'birthday': birthday
    }, function (err, result) {
      if (err) {
        debug('registerEmail error %s', err)
        return reject(err)
      } else if (result && !!result.logged) {
        return resolve(result)
      }
      return reject(new Error('registerEmail parse error'))
    })
  }).nodeify(cb)
}

/**
 * The second step in creating a new Snapchat account.
 * Registers a username with an email that was registered in the first step.
 * You must call this method after successfully completing the first step in registration.
 *
 * @param {string} username The username of the account to be created, trimmed to the first 15 characters.
 * @param {string} registeredEmail The previously registered email address associated with the account (from the first step of registration).
 * @param {string} gmailEmail A valid GMail address. Required to make Snapchat think this is an official client.
 * @param {string} gmailPassword The password to the Google account associated with gmailEmail.
 * @param {function} cb
 */
Snapchat.prototype.registerUsername = function (username, registeredEmail, gmailEmail, gmailPassword, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.registerUsername (username %s, email %s)', username, registeredEmail)

    self._getGoogleAuthToken(gmailEmail, gmailPassword, function (err, googleAuthToken) {
      if (err) {
        debug('could not retrieve google auth token')
        return reject(err)
      }

      self.post(constants.endpoints.account.registration.username, {
        'username': registeredEmail,
        'selected_username': username
      }, function (err, result) {
        if (err) {
          debug('registerUsername error %s', err)
          return reject(err)
        } else if (result) {
          self.session = new Session(self, result)
          self._googleAuthToken = googleAuthToken
          return resolve()
        }
        return reject(new Error('registerUsername parse error'))
      })
    })
  }).nodeify(cb)
}

/**
 * The third and final step in registration.
 * If you don't want to verify your humanity a phone number,
 * you can verify it by with a 'captcha' image of sorts.
 *
 * @param {string} mobile A 10-digit (+ optional country code, defaults to 1) mobile phone number to be associated with the account, in any format. i.e. +11234567890, (123) 456-7890, 1-1234567890
 * @param {boolean} sms YES if you want a code sent via SMS, NO if you want to be called for verification.
 * @param {function} cb
 */
Snapchat.prototype.sendPhoneVerification = function (mobile, sms, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.sendPhoneVerification (mobile %s, sms %d)', mobile, sms)

    if (!self.isSignedIn) {
      return reject(new Error('signin required'))
    }

    mobile = phone(mobile)
    var countryCode = +mobile[1]
    mobile = mobile.substr(2)

    self.post(constants.endpoints.account.registration.verifyPhone, {
      'username': self._username,
      'phoneNumber': mobile,
      'countryCode': countryCode,
      'action': 'updatePhoneNumber',
      'skipConfirmation': true
    }, function (err, result) {
      if (err) {
        debug('sendPhoneVerification error %s', err)
        return reject(err)
      }

      debug('sendPhoneVerification result %j', result)
      return resolve(result)
    })
  }).nodeify(cb)
}

/**
 * Verifies your phone number, completing registration.
 *
 * @warning cb is not called in this implementaiton because I haven't tested it yet.
 * @todo: document response, get cb working
 * @param {string} code The code sent to verify your number.
 * @param {function} cb
*/
Snapchat.prototype.verifyPhoneNumber = function (code, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.verifyPhoneNumber (code %s)', code)

    if (!self.isSignedIn) {
      return reject(new Error('signin required'))
    }

    Request.post(constants.endpoints.account.registration.verifyPhone, {
      'action': 'verifyPhoneNumber',
      'username': self._username,
      'code': code
    }, self._googleAuthToken, constants.core.staticToken, function (err, result) {
      if (err) {
        debug('verifyPhoneNumber error %s', err)
        return reject(err)
      }

      debug('verifyPhoneNumber result %j', result)
      return resolve(result)
    })
  }).nodeify(cb)
}

/**
 * Downloads captcha images to verify a new account with.
 * cb will be called with an array of 9 Blobs representing captcha images.
 *
 * @param {function} cb
 */
Snapchat.prototype.getCaptcha = function (cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.getCaptcha')

    if (!self.isSignedIn) {
      return reject(new Error('signin required'))
    }

    self.post(constants.endpoints.account.registration.getCaptcha, {
      'username': self._username
    }, function (err, body) {
      if (err) {
        debug('getCaptcha error %s', err)
        return reject(err)
      }

      zlib.gunzip(new Buffer(body), function (err, data) {
        if (err) {
          debug('getCaptcha gunzip error %s', err)
          return reject(err)
        }

        // TODO
        return reject(new Error('Snapchat.getCaptcha TODO'))
      })
    })
  }).nodeify(cb)
}

/**
 * Use this to 'solve' a captcha.
 * @warning Seems to not be working.
 * @todo: document response
 *
 * @param {string} solution The solution to the captcha as a binary string. If the first, second, and last images contain ghosts, the solution would be '110000001'.
 * @param {function} cb
 */
Snapchat.prototype.solveCaptcha = function (solution, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.solveCaptcha')

    if (!self.isSignedIn) {
      return reject(new Error('signin required'))
    }

    return reject(new Error('Snapchat.solveCaptcha TODO'))
  }).nodeify(cb)
}

/**
 * Initializes a POST request to the Snapchat API.
 *
 * @param {string} endpoint Snapchat API endpoint
 * @param {Object} params Form data (will be augmented with required snapchat API params)
 * @param {function} cb
 */
Snapchat.prototype.post = function (endpoint, params, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.post (%s)', endpoint)

    Request.post(endpoint, params, self._googleAuthToken, self._authToken, function (err, result) {
      if (err) {
        return reject(err)
      }
      return resolve(result)
    })
  }).nodeify(cb)
}

/**
 * [get description]
 *
 * @param  {string}   endpoint
 * @param  {function} cb
 */
Snapchat.prototype.get = function (endpoint, cb) {
  return new Promise(function (resolve, reject) {
    debug('Snapchat.get (%s)', endpoint)

    Request.get(endpoint, function (err, result) {
      if (err) {
        return reject(err)
      } else {
        return resolve(result)
      }
    })
  }).nodeify(cb)
}

/**
 * internal
 */
Snapchat.prototype.sendEvents = function (events, snapInfo, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Snapchat.sendEvents')

    if (!self.isSignedIn) {
      return reject(new Error('signin required'))
    }

    events = events || { }
    snapInfo = snapInfo || { }

    self._post(constants.endpoints.update.snaps, {
      'events': events,
      'json': snapInfo,
      'username': self._username
    }, function (err, result) {
      if (err) {
        debug('sendEvents error %s', err)
        return reject(err)
      }

      debug('sendEvents result %j', result)
      return resolve(result)
    })
  }).nodeify(cb)
}

/**
 * @private
 */
Snapchat.prototype._getGoogleAuthToken = function (gmailEmail, gmailPassword, cb) {
  var encryptedGmailPassword = StringUtils.encryptGmailPassword(gmailEmail, gmailPassword)

  Request.postRaw({
    url: 'https://android.clients.google.com/auth',
    form: {
      'google_play_services_version': '7097038',
      'device_country': 'us',
      'operatorCountry': 'us',
      'lang': 'en_US',
      'sdk_version': '19',
      'accountType': 'HOSTED_OR_GOOGLE',
      'Email': gmailEmail,
      'EncryptedPasswd': encryptedGmailPassword,
      // 'Passwd': gmailPassword, // unencrypted version
      'service': 'audience:server:client_id:694893979329-l59f3phl42et9clpoo296d8raqoljl6p.apps.googleusercontent.com',
      'source': 'android',
      'androidId': '378c184c6070c26c',
      'app': 'com.snapchat.android',
      'client_sig': '49f6badb81d89a9e38d65de76f09355071bd67e7',
      'callerPkg': 'com.snapchat.android',
      'callerSig': '49f6badb81d89a9e38d65de76f09355071bd67e7'
    },
    headers: {
      'device': '378c184c6070c26c',
      'app': 'com.snapchat.android',
      'Accept-Encoding': 'gzip',
      'User-Agent': 'GoogleAuth/1.4 (mako JDQ39)'
    }
  }, function (err, response, body) {
    if (err) {
      debug('_getGoogleAuthToken error %s', err)
      return cb(err)
    } else if (body) {
      var auth = StringUtils.matchGroup(body, /Auth=([\w\.-]+)/i, 1)

      if (auth) {
        return cb(null, auth)
      }
    }

    return cb('Snapchat._getGoogleAuthToken unknown error')
  })
}

// static cache of device tokens
var sDeviceToken1i = null
var sDeviceToken1v = null

/**
 * @private
 */
Snapchat.prototype._getDeviceTokens = function (cb) {
  // cache device tokens
  var dt1i = sDeviceToken1i
  var dt1v = sDeviceToken1v

  function completion () {
    var result = { }
    result[constants.core.deviceToken1i] = dt1i
    result[constants.core.deviceToken1v] = dt1v

    return cb(null, result)
  }

  if (dt1i && dt1v) {
    return completion()
  } else {
    Request.post(constants.endpoints.device.identifier, { }, null, null, function (err, result) {
      if (err) {
        debug('_getDeviceTokens error %s', err)
        return cb(err)
      } else if (result) {
        dt1i = result[constants.core.deviceToken1i]
        dt1v = result[constants.core.deviceToken1v]

        if (dt1i && dt1v) {
          sDeviceToken1i = dt1i
          sDeviceToken1v = dt1v

          return completion()
        }
      }

      debug('Snapchat._getDeviceTokens parse error %j', result)
      return cb('Snapchat._getDeviceTokens parse error')
    })
  }
}

/**
 * ptoken value
 * @private
 */
Snapchat.prototype._getGoogleCloudMessagingIdentifier = function (cb) {
  var DEFAULT_TOKEN = 'ie'

  process.nextTick(function () {
    cb(null, DEFAULT_TOKEN)
  })

  // TODO: cloud messaging identifier always returns 'Error=AUTHENTICATION_FAILED'
  // skipping this for now to speedup testing

  /*
  Request.postRaw({
    url: 'https://android.clients.google.com/c2dm/register3',
    form: {
      'X-google.message_id': 'google.rpc1',
      'device': 4343470343591528399,
      'sender': 191410808405,
      'app_ver': 706,
      'gcm_ver': 7097038,
      'app': 'com.snapchat.android',
      'iat': (new Date()).getTime(),
      'cert': '49f6badb81d89a9e38d65de76f09355071bd67e7'
    },
    headers: {
      'Accept-Language': 'en',
      'Accept-Locale': 'en_US',
      'app': 'com.snapchat.android',
      'Authorization': 'AidLogin 4343470343591528399:5885638743641649694',
      'Gcm-ver': '7097038',
      'Accept-Encoding': 'gzip',
      'User-Agent': 'Android-GCM/1.5 (A116 _Quad KOT49H)'
    }
  }, function (err, response, body) {
    if (err) {
      return cb(err)
    } else if (body) {
      // parse token
      var token = StringUtils.matchGroup(body, /token=([\w\.-]+)/, 1)

      if (token) {
        return cb(null, token)
      } else {
        // debug('_getGoogleCloudMessagingIdentifier using default token %s', body)

        // default token
        return cb(null, DEFAULT_TOKEN)
      }
    }

    debug('_getGoogleCloudMessagingIdentifier parse error %s', body)
    return cb('_getGoogleCloudMessagingIdentifier error')
  })*/
}

/**
 * Attestation, courtesy of casper.io
 * @private
 */
Snapchat.prototype._getAttestation = function (username, password, ts, cb) {
  var preHash = StringUtils.getSCPreHashString(username, password, ts, constants.endpoints.account.login)
  var nonce = StringUtils.sha256HashToBase64(preHash)

  var params = {
    'nonce': nonce,
    'authentication': constants.attestation.auth,
    'apk_digest': constants.attestation.digest(),
    'timestamp': ts
  }

  Request.postRaw({
    url: constants.attestation.URLCasper,
    form: params
  }, function (err, response, result) {
    if (err) {
      return cb(err)
    } else if (result && +result.code === 200 && result.attestation) {
      return cb(null, result.attestation)
    }

    return cb('Snapchat._getAttestation unknown error')
  })
}

/**
 * Client Auth Token, courtesy of casper.io
 *
 * Note that casper.io uses libscplugin.so which has been extracted from the
 * Android Snapchat client.
 *
 * @private
 */
Snapchat.prototype._getClientAuthToken = function (username, password, ts, cb) {
  Request.postRaw({
    url: constants.attestation.URLCasperAuth,
    form: {
      username: username,
      password: password,
      timestamp: ts
    }
  }, function (err, response, result) {
    if (err) {
      return cb(err)
    } else if (result && +result.code === 200 && result.signature) {
      return cb(null, result.signature)
    }

    return cb('Snapchat._getClientAuthToken unknown error')
  })
}