diff --git a/docs/API.md b/docs/API.md index 8523731..ad1ee51 100644 --- a/docs/API.md +++ b/docs/API.md @@ -56,7 +56,7 @@ If you reply with a string, the response will be packaged in a bot-specific form Individual bots support more complex responses, such as buttons, attachments and so on. You can send all those responses by replying with an object, instead of a string. In that case, _Claudia Bot Builder_ does not transform the response at all, and just passes it back to the sender. It's then your responsibility to ensure that the resulting object is in the correct format for the bot engine. Use `request.type` to discover the bot engine sending the requests. -If you reply with an array multiple messages will be sent in sequence. Each array item can be text or already formatted object and it'll follow the same rules explained above. At the moment, this is supported for Facebook Messenger only. +If you reply with an array multiple messages will be sent in sequence. Each array item can be text or already formatted object and it'll follow the same rules explained above. At the moment, this is supported for Facebook Messenger, GroupMe, Telegram, and Viber only. Additionally, _Claudia Bot Builder_ exports message generators for for generating more complex responses including buttons and attachments for Facebook and Slack and function for sending delayed/multiple replies. diff --git a/lib/facebook/reply.js b/lib/facebook/reply.js index 1faf42f..c1a9f04 100644 --- a/lib/facebook/reply.js +++ b/lib/facebook/reply.js @@ -1,6 +1,7 @@ 'use strict'; const rp = require('minimal-request-promise'), + promiseEach = require('../promise-each'), breakText = require('../breaktext'); module.exports = function fbReply(recipient, message, fbAccessToken) { @@ -30,13 +31,6 @@ module.exports = function fbReply(recipient, message, fbAccessToken) { }; return rp.post(`https://graph.facebook.com/v2.6/me/messages?access_token=${fbAccessToken}`, options); }, - sendAll = function () { - if (!messages.length) { - return Promise.resolve(); - } else { - return sendSingle(messages.shift()).then(sendAll); - } - }, messages = []; function breakTextAndReturnFormatted(message) { @@ -58,5 +52,5 @@ module.exports = function fbReply(recipient, message, fbAccessToken) { } else { messages = [message]; } - return sendAll(); + return promiseEach(sendSingle)(messages); }; diff --git a/lib/groupme/reply.js b/lib/groupme/reply.js index 33d34ca..e386072 100644 --- a/lib/groupme/reply.js +++ b/lib/groupme/reply.js @@ -1,7 +1,8 @@ 'use strict'; const rp = require('minimal-request-promise'); +const promiseEach = require('../promise-each'); -function gmReply(message, botId) { +function sendSingle(message, botId) { var data = { bot_id: botId, text: typeof message === 'string' ? message : message.text @@ -17,4 +18,8 @@ function gmReply(message, botId) { return rp.post('https://api.groupme.com/v3/bots/post', options); } -module.exports = gmReply; \ No newline at end of file +function gmReply(message, botId) { + return promiseEach(m => sendSingle(m, botId))(message); +} + +module.exports = gmReply; diff --git a/lib/promise-each.js b/lib/promise-each.js new file mode 100644 index 0000000..638f4df --- /dev/null +++ b/lib/promise-each.js @@ -0,0 +1,16 @@ +// adapted from https://raw.githubusercontent.com/yoshuawuyts/promise-each/4d4397e52bea67cd94b0e13010e95ca68fd625f0/index.js + +module.exports = each; + +// apply a function to all values +// should only be used for side effects +// (fn) -> prom +function each (fn) { + return (arr) => { + arr = Array.isArray(arr) ? arr : [arr]; + + return arr.reduce((prev, curr, i) => { + return prev.then(() => fn(curr, i, arr.length)); + }, Promise.resolve()).then(() => arr); + }; +} diff --git a/lib/telegram/reply.js b/lib/telegram/reply.js index 1baca58..e963cd6 100644 --- a/lib/telegram/reply.js +++ b/lib/telegram/reply.js @@ -1,6 +1,7 @@ 'use strict'; const rp = require('minimal-request-promise'); +const promiseEach = require('../promise-each'); const REQUEST_THROTTLE = 1000/30; const RETRIABLE_ERRORS = ['ECONNRESET', 'ENOTFOUND', 'ESOCKETTIMEDOUT', 'ETIMEDOUT', 'ECONNREFUSED', 'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN']; const NUMBER_OF_RETRIES = 20; @@ -59,26 +60,13 @@ module.exports = function tlReply(messageObject, message, tlAccessToken) { }; return sendHttpRequest(tlAccessToken, method, options, numberOfRetries); - }, - - sendAll = function () { - if (!messages.length) { - return Promise.resolve(); - } else { - return sendSingle(messages.shift()) - .then(() => { - return new Promise((resolve) => { - if (!messages.length) - resolve(); - else - setTimeout(resolve, REQUEST_THROTTLE); - }); - }) - .then(sendAll); - } - }, - - messages = Array.isArray(message) ? message : [message]; + }; - return sendAll(); + return promiseEach(m => + sendSingle(m).then(() => + new Promise(resolve => + setTimeout(resolve, REQUEST_THROTTLE) + ) + ) + )(message); }; diff --git a/lib/viber/reply.js b/lib/viber/reply.js index 665f110..ac97d58 100644 --- a/lib/viber/reply.js +++ b/lib/viber/reply.js @@ -1,5 +1,6 @@ 'use strict'; const rp = require('minimal-request-promise'); +const promiseEach = require('../promise-each'); function sendSingle(receiver, authToken, messageObj) { let message; @@ -30,20 +31,8 @@ function sendSingle(receiver, authToken, messageObj) { return rp.post('https://chatapi.viber.com/pa/send_message', options); } -function sendAll(receiver, authToken, messages) { - if (!messages.length) { - return Promise.resolve(); - } else { - return sendSingle(receiver, authToken, messages.shift()) - .then(() => sendAll(receiver, authToken, messages)); - } -} - function sendReply(receiver, messages, authToken) { - if (!Array.isArray(messages)) - messages = [messages]; - - return sendAll(receiver, authToken, messages); + return promiseEach(m => sendSingle(receiver, authToken, m))(messages); } module.exports = sendReply; diff --git a/spec/groupme/groupme-reply-spec.js b/spec/groupme/groupme-reply-spec.js index 114966d..20aeec8 100644 --- a/spec/groupme/groupme-reply-spec.js +++ b/spec/groupme/groupme-reply-spec.js @@ -48,4 +48,29 @@ describe('GroupMe Reply', () => { reply({ sender: 1, text: 'hello groupme', originalRequest: {}, type: 'groupme'}, 123123).then(done, done.fail); }); + describe('when an array is passed', () => { + it('does not send the second request until the first one completes', done => { + let answers = ['foo', 'bar']; + https.request.pipe(() => { + Promise.resolve().then(() => { + expect(https.request.calls.length).toEqual(1); + }).then(done); + }); + reply(answers, 123123); + }); + it('sends the requests in sequence', done => { + let answers = ['foo', 'bar']; + https.request.pipe(function () { + this.respond('200', 'OK'); + if (https.request.calls.length === 2) { + expect(JSON.parse(https.request.calls[0].body[0])).toEqual({bot_id: 123123, text:'foo'}); + expect(JSON.parse(https.request.calls[1].body[0])).toEqual({bot_id: 123123, text:'bar'}); + done(); + } + }); + reply(answers, 123123); + + }); + + }); });