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) }