Source: routes/friends.js

module.exports = Friends

var debug = require('debug')('snapchat:friends')
var Promise = require('bluebird')

var constants = require('../lib/constants')

var FoundFriend = require('../models/found-friend')
var NearbyUser = require('../models/nearby-user')
var User = require('../models/user')

/**
 * Friends wrapper for friends-related API calls.
 *
 * @class
 * @param {Object} opts
 */
function Friends (client, opts) {
  var self = this
  if (!(self instanceof Friends)) return new Friends(client, opts)
  if (!opts) opts = {}

  self.client = client
}

/**
 * Adds the users in toAdd as friends, and unfriends the users in toUnfriend.
 *
 * @param {Array<string>} toAdd An array of username strings of users to add. Doesn't matter if they're already in your friends.
 * @param {Array<string>} toUnfriend An array of username strings of users to un-friend. Doesn't matter if they're not already in your friends.
 * @param {function} cb
 */
Friends.prototype.addFriends = function (toAdd, toUnfriend, cb) {
  var self = this
  debug('Friends.addFriends (toAdd %j, toUnfriend %j)', toAdd, toUnfriend)

  if (!toAdd) toAdd = []
  if (!toUnfriend) toUnfriend = []

  return self.client.post(constants.endpoints.friends.friend, {
    'username': self.client.username,
    'action': 'multiadddelete',
    'friend': {
      friendsToAdd: JSON.stringify(toAdd),
      friendsToDelete: JSON.stringify(toUnfriend)
    },
    'added_by': 'ADDED_BY_USERNAME'
  }, cb)
}

/**
 * Adds username as a friend.
 *
 * @param {string} username The user to add.
 * @param {function} cb
 */
Friends.prototype.addFriend = function (username, cb) {
  var self = this
  debug('Friends.addFriend (%s)', username)

  return self.client.post(constants.endpoints.friends.friend, {
    'action': 'add',
    'friend': username,
    'username': self.client.username,
    'added_by': 'ADDED_BY_USERNAME'
  }, cb)
}

/**
 * Use this to add back a user who has added you as a friend. Sort of like accepting a friend request.
 *
 * This only affects the "added by" string the other user will see.
 * @param {string} username The username of the user to add back.
 * @param {function} cb
 */
Friends.prototype.addFriendBack = function (username, cb) {
  var self = this
  debug('Friends.addFriendBack (%s)', username)

  return self.client.post(constants.endpoints.friends.friend, {
    'action': 'add',
    'friend': username,
    'username': self.client.username,
    'added_by': 'ADDED_BY_ADDED_ME_BACK'
  }, cb)
}

/**
 * Unfriends username.
 *
 * @param {string} username The username of the user to unfriend.
 * @param {function} cb
 */
Friends.prototype.unfriend = function (username, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Friends.unfriend (%s)', username)

    self.client.post(constants.endpoints.friends.friend, {
      'action': 'delete',
      'friend': username,
      'username': self.client.username
    }, function (err) {
      if (err) {
        return reject(err)
      }

      self._removeFriendsFromSession([ { username: username } ])
      return resolve()
    })
  }).nodeify(cb)
}

/**
 * Finds friends given phone numbers and names.
 *
 * friends is a number->name map, where "name" is the desired screen name of that friend and "number" is their phone number.
 * The names given will be used as display names for any usernames found.
 *
 * @param {Object} friends a dictionary with phone number strings as the keys and name strings as the values.
 * @param {function} cb
 */
Friends.prototype.findFriends = function (friends, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Friends.findFriends (%j)', friends)

    if (self.client.session.shouldTextToVerifyNumber ||
        self.client.session.shouldCallToVerifyNumber) {
      return reject(new Error('Friends.findFriends error client needs to verify phone first'))
    }

    self.client.post(constants.endpoints.friends.find, {
      'username': self.client.username,
      'countryCode': self.client.session.countryCode,
      'numbers': JSON.stringify(friends)
    }, function (err, result) {
      if (err) {
        return reject(err)
      } else if (result && result.results) {
        var results = result.results.map(function (friend) {
          return new FoundFriend(friend)
        })
        return resolve(results)
      }

      return reject(new Error('Friends.findFriends parse error'))
    })
  }).nodeify(cb)
}

/**
 * Finds nearby snapchatters who are also looking for nearby snapchatters.
 *
 * @param {Object} location The location to search from { lat, lng }.
 * @param {number} accuracy The radius in meters to find nearby snapchatters at location. Defaults to 10.
 * @param {number} milliseconds The total poll duration so far. If you're polling in a for-loop for example, pass the time in milliseconds since you started polling. This has been guess-work, but I think it's right.
 * @param {function} cb
 */
Friends.prototype.findFriendsNear = function (location, accuracy, milliseconds, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Friends.findFriendsNear (%j)', location)

    if (accuracy <= 0) accuracy = 10

    self.client.post(constants.endpoints.friends.findNearby, {
      'username': self.client.username,
      'accuracyMeters': accuracy,
      'action': 'update',
      'lat': location.lat,
      'lng': location.lng,
      'totalPollingDurationMillis': milliseconds
    }, function (err, result) {
      if (err) {
        return reject(err)
      } else if (result && result['nearby_snapchatters']) {
        var results = result['nearby_snapchatters'].map(function (user) {
          return new NearbyUser(user['username'], user['user_id'])
        })
        return resolve(results)
      }

      return reject(new Error('Friends.findFriendNear parse error'))
    })
  }).nodeify(cb)
}

/**
 * Not sure what this is for.
 */
Friends.prototype.searchFriend = function (query, cb) {
  var self = this
  debug('Friends.searchFriend (%s)', query)

  return self.client.post(constants.endpoints.friends.search, {
    'query': query,
    'username': self.client.username
  }, cb)
}

/**
 * Checks to see whether username is a registered username.
 *
 * @param {string} username
 * @param {function} cb
 */
Friends.prototype.userExists = function (username, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Friends.userExists (%s)', username)

    self.client.post(constants.endpoints.friends.exists, {
      'request_username': username,
      'username': self.client.username
    }, function (err, result) {
      if (err) {
        return reject(err)
      } else if (result) {
        return resolve(!!result.exists)
      }

      return reject(new Error('Friends.userExists parse error'))
    })
  }).nodeify(cb)
}

/**
 * Updates the display name for one of your friends.
 *
 * @param {string} friend The username to give the new display name to.
 * @param {string} displayName The new display name.
 * @param {function} cb
 */
Friends.prototype.updateDisplayNameForUser = function (friend, displayName, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Friends.updateDisplayNameForUser (%s, "%s")', friend, displayName)

    self.client.post(constants.endpoints.friends.friend, {
      'action': 'display',
      'display': displayName,
      'friend': friend,
      'friend_id': '',
      'username': self.client.username
    }, function (err, result) {
      if (err) {
        return reject(err)
      } else if (result && result.object) {
        var updated = new User(result.object)

        self._removeFriendsFromSession([ updated ])
        self._addFriendsToSession([ updated ])
        return resolve(updated)
      } else {
        debug('Friends.updateDisplayNameForUser parse error %j', result)
      }

      return reject(new Error('Friends.updateDisplayNameForUser parse error'))
    })
  }).nodeify(cb)
}

/**
 * Blocks username.
 *
 * @param {string} username The username of the user to block.
 * @param {function} cb
 */
Friends.prototype.blockUser = function (username, cb) {
  var self = this
  debug('Friends.blockUser (%s)', username)

  return self._setUserBlocked(username, true, cb)
}

/**
 * Unblocks username.
 *
 * @param {string} username The username of the user to block.
 * @param {function} cb
 */
Friends.prototype.unblockUser = function (username, cb) {
  var self = this
  debug('Friends.unblockUser (%s)', username)

  return self._setUserBlocked(username, false, cb)
}

/**
 * This appears to be for an upcoming feature: suggested friends?
 *
 * @param {Array<string>} usernames.
 * @param {boolean} seen Whether to mark as seen.
 * @param {function} cb
 */
Friends.prototype.seenSuggestedFriends = function (usernames, seen, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Friends.seenSuggestedFriends (%j, %d)', usernames, seen)

    if (!usernames || !usernames.length) usernames = [ ]

    self.client.post(constants.endpoints.misc.suggestFriend, {
      'action': 'update',
      'seen': !!seen,
      'seen_suggested_friend_list': JSON.stringify(usernames),
      'username': self.client.username
    }, function (err, result) {
      if (err) {
        return reject(err)
      } else if (result) {
        return resolve(!!result.logged)
      }

      return reject(new Error('Friends.seenSuggestedFriends parse error'))
    })
  }).nodeify(cb)
}

/**
 * @private
 *
 * @param {Array<User>} friends
 */
Friends.prototype._removeFriendsFromSession = function (friends) {
  var self = this
  var friendsMap = { }

  friends.forEach(function (friend) {
    friendsMap[friend.username] = true
  })

  self.client.session.friends = self.client.session.friends.filter(function (friend) {
    return !(friend.username in friendsMap)
  })
}

/**
 * @private
 *
 * @param {Array<User>} friends
 */
Friends.prototype._addFriendsToSession = function (friends) {
  var self = this
  self.client.session.friends = self.client.session.friends.concat(friends)
}

/**
 * @private
 *
 * @param {string} username
 * @param {boolean} blocked
 * @param {function} cb
 */
Friends.prototype._setUserBlocked = function (username, blocked, cb) {
  var self = this

  return self.client.post(constants.endpoints.friends.friend, {
    'action': blocked ? 'block' : 'unblock',
    'friend': username,
    'username': self.client.username
  }, cb)
}