module.exports = Chat
var debug = require('debug')('snapchat:chat')
var async = require('async')
var extend = require('xtend')
var Promise = require('bluebird')
var constants = require('../lib/constants')
var StringUtils = require('../lib/string-utils')
var Conversation = require('../models/conversation')
/**
* Snapchat wrapper for chat-related API calls.
*
* @class
* @param {Object} opts
*/
function Chat (client, opts) {
var self = this
if (!(self instanceof Chat)) return new Chat(client, opts)
if (!opts) opts = {}
self.client = client
}
/**
* Sends the typing notification to the given users.
*
* @param {Array<string>} recipients An array of username strings.
* @param {function} cb
*/
Chat.prototype.sendTypingToUsers = function (recipients, cb) {
var self = this
debug('Chat.sendTypingToUsers')
return self._sendTyping(recipients, cb)
}
/**
* Sends the typing notification to a single user.
*
* @param {string} username
* @param {function} cb
*/
Chat.prototype.sendTypingToUser = function (username, cb) {
var self = this
debug('Chat.sendTypingToUser (%s)', username)
return self._sendTyping([ username ], cb)
}
/**
* Marks all chat messages in a conversation as read.
*
* @TODO currently not working
* @param {Conversation} conversation
* @param {function} cb
*/
Chat.prototype.markRead = function (conversation, cb) {
var self = this
debug('Chat.markRead (%s)', conversation.identifier)
return self.client.sendEvents([
{
'eventName': 'CHAT_TEXT_VIEWED',
'params': {
'id': conversation.identifier
},
'ts': StringUtils.timestamp() | 0
}
], null, cb)
}
/**
* Retrieves the conversation auth mac and payload for a conversation with username.
*
* @param {string} username
* @param {function} cb
*/
Chat.prototype.conversationAuth = function (username, cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.conversationAuth (%s)', username)
var cid = StringUtils.SCIdentifier(self.client.username, username)
self.client.post(constants.endpoints.chat.authToken, {
'username': self.client.username,
'conversation_id': cid
}, function (err, result) {
if (err) {
return reject(err)
} else if (result) {
result = result['messaging_auth']
if (result && result['mac'] && result['payload']) {
return resolve(result)
}
return reject(new Error('Chat.conversationAuth parse error'))
}
})
}).nodeify(cb)
}
/**
* Retrieves the conversation with \e username.
*
* @param {string} username
* @param {function} cb
*/
Chat.prototype.conversationWithUser = function (username, cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.conversationWithUser (%s)', username)
self.conversationsWithUsers([ username ], function (err, results) {
if (err) {
return reject(err)
} else {
return resolve(results.conversations[0])
}
})
}).nodeify(cb)
}
/**
* Fetches the conversations for all users in \e usernames
*
* @param {Array<string>} usernames
* @param {function} cb
*/
Chat.prototype.conversationsWithUsers = function (usernames, cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.conversationsWithUsers (%j)', usernames)
var results = {
conversations: [ ],
failed: [ ],
errors: [ ]
}
var messages = [ ]
async.eachLimit(usernames, 4, function (username, cb) {
self.conversationAuth(username, function (err, auth) {
if (err) {
results.failed.push(username)
results.errors.push(err)
return cb(err)
} else {
var identifier = StringUtils.uniqueIdentifer()
var header = {
'auth': auth,
'to': [ username ],
'conv_id': StringUtils.SCIdentifier(self.client.username, username),
'from': self.client.username,
'conn_sequence_number': 0
}
var first = {
'presences': { },
'receiving_video': false,
'supports_here': true,
'header': header,
'retried': false,
'id': identifier,
'type': 'presence'
}
first.presences[self.client.username] = true
first.presences[username] = false
var header2 = extend(header, {
'conv_id': StringUtils.SCIdentifier(username, self.client.username)
})
var second = {
'presences': { },
'receiving_video': false,
'supports_here': true,
'header': header2,
'retried': false,
'id': identifier,
'type': 'presence'
}
second.presences[self.client.username] = true
second.presences[username] = false
messages.push(first)
messages.push(second)
return cb(null)
}
})
}, function () {
self.client.post(constants.endpoints.chat.sendMessage, {
'auth_token': self.client.authToken,
'messages': JSON.stringify(messages),
'username': self.client.username
}, function (err, result) {
if (err) {
return reject(err)
} else if (result) {
if (result.conversations) {
results.conversations = result.conversations.map(function (convo) {
return new Conversation(convo)
})
return resolve(results)
} else {
debug('Chat.conversationsWithUsers parse error %j', result)
}
}
return reject(new Error('Chat.conversationsWithUsers parse error'))
})
})
}).nodeify(cb)
}
/**
* Clears the conversation with the given identifier.
*
* @param {string} identifier The identifier of the conversation to clear.
* @param {function} cb
*/
Chat.prototype.clearConversationWithIdentifier = function (identifier, cb) {
var self = this
debug('Chat.clearConversationWithIdentifier (%s)', identifier)
return self.client.post(constants.endpoints.chat.clearConvo, {
'conversation_id': identifier,
'username': self.client.username
}, cb)
}
/**
* Clears the entire feed.
*
* @param {function} cb
*/
Chat.prototype.clearFeed = function (cb) {
var self = this
debug('Chat.clearFeed')
return self.client.post(constants.endpoints.chat.clearFeed, {
'username': self.client.username
}, cb)
}
/**
* Sends a message \e message to \e username.
*
* @param {string} message The message to send.
* @param {Array<string>} username The username of the recipient.
* @param {function} cb
*/
Chat.prototype.sendMessage = function (message, username, cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.sendMessage ("%s", %s)', message, username)
self.sendMessageToUsers(message, [ username ], function (err, results) {
if (err) {
return reject(err)
} else if (!results.conversations.length) {
return reject(new Error('Chat.conversationWithUser error'))
} else {
return resolve(results.conversations[0])
}
})
}).nodeify(cb)
}
/**
* Sends a message \e message to each user in \e usernames.
*
* @TODO: what to do if message fails to send
*
* @param {string} message The message to send.
* @param {Array<string>} usernames An array of username strings as recipients.
* @param {function} cb
*/
Chat.prototype.sendMessageToUsers = function (message, usernames, cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.sendMessageToUsers ("%s", %j)', message, usernames)
var results = {
conversations: [ ],
failed: [ ],
errors: [ ]
}
self.conversationsWithUsers(usernames, function (err, convoResults) {
if (err) {
return reject(err)
}
results.failed = convoResults.failed
results.errors = convoResults.errors
var messages = convoResults.conversations.map(function (convo) {
var identifier = StringUtils.uniqueIdentifer()
var sequenceNum = convo.state['conversation_state']
sequenceNum = sequenceNum && sequenceNum['user_sequences']
sequenceNum = sequenceNum && sequenceNum[self.client.username]
sequenceNum = sequenceNum | 0
var header = {
'auth': convo.messagingAuth,
'to': [ convo.recipient ],
'from': self.client.username,
// 'conn_sequence_number': 1,
'conn_sequ_num': 1,
'conv_id': convo.identifier
}
return {
'body': { 'type': 'text', 'text': message },
'chat_message_id': identifier,
'seq_num': sequenceNum + 1,
'timestamp': StringUtils.timestamp(),
'retried': false,
'id': identifier,
'type': 'chat_message',
'header': header
}
})
if (!messages.length) {
debug('Chat.sendMessageToUsers error retrieving conversations')
return reject(new Error('Chat.sendMessageToUsers error retrieving conversations'))
} else {
self.client.post(constants.endpoints.chat.sendMessage, {
'auth_token': self.client.authToken,
'messages': JSON.stringify(messages),
'username': self.client.username
}, function (err, result) {
if (err) {
return reject(err)
} else if (result) {
if (result.conversations && result.conversations) {
results.conversations = result.conversations.map(function (convo) {
return new Conversation(convo)
})
return resolve(results)
} else {
debug('Chat.sendMessageToUsers parse error %j', result)
}
}
return reject(new Error('Chat.sendMessageToUsers parse error'))
})
}
})
}).nodeify(cb)
}
/**
* Loads another page of conversations in the feed after the given conversation.
*
* This method will update client.session.conversations accordingly.
*
* @param {Conversation} conversation The conversation after which to load more conversations.
* @param {function} cb
*/
Chat.prototype.loadConversationsAfter = function (conversation, cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.loadConversationsAfter')
if (!conversation || !conversation.pagination || !conversation.pagination.length) {
return resolve([ ])
}
self.client.post(constants.endpoints.chat.conversations, {
'username': self.client.username,
'checksum': StringUtils.md5HashToHex(self.client.username),
'offset': conversation.pagination
}, function (err, result) {
if (err) {
return reject(err)
} else if (result) {
result = result['conversations_response']
if (result) {
var conversations = result.map(function (result) {
var convo = new Conversation(result)
self.client.session.conversations.push(convo)
return convo
})
return resolve(conversations)
}
return reject(new Error('Chat.loadConversationsAfter parse error'))
}
})
}).nodeify(cb)
}
/**
* Loads all conversations into the current session.
*
* This method will update client.session.conversations accordingly.
* @param {function} cb
*/
Chat.prototype.loadAllConversations = function (cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.loadAllConversations')
self.client.updateSession(function (err) {
if (err) {
return reject(err)
}
var conversations = [ ]
var last = self.client.session.conversations[self.client.session.conversations.length - 1]
function loadPage () {
self.loadConversationsAfter(last, function (err, convos) {
if (err) {
return reject(err)
} else if (convos.length > 0) {
conversations = conversations.concat(convos)
last = convos[convos.length - 1]
return loadPage()
}
conversations.forEach(function (convo) {
self.client.session.conversations.push(convo)
})
return resolve(conversations)
})
}
loadPage()
})
}).nodeify(cb)
}
/**
* Loads more messages after the given message or cash transaction.
*
* @param {Transaction|Message} messageOrTransaction any object conforming to Pagination \b EXCEPT AN \C Conversation.
* @warning Do not pass an Conversation object to messageOrTransaction. Doing so will throw an exception.
* @param {function} cb
*/
Chat.prototype.loadMessagesAfterPagination = function (messageOrTransaction, cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.loadMessagesAfterPagination')
if (messageOrTransaction instanceof Conversation ||
!messageOrTransaction.conversationIdentifier) {
return reject(new Error('Chat.loadMessagesAfterPagination invalid param'))
}
if (!messageOrTransaction ||
!messageOrTransaction.pagination ||
!messageOrTransaction.pagination.length) {
return resolve()
}
self.clients.post(constants.endpoints.chat.conversation, {
'username': self.client.username,
'conversation_id': messageOrTransaction.conversationIdentifier,
'offset': messageOrTransaction.pagination
}, function (err, result) {
if (err) {
return reject(err)
} else if (result && result.conversation) {
return resolve(new Conversation(result.conversation))
}
return reject(new Error('Chat.loadConversationsAfter parse error'))
})
}).nodeify(cb)
}
/**
* Loads all messages in the given thread and adds them to that Conversation object.
*
* @param {Conversation} conversation The conversation to load completely.
* @param {function} cb
*/
Chat.prototype.loadFullConversation = function (conversation, cb) {
var self = this
return new Promise(function (resolve, reject) {
debug('Chat.loadFullConversation')
var last = conversation.messages[conversation.messages.length - 1]
function loadPage () {
self.loadMessagesAfterPagination(last, function (err, convo) {
if (err) {
return reject(err)
} else if (convo) {
last = convo.messages[convo.messages.length - 1]
conversation.addMessagesFromConversation(convo)
return loadPage()
}
return resolve()
})
}
loadPage()
}).nodeify(cb)
}
/**
* @private
*/
Chat.prototype._sendTyping = function (recipients, cb) {
var self = this
return self.client.post(constants.endpoints.chat.typing, {
'recipient_usernames': JSON.stringify(recipients),
'username': self.client.username
}, cb)
}