diff --git a/.gitignore b/.gitignore index 29a07e164..74b5a7fe8 100755 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ examples/db_team_bot/ */.DS_Store .env .idea -.vscode/settings.json \ No newline at end of file +.vscode +coverage diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 21ff6bb99..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/botframework_bot.js", - "stopOnEntry": false, - "args": [], - "cwd": "${workspaceRoot}", - "preLaunchTask": null, - "runtimeExecutable": null, - "runtimeArgs": [ - "--nolazy" - ], - "env": { - "NODE_ENV": "development" - }, - "console": "internalConsole", - "sourceMaps": false, - "outDir": null - }, - { - "name": "Attach", - "type": "node", - "request": "attach", - "port": 5858, - "address": "localhost", - "restart": false, - "sourceMaps": false, - "outDir": null, - "localRoot": "${workspaceRoot}", - "remoteRoot": null - }, - { - "name": "Attach to Process", - "type": "node", - "request": "attach", - "processId": "${command.PickProcess}", - "port": 5858, - "sourceMaps": false, - "outDir": null - } - ] -} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4619447cc..269fef495 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,8 @@ possible with your report. If you can, please include: * Create, or link to an existing issue identifying the need driving your PR request. The issue can contain more details of the need for the PR as well as host debate as to which course of action the PR will take that will most serve the common good. * Include screenshots and animated GIFs in your pull request whenever possible. * Follow the JavaScript coding style with details from `.jscsrc` and `.editorconfig` files and use necessary plugins for your text editor. +* Run `npm test` before submitting and fix any issues. +* Add tests to cover any new functionality. Add and/or update tests for any updates to the code. * Write documentation in [Markdown](https://daringfireball.net/projects/markdown). * Please follow, [JSDoc](http://usejsdoc.org/) for proper documentation. * Use short, present tense commit messages. See [Commit Message Styleguide](#git-commit-messages). diff --git a/__test__/lib/Botkit.test.js b/__test__/lib/Botkit.test.js new file mode 100644 index 000000000..bd040d362 --- /dev/null +++ b/__test__/lib/Botkit.test.js @@ -0,0 +1,28 @@ +'use strict'; + +let botkit; + +jest.mock('../../lib/CoreBot', () => 'corebot'); +jest.mock('../../lib/SlackBot', () => 'slackbot'); +jest.mock('../../lib/Facebook', () => 'facebook'); +jest.mock('../../lib/TwilioIPMBot', () => 'twilioipm'); +jest.mock('../../lib/TwilioSMSBot', () => 'twiliosms'); +jest.mock('../../lib/BotFramework', () => 'botframework'); +jest.mock('../../lib/CiscoSparkbot', () => 'spark'); +jest.mock('../../lib/ConsoleBot', () => 'console'); + +beforeEach(() => { + jest.clearAllMocks(); + botkit = require('../../lib/Botkit'); +}); + +test('exports bot interfaces', () => { + expect(botkit.core).toBe('corebot'); + expect(botkit.slackbot).toBe('slackbot'); + expect(botkit.facebookbot).toBe('facebook'); + expect(botkit.twilioipmbot).toBe('twilioipm'); + expect(botkit.twiliosmsbot).toBe('twiliosms'); + expect(botkit.botframeworkbot).toBe('botframework'); + expect(botkit.sparkbot).toBe('spark'); + expect(botkit.consolebot).toBe('console'); +}); diff --git a/__test__/lib/Slack_web_api.test.js b/__test__/lib/Slack_web_api.test.js new file mode 100644 index 000000000..1067b0e21 --- /dev/null +++ b/__test__/lib/Slack_web_api.test.js @@ -0,0 +1,322 @@ +'use strict'; + +let slackWebApi; +let mockRequest; +let mockResponse; +let mockBot; + +mockRequest = {}; + +jest.mock('request', () => mockRequest); + +beforeEach(() => { + mockResponse = { + statusCode: 200, + body: '{"ok": true}' + }; + + mockBot = { + config: {}, + debug: jest.fn(), + log: jest.fn(), + userAgent: jest.fn().mockReturnValue('jesting') + }; + + mockBot.log.error = jest.fn(); + + mockRequest.post = jest.fn().mockImplementation((params, cb) => { + cb(null, mockResponse, mockResponse.body); + }); + + slackWebApi = require('../../lib/Slack_web_api'); +}); + +describe('config', () => { + test('default api_root', () => { + const instance = slackWebApi(mockBot, {}); + expect(instance.api_url).toBe('https://slack.com/api/'); + }); + + test('setting api_root', () => { + mockBot.config.api_root = 'http://www.somethingelse.com'; + const instance = slackWebApi(mockBot, {}); + expect(instance.api_url).toBe('http://www.somethingelse.com/api/'); + }); +}); + +describe('callApi', () => { + let instance; + + test('uses data.token by default and post', () => { + const data = { + token: 'abc123' + }; + const cb = jest.fn(); + + instance = slackWebApi(mockBot, {}); + instance.callAPI('some.method', data, cb); + + expect(mockRequest.post).toHaveBeenCalledTimes(1); + const firstArg = mockRequest.post.mock.calls[0][0]; + expect(firstArg.form.token).toBe('abc123'); + }); + + test('uses config.token if data.token is missing', () => { + const data = {}; + const cb = jest.fn(); + + instance = slackWebApi(mockBot, { token: 'abc123' }); + instance.callAPI('some.method', data, cb); + + expect(mockRequest.post).toHaveBeenCalledTimes(1); + const firstArg = mockRequest.post.mock.calls[0][0]; + expect(firstArg.form.token).toBe('abc123'); + }); + + // this case is specific to callAPI, shared cases will be tested below + test(`handles multipart data`, () => { + const cb = jest.fn(); + instance = slackWebApi(mockBot, {}); + instance.callAPI('some.method', 'data', cb, true); + + expect(mockRequest.post).toHaveBeenCalledTimes(1); + const firstArg = mockRequest.post.mock.calls[0][0]; + + expect(firstArg.formData).toBe('data'); + expect(firstArg.form).toBeUndefined(); + expect(cb).toHaveBeenCalledWith(null, { ok: true }); + }); +}); + +describe('callApiWithoutToken', () => { + let instance; + + test('uses data values by default', () => { + const data = { + client_id: 'id', + client_secret: 'secret', + redirect_uri: 'redirectUri' + }; + const cb = jest.fn(); + + instance = slackWebApi(mockBot, {}); + instance.callAPIWithoutToken('some.method', data, cb); + + expect(mockRequest.post.mock.calls.length).toBe(1); + const firstArg = mockRequest.post.mock.calls[0][0]; + expect(firstArg.form.client_id).toBe('id'); + expect(firstArg.form.client_secret).toBe('secret'); + expect(firstArg.form.redirect_uri).toBe('redirectUri'); + }); + + test('uses config values if not set in data', () => { + const config = { + clientId: 'id', + clientSecret: 'secret', + redirectUri: 'redirectUri' + }; + const cb = jest.fn(); + + // this seems to be an API inconsistency: + // callAPIWithoutToken uses bot.config, but callAPI uses that passed config + mockBot.config = config; + + instance = slackWebApi(mockBot, {}); + instance.callAPIWithoutToken('some.method', {}, cb); + + expect(mockRequest.post.mock.calls.length).toBe(1); + const firstArg = mockRequest.post.mock.calls[0][0]; + expect(firstArg.form.client_id).toBe('id'); + expect(firstArg.form.client_secret).toBe('secret'); + expect(firstArg.form.redirect_uri).toBe('redirectUri'); + }); +}); + +describe('postForm', () => { + + ['callAPI', 'callAPIWithoutToken'].forEach((methodName) => { + let method; + let cb; + + beforeEach(() => { + const instance = slackWebApi(mockBot, {}); + method = instance[methodName]; + cb = jest.fn(); + }); + + test(`${methodName}: handles success`, () => { + method('some.action', 'data', cb); + expect(mockRequest.post).toHaveBeenCalledTimes(1); + const firstArg = mockRequest.post.mock.calls[0][0]; + + // do some thorough assertions here for a baseline + expect(firstArg.url).toMatch(/some.action$/); + expect(firstArg.form).toBe('data'); + expect(firstArg.formData).toBeUndefined(); + expect(firstArg.headers).toEqual({ 'User-Agent': 'jesting' }); + expect(cb).toHaveBeenCalledWith(null, { ok: true }); + }); + + test(`${methodName}: defaults callback`, () => { + method('some.action', 'data'); + expect(mockRequest.post).toHaveBeenCalledTimes(1); + }); + + test(`${methodName}: handles request lib error`, () => { + const error = new Error('WHOOPS!'); + mockRequest.post.mockImplementation((params, callback) => { + callback(error, null, null); + }); + + method('some.action', 'data', cb); + + expect(mockRequest.post).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledWith(error); + }); + + test(`${methodName}: handles 429 response code`, () => { + mockRequest.post.mockImplementation((params, callback) => { + callback(null, { statusCode: 429 }, null); + }); + + method('some.action', 'data', cb); + + expect(mockRequest.post).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledTimes(1); + const firstArg = cb.mock.calls[0][0]; + expect(firstArg.message).toBe('Rate limit exceeded'); + }); + + test(`${methodName}: handles other response codes`, () => { + mockRequest.post.mockImplementation((params, callback) => { + callback(null, { statusCode: 400 }, null); + }); + + method('some.action', 'data', cb); + + expect(mockRequest.post).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledTimes(1); + const firstArg = cb.mock.calls[0][0]; + expect(firstArg.message).toBe('Invalid response'); + }); + + test(`${methodName}: handles error parsing body`, () => { + mockRequest.post.mockImplementation((params, callback) => { + callback(null, { statusCode: 200 }, '{'); + }); + + method('some.action', 'data', cb); + + expect(mockRequest.post).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledTimes(1); + const firstArg = cb.mock.calls[0][0]; + expect(firstArg).toBeInstanceOf(Error); + }); + + test(`${methodName}: handles ok.false response`, () => { + mockRequest.post.mockImplementation((params, callback) => { + callback(null, { statusCode: 200 }, '{ "ok": false, "error": "not ok"}'); + }); + + method('some.action', 'data', cb); + + expect(mockRequest.post).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledWith('not ok', { ok: false, error: 'not ok' }); + }); + }); +}); + +describe('api methods', () => { + let instance; + let cb; + + beforeEach(() => { + instance = slackWebApi(mockBot, {}); + cb = jest.fn(); + jest.spyOn(instance, 'callAPI'); + instance.callAPI.mockImplementation(() => { }); + }); + + afterEach(() => { + if (jest.isMockFunction(JSON.stringify)) { + JSON.stringify.mockRestore(); + } + instance.callAPI.mockRestore(); + }); + + test('spot check api methods ', () => { + // testing for all methods seems wasteful, but let's confirm the methods got built correctly and test the following scenarios + + // two levels + expect(instance.auth).toBeDefined(); + expect(instance.auth.test).toBeDefined(); + + instance.auth.test('options', 'cb'); + const firstCallArgs = instance.callAPI.mock.calls[0]; + expect(firstCallArgs).toEqual(['auth.test', 'options', 'cb']); + + // three levels + expect(instance.users).toBeDefined(); + expect(instance.users.profile).toBeDefined(); + expect(instance.users.profile.get).toBeDefined(); + + instance.users.profile.get('options', 'cb'); + const secondCallArgs = instance.callAPI.mock.calls[1]; + expect(secondCallArgs).toEqual(['users.profile.get', 'options', 'cb']); + }); + + describe('special cases', () => { + + test('chat.postMessage stringifies attachments', () => { + instance.chat.postMessage({ attachments: [] }, cb); + expect(instance.callAPI).toHaveBeenCalledWith('chat.postMessage', { attachments: '[]' }, cb); + }); + + test('chat.postMessage handles attachments as Strings', () => { + jest.spyOn(JSON, 'stringify'); + instance.chat.postMessage({ attachments: 'string' }, cb); + expect(instance.callAPI).toHaveBeenCalledWith('chat.postMessage', { attachments: 'string' }, cb); + expect(JSON.stringify).not.toHaveBeenCalled(); + }); + + test('chat.postMessage handles attachments stringification errors', () => { + const error = new Error('WHOOPSIE'); + jest.spyOn(JSON, 'stringify').mockImplementation(() => { throw error; }); + instance.chat.postMessage({ attachments: [] }, cb); + expect(instance.callAPI).toHaveBeenCalledWith('chat.postMessage', {}, cb); + expect(JSON.stringify).toHaveBeenCalled(); + }); + + test('chat.update stringifies attachments', () => { + instance.chat.update({ attachments: [] }, cb); + expect(instance.callAPI).toHaveBeenCalledWith('chat.update', { attachments: '[]' }, cb); + }); + + test('chat.update handles attachments as Strings', () => { + jest.spyOn(JSON, 'stringify'); + instance.chat.update({ attachments: 'string' }, cb); + expect(instance.callAPI).toHaveBeenCalledWith('chat.update', { attachments: 'string' }, cb); + expect(JSON.stringify).not.toHaveBeenCalled(); + }); + + test('chat.postMessage handles attachments stringification errors', () => { + const error = new Error('WHOOPSIE'); + jest.spyOn(JSON, 'stringify').mockImplementation(() => { throw error; }); + instance.chat.update({ attachments: [] }, cb); + expect(instance.callAPI).toHaveBeenCalledWith('chat.update', {}, cb); + expect(JSON.stringify).toHaveBeenCalled(); + }); + + test('files.upload should not use multipart if file is false', () => { + const options = { file: false, token: 'abc123' }; + instance.files.upload(options, cb); + expect(instance.callAPI).toHaveBeenCalledWith('files.upload', options, cb, false); + }); + + test('files.upload should use multipart if file is true', () => { + const options = { file: true, token: 'abc123' }; + instance.files.upload(options, cb); + expect(instance.callAPI).toHaveBeenCalledWith('files.upload', options, cb, true); + }); + }); +}); diff --git a/changelog.md b/changelog.md index 1ffe1a53a..49df7a2e4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,43 @@ # Change Log +[View the official Botkit roadmap](https://github.com/howdyai/botkit/projects/7) for upcoming changes and features. + +[Want to contribute? Read our guide!](https://github.com/howdyai/botkit/blob/master/CONTRIBUTING.md) + + +## 0.5.6 + +Fix for Botkit Studio-powered bots: Facebook attachments can now be added without buttons + +Fix for Cisco Spark: Bot mentions will now reliably be pruned from message, regardless of what client originated the message + +Fix for Cisco Spark: startPrivateConversationWithPersonID has been fixed. + +## 0.5.5 + +*Introducing Botkit for SMS!* Botkit bots can now send and receive messages using Twilio's Programmable SMS API! +Huge thanks to @krismuniz who spearheaded this effort! [Read all about Twilio SMS here](docs/readme-twiliosms.md) + +*New unit tests* have been added, thanks to the ongoing efforts of @colestrode, @amplicity and others. +This release includes coverage of the Botkit core library and the Slack API library. +This is an [ongoing effort](https://github.com/howdyai/botkit/projects/3), and we encourage interested developers to get involved! + +Add missing error callback to catch Slack condition where incoming messages do not match a team in the database. +[PR #887](https://github.com/howdyai/botkit/pull/887) thanks to @alecl! + +Add support for Facebook attachment upload api [PR #899](https://github.com/howdyai/botkit/pull/899) thanks @ouadie-lahdioui! +Read docs about this feature [here](docs/readme-facebook.md#attachment-upload-api) + +Fixed issue with Slack message menus. [PR #769](https://github.com/howdyai/botkit/pull/769) + +Fixed confusing parameter in JSON storage system. `delete()` methods now expect object id as first parameter. [PR #854](https://github.com/howdyai/botkit/pull/854) thanks to @mehamasum! + +All example bot scripts have been moved into the [examples/](examples/) folder. Thanks @colestrode! + +Fixes an instance where Botkit was not automatically responding to incoming webhooks from Cisco with a 200 status. [PR #843](https://github.com/howdyai/botkit/pull/843) + +Updated dependencies to latest: twilio, ciscospark, https-proxy-agent, promise + ## 0.5.4 Fix for [#806](https://github.com/howdyai/botkit/issues/806) - new version of websocket didn't play nice with Slack's message servers @@ -206,7 +244,7 @@ Adds [ConsoleBot](lib/ConsoleBot.js) for creating bots that work on the command Adds a new [Middleware Readme](readme-middlewares.md) for documenting the existing middleware modules -Adds an example for using quick replies in the [Facebook Example Bot](facebook_bot.js) +Adds an example for using quick replies in the [Facebook Example Bot](examples/facebook_bot.js) Adds additional fields to Facebook messages to specify if they are `facebook_postback`s or normal messages. @@ -290,7 +328,7 @@ Make the oauth identity available to the user of the OAuth endpoint via `req.ide Fix issue where single team apps had a hard time receiving slash command events without funky workaround. (closes [Issue #108](https://github.com/howdyai/botkit/issues/108)) -Add [team_slashcommand.js](/examples/team_slashcommand.js) and [team_outgoingwebhook.js](/examples/team_outgoingwebhook.js) to the examples folder. +Add [team_slashcommand.js](/examples/slack/team_slashcommand.js) and [team_outgoingwebhook.js](/examples/slack/team_outgoingwebhook.js) to the examples folder. diff --git a/docs/examples.md b/docs/examples.md index 6a2910b61..c4bb07357 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -3,24 +3,26 @@ These examples are included in the Botkit [Github repo](https://github.com/howdyai/botkit). -[slack_bot.js](https://github.com/howdyai/botkit/blob/master/slack_bot.js) An example bot that can be connected to your team. Useful as a basis for creating your first bot! +[slack_bot.js](https://github.com/howdyai/botkit/blob/master/examples/slack_bot.js) An example bot that can be connected to your team. Useful as a basis for creating your first bot! -[spark_bot.js](https://github.com/howdyai/botkit/blob/master/spark_bot.js) An example bot that can be connected to Cisco Spark. Useful as a basis for creating your first bot! +[spark_bot.js](https://github.com/howdyai/botkit/blob/master/examples/spark_bot.js) An example bot that can be connected to Cisco Spark. Useful as a basis for creating your first bot! -[facebook_bot.js](https://github.com/howdyai/botkit/blob/master/facebook_bot.js) An example bot that can be connected to your Facebook page. Useful as a basis for creating your first bot! +[facebook_bot.js](https://github.com/howdyai/botkit/blob/master/examples/facebook_bot.js) An example bot that can be connected to your Facebook page. Useful as a basis for creating your first bot! -[twilio_ipm_bot.js](https://github.com/howdyai/botkit/blob/master/twilio_ipm_bot.js) An example bot that can be connected to your Twilio IP Messaging client. Useful as a basis for creating your first bot! +[twilio_sms_bot.js](https://github.com/howdyai/botkit/blob/master/examples/twilio_sms_bot.js) An example bot that can be connected to your Twilio SMS service. Useful as a basis for creating your first bot! -[botframework_bot.js](https://github.com/howdyai/botkit/blob/master/botframework_bot.js) An example bot that can be connected to the Microsoft Bot Framework network. Useful as a basis for creating your first bot! +[twilio_ipm_bot.js](https://github.com/howdyai/botkit/blob/master/examples/twilio_ipm_bot.js) An example bot that can be connected to your Twilio IP Messaging client. Useful as a basis for creating your first bot! -[examples/demo_bot.js](https://github.com/howdyai/botkit/blob/master/examples/demo_bot.js) another example bot that uses different ways to send and receive messages. +[botframework_bot.js](https://github.com/howdyai/botkit/blob/master/examples/botframework_bot.js) An example bot that can be connected to the Microsoft Bot Framework network. Useful as a basis for creating your first bot! -[examples/team_outgoingwebhook.js](https://github.com/howdyai/botkit/blob/master/examples/team_outgoingwebhook.js) an example of a Botkit app that receives and responds to outgoing webhooks from a single team. +[examples/demo_bot.js](https://github.com/howdyai/botkit/blob/master/examples/slack/demo_bot.js) another example bot that uses different ways to send and receive messages. -[examples/team_slashcommand.js](https://github.com/howdyai/botkit/blob/master/examples/team_slashcommand.js) an example of a Botkit app that receives slash commands from a single team. +[examples/team_outgoingwebhook.js](https://github.com/howdyai/botkit/blob/master/examples/slack/team_outgoingwebhook.js) an example of a Botkit app that receives and responds to outgoing webhooks from a single team. -[examples/slackbutton_bot.js](https://github.com/howdyai/botkit/blob/master/examples/slackbutton_bot.js) an example of using the Slack Button to offer a bot integration. +[examples/team_slashcommand.js](https://github.com/howdyai/botkit/blob/master/examples/slack/team_slashcommand.js) an example of a Botkit app that receives slash commands from a single team. -[examples/slackbutton_incomingwebhooks.js](https://github.com/howdyai/botkit/blob/master/examples/slackbutton_incomingwebhooks.js) an example of using the Slack Button to offer an incoming webhook integration. This example also includes a simple form which allows you to broadcast a message to any team who adds the integration. +[examples/slackbutton_bot.js](https://github.com/howdyai/botkit/blob/master/examples/slack/slackbutton_bot.js) an example of using the Slack Button to offer a bot integration. -[example/sentiment_analysis.js](https://github.com/howdyai/botkit/blob/master/examples/sentiment_analysis.js) a simple example of a chatbot using sentiment analysis. Keeps a running score of each user based on positive and negative keywords. Messages and thresholds can be configured. +[examples/slackbutton_incomingwebhooks.js](https://github.com/howdyai/botkit/blob/master/examples/slack/slackbutton_incomingwebhooks.js) an example of using the Slack Button to offer an incoming webhook integration. This example also includes a simple form which allows you to broadcast a message to any team who adds the integration. + +[example/sentiment_analysis.js](https://github.com/howdyai/botkit/blob/master/examples/slack/sentiment_analysis.js) a simple example of a chatbot using sentiment analysis. Keeps a running score of each user based on positive and negative keywords. Messages and thresholds can be configured. diff --git a/docs/logging.md b/docs/logging.md index 418874501..981cdf9cf 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -1,5 +1,3 @@ - - ### Writing your own logging module By default, your bot will log to the standard JavaScript `console` object @@ -47,6 +45,7 @@ Note: with Winston, we must use the syslog.levels over the default or else some * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/middleware.md b/docs/middleware.md index 8eafa1801..acc884954 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -1,7 +1,3 @@ - - - - ## Middleware The functionality of Botkit can be extended using middleware @@ -197,6 +193,7 @@ controller.middleware.capture.use(function(bot, message, convo, next) { * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/provisioning/cisco-spark.md b/docs/provisioning/cisco-spark.md index 9814a15c2..3ac4f398b 100644 --- a/docs/provisioning/cisco-spark.md +++ b/docs/provisioning/cisco-spark.md @@ -18,7 +18,9 @@ Take note of the bot username, you'll need it later. **Note about your icon**: Cisco requires you host an avatar for your bot before you can create it in their portal. This bot needs to be a 512x512px image icon hosted anywhere on the web. This can be changed later. -You can copy and paste this URL for a Botkit icon you can use right away: `https://raw.githubusercontent.com/howdyai/botkit-starter-ciscospark/master/public/default_icon.png` +You can copy and paste this URL for a Botkit icon you can use right away: + +https://raw.githubusercontent.com/howdyai/botkit-starter-ciscospark/master/public/default_icon.png ### 3. Copy your access token diff --git a/docs/provisioning/readme.md b/docs/provisioning/readme.md index cfe5b5d3f..cacc889f1 100644 --- a/docs/provisioning/readme.md +++ b/docs/provisioning/readme.md @@ -11,6 +11,9 @@ To help Botkit developers, we are pulling out detailed provisioning documents fo #### [Cisco Spark](cisco-spark.md) +#### [SMS from Twilio](twilio-sms.md) + + ## Documentation * [Get Started](../../readme.md) @@ -24,6 +27,7 @@ To help Botkit developers, we are pulling out detailed provisioning documents fo * [Slack](../readme-slack.md) * [Cisco Spark](../readme-ciscospark.md) * [Facebook Messenger](../readme-facebook.md) + * [Twilio SMS](https://../readme-twiliosms.md) * [Twilio IPM](https://../readme-twilioipm.md) * [Microsoft Bot Framework](../readme-botframework.md) * Contributing to Botkit diff --git a/docs/provisioning/slack-events-api.md b/docs/provisioning/slack-events-api.md index 2ea514d88..2a5c39088 100644 --- a/docs/provisioning/slack-events-api.md +++ b/docs/provisioning/slack-events-api.md @@ -1,6 +1,6 @@ # Configure Botkit and the Slack Events API -Building a bot with Botkit and the Slack Events API gives you access to all of the best tools and options available to createe a feature-rich bot for Slack. +Building a bot with Botkit and the Slack Events API gives you access to all of the best tools and options available to create a feature-rich bot for Slack. In order to get everything set up, you will need to configure a new Slack App inside the [Slack Developer Portal](http://api.slack.com/apps), and at the same time, configure a [Botkit-powered bot](http://botkit.ai). It only takes a few moments, but there are a bunch of steps, so follow these instructions carefully. diff --git a/docs/provisioning/twilio-sms.md b/docs/provisioning/twilio-sms.md new file mode 100644 index 000000000..20c5a67fe --- /dev/null +++ b/docs/provisioning/twilio-sms.md @@ -0,0 +1,40 @@ +# Configure Botkit and Twilio SMS + +Setting up a bot for Twilio SMS is one of the easiest experiences for bot developers! Follow these steps carefully to configure your bot. + +### 1. Install Botkit + +The easiest path to creating a new bot for Twilio SMS is through Botkit Studio. [Sign up for an account here](https://studio.botkit.ai/signup/). This method will provide a guided path to hosting, along with other useful tools for creating and managing your bot. + +For advanced users looking to run their own code, you will need to [install Botkit](../readme-twilio-sms.md#getting-started) and run it before your bot can be configured with Twilio SMS. + +### 2. Create a new bot in the Twilio Developer Console + +Login and click `Get Started` in [Twilio SMS Developer Console](https://www.twilio.com/console/sms/dashboard). You will be taken through the process of obtaining a number to use with your bot. + +At this point you can use the Twilio wizard to help you create an application, or build one directly by clicking `Messanging Services`. You can give it a friendly, and chose `Mixed` for use case. + +Check the box `Process Inbound Messages` and under request URL, type the name of your request url. + +By default in Botkit, this is: +https://*mybot.yoururl.com*/sms/receive + +### 3. Collect your tokens + +Next, visit [your console Dashboard](https://www.twilio.com/console) and copy your `Account SID` and `Auth Token`. You will use these in the next step along with your assignedmobile number to setup Botkit. + +### 4. Run your bot with variables set + + [Follow these instructions](../readme-TwilioSMS.md#getting-started) to run your bot locally, or by using a third-party service such as [Glitch](https://glitch.com) or [Heroku](https://heroku.com). + + You will need the following environment variables when running your bot: + +* TWILIO_ACCOUNT_SID= Your account's SID collected in step 3 above. +* TWILIO_AUTH_TOKEN= Your account's Auth Token collected in step 3 above. +* TWILIO_NUMBER= The number you were assigned in step 2 above. + +You should now be able to text message your number the words `Hello` and receive a friendly reply back! + +### Additional resources + +Read more about making bots for this platform in the [Twilio Developer Portal](https://www.twilio.com/console). diff --git a/docs/readme-botframework.md b/docs/readme-botframework.md index 0aeeb6405..66014eae4 100644 --- a/docs/readme-botframework.md +++ b/docs/readme-botframework.md @@ -34,7 +34,7 @@ Table of Contents 4) Run the example bot using the App ID & Password you were assigned. If you are _not_ running your bot at a public, SSL-enabled internet address, use the --lt option and update your bots endpoint in the developer portal to use the URL assigned to your bot. ``` -app_id= app_password= node botframework_bot.js [--lt [--ltsubdomain CUSTOM_SUBDOMAIN]] +app_id= app_password= node examples/botframework_bot.js [--lt [--ltsubdomain CUSTOM_SUBDOMAIN]] ``` 5) Your bot should be online! Within Skype, find the bot in your contacts list, and send it a message. @@ -221,6 +221,7 @@ You can easily turn on the typing indicator on platforms that support that behav * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/readme-ciscospark.md b/docs/readme-ciscospark.md index 4f1ca2a51..bc4a5d4f0 100644 --- a/docs/readme-ciscospark.md +++ b/docs/readme-ciscospark.md @@ -35,7 +35,7 @@ ngrok http 3000 4) Run your bot application using the access token you received, the base url of your bot application, and a secret which is used to validate the origin of incoming webhooks: ``` -access_token= public_address= secret= node spark_bot.js +access_token= public_address= secret= node examples/spark_bot.js ``` 5) Your bot should now come online and respond to requests! Find it in Cisco Spark by searching for it's name. @@ -268,6 +268,7 @@ controller.on('bot_space_join', function(bot, message) { * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/readme-facebook.md b/docs/readme-facebook.md index 3c35cd0ae..86587674c 100644 --- a/docs/readme-facebook.md +++ b/docs/readme-facebook.md @@ -20,6 +20,7 @@ Table of Contents * [Simulate typing](#simulate-typing) * [Silent and No Notifications](#silent-and-no-notifications) * [Messenger code API](#messenger-code-api) +* [Attachment upload API](#attachment-upload-api) * [Running Botkit with an Express server](#use-botkit-for-facebook-messenger-with-an-express-web-server) ## Getting Started @@ -38,7 +39,7 @@ Copy this token, you'll need it! 5) Run the example bot app, using the two tokens you just created. If you are _not_ running your bot at a public, SSL-enabled internet address, use the --lt option and note the URL it gives you. ``` -page_token= verify_token= node facebook_bot.js [--lt [--ltsubdomain CUSTOM_SUBDOMAIN]] +page_token= verify_token= node examples/facebook_bot.js [--lt [--ltsubdomain CUSTOM_SUBDOMAIN]] ``` 6) [Set up a webhook endpoint for your app](https://developers.facebook.com/docs/messenger-platform/guides/setup#webhook_setup) that uses your public URL. Use the verify token you defined in step 4! @@ -65,7 +66,7 @@ Facebook sends an X-HUB signature header with requests to your webhook. You can The Facebook App secret is available on the Overview page of your Facebook App's admin page. Click show to reveal it. ``` -app_secret=abcdefg12345 page_token=123455abcd verify_token=VerIfY-tOkEn node facebook_bot.js +app_secret=abcdefg12345 page_token=123455abcd verify_token=VerIfY-tOkEn node examples/facebook_bot.js ``` ## Facebook-specific Events @@ -506,6 +507,38 @@ controller.api.messenger_profile.get_target_audience(function (err, data) { ``` +## Attachment upload API + +Attachment upload API allows you to upload an attachment that you may later send out to many users, without having to repeatedly upload the same data each time it is sent : + + +```js +var attachment = { + "type":"image", + "payload":{ + "url":"https://pbs.twimg.com/profile_images/803642201653858305/IAW1DBPw_400x400.png", + "is_reusable": true + } + }; + + controller.api.attachment_upload.upload(attachment, function (err, attachmentId) { + if(err) { + // Error + } else { + var image = { + "attachment":{ + "type":"image", + "payload": { + "attachment_id": attachmentId + } + } + }; + bot.reply(message, image); + } + }); + +``` + ## Use BotKit for Facebook Messenger with an Express web server Instead of the web server generated with setupWebserver(), it is possible to use a different web server to receive webhooks, as well as serving web pages. @@ -525,6 +558,7 @@ Here is an example of [using an Express web server alongside BotKit for Facebook * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/readme-middlewares.md b/docs/readme-middlewares.md index 455af01c5..3731cb3b1 100644 --- a/docs/readme-middlewares.md +++ b/docs/readme-middlewares.md @@ -121,6 +121,7 @@ We would love to hear about it! [Contact the Howdy team](https://howdy.ai/) to b * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/readme-slack.md b/docs/readme-slack.md index cdb1643b2..bb5ba9f0f 100644 --- a/docs/readme-slack.md +++ b/docs/readme-slack.md @@ -22,7 +22,7 @@ Table of Contents --- ## Getting Started -1) Install Botkit on your hosting platform of choice [more info here](readme.md#installation). +1) Install Botkit on your hosting platform of choice [more info here](readme.md#installation). 2) First make a bot integration inside of your Slack channel. Go here: @@ -39,7 +39,7 @@ Copy the API token that Slack gives you. You'll need it. 4) Run the example bot app, using the token you just copied: ​ ``` -token=REPLACE_THIS_WITH_YOUR_TOKEN node slack_bot.js +token=REPLACE_THIS_WITH_YOUR_TOKEN node examples/slack_bot.js ``` ​ 5) Your bot should be online! Within Slack, send it a quick direct message to say hello. It should say hello back! @@ -913,9 +913,6 @@ The [Events API](https://api.slack.com/events-api) is a streamlined way to build During development, a tool such as [localtunnel.me](http://localtunnel.me) is useful for temporarily exposing a compatible webhook url to Slack while running Botkit privately. -Note: Currently [presence](https://api.slack.com/docs/presence) is not supported by Slack Events API, so bot users will appear offline, but will still function normally. -Developers may want to create an RTM connection in order to make the bot appear online - see note below. - ### To get started with the Events API: 1. Create a [Slack App](https://api.slack.com/apps/new) @@ -949,7 +946,7 @@ controller.setupWebserver(process.env.port, function(err, webserver) { res.send('Success!'); } }); - + // If not also opening an RTM connection controller.startTicking(); }); @@ -985,6 +982,7 @@ var controller = Botkit.slackbot({ * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/readme-studio.md b/docs/readme-studio.md index 40d2bc1d2..ddbe1c765 100644 --- a/docs/readme-studio.md +++ b/docs/readme-studio.md @@ -357,6 +357,7 @@ controller.studio.beforeThread('search', 'results', function(convo, next) { * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/readme-twilioipm.md b/docs/readme-twilioipm.md index 350e60c52..13c06fa58 100644 --- a/docs/readme-twilioipm.md +++ b/docs/readme-twilioipm.md @@ -45,7 +45,7 @@ Follow the instructions to get your IP Messaging Demo client up and running usin 5) Start up the sample Twilio IPM Bot. From inside your cloned Botkit repo, run: ``` -TWILIO_ACCOUNT_SID= TWILIO_AUTH_TOKEN= TWILIO_IPM_SERVICE_SID= TWILIO_API_KEY= TWILIO_API_SECRET= node twilio_ipm_bot.js +TWILIO_ACCOUNT_SID= TWILIO_AUTH_TOKEN= TWILIO_IPM_SERVICE_SID= TWILIO_API_KEY= TWILIO_API_SECRET= node examples/twilio_ipm_bot.js ``` 6) If you are _not_ running your bot at a public, SSL-enabled internet address, use [localtunnel.me](http://localtunnel.me) to make it available to Twilio. Note the URL it gives you. For example, it may say your url is `https://xyx.localtunnel.me/` In this case, the webhook URL for use in step 7 would be `https://xyx.localtunnel.me/twilio/receive` @@ -302,6 +302,7 @@ controller.on('onChannelAdded', function(bot, message){ * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/readme-twiliosms.md b/docs/readme-twiliosms.md new file mode 100644 index 000000000..41fe26e84 --- /dev/null +++ b/docs/readme-twiliosms.md @@ -0,0 +1,126 @@ +# Botkit and Twilio Programmable SMS + +Botkit is designed to ease the process of designing and running useful, creative bots that live inside [Slack](http://slack.com), [Facebook Messenger](http://facebook.com), [Twilio IP Messaging](https://www.twilio.com/docs/api/ip-messaging), and other messaging platforms like [Twilio's Programmable SMS](https://www.twilio.com/sms/). + +Botkit features a comprehensive set of tools to deal with [Twilio's Programmable SMS API](http://www.twilio.com/sms/), and allows developers to build interactive bots and applications that send and receive messages just like real humans. Twilio SMS bots receive and send messages through a regular phone number. + +This document covers the Twilio Programmable SMS API implementation details only. [Start here](readme.md) if you want to learn about how to develop with Botkit. + +# Getting Started + +1) Install Botkit [more info here](readme.md#installation) + +2) [Register a developer account with Twilio](https://github.com/howdyai/botkit/blob/master/docs/provisioning/twilio-sms.md). Once you've got it, head to the [Get Started with SMS](https://www.twilio.com/console/sms/getting-started/basics) page in your Twilio Console. + + After completing the tutorial above you should have all three values to get your bot running: A **Twilio Account SID**, a **Twilio Auth Token**, and a **Twilio Number**. + + **Twilio Account SID and Auth Token** + + These values are available on your [Twilio Account Settings](https://www.twilio.com/user/account/settings) page on the Twilio Console. Copy both the SID and token values (located under API Credentials) + + **Twilio Number** + + You should have purchased a Twilio Number. You will send/receive messages using this phone number. Example: `+19098765432` + +3) Configure your Twilio Number. Head to the [Phone Numbers](https://www.twilio.com/console/phone-numbers) in your Twilio Console and select the phone number you will use for your SMS bot. + + Under the *Messaging* section, select "Webhooks/TwiML" as your *Configure with* preference. Two more fields will pop up: ***A message comes in***, and ***Primary handler fails***. + + The first one is the type of handler you will use to respond to Twilio webhooks. Select "Webhook" and input the URI of your endpoint (e.g. `https://mysmsbot.localtunnel.me/sms/receive`) and select `HTTP POST` as your handling method. + + Twilio will send `POST` request to this address every time a user sends an SMS to your Twilio Number. + + > By default Botkit will serve content from `https://YOURSERVER/sms/receive`. If you are not running your bot on a public, SSL-enabled internet address, you can use a tool like [ngrok.io](http://ngrok.io/) or [localtunnel.me](localtunnel.me) to expose your local development enviroment to the outside world for the purposes of testing your SMS bot. + + The second preference ("Primary handler fails") is your backup plan. The URI Twilio should `POST` to in case your primary handler is unavailable. You can leave this field in blank for now but keep in mind this is useful for error handling (e.g. to notify users that your bot is unavailable). + +4) Run the example Twilio SMS bot included in Botkit's repository ([`twilio_sms_bot.js`](../examples/twilio_sms_bot.js)). Copy and paste the example bot's code into a new JavaScript file (e.g. `twilio_sms_bot.js`) in your current working directory and run the following command on your terminal: + + ```bash + $ TWILIO_ACCOUNT_SID= TWILIO_AUTH_TOKEN= TWILIO_NUMBER= node twilio_sms_bot.js + ``` + + > Note: Remember to run localtunnel or ngrok to expose your local development environment to the outside world. For example, in localtunnel run `lt --port 5000 --subdomain mysmsbot` (See note on step 4) + +6) Your bot should be online! Grab your phone and text `hi` to your Twilio Number and you will get a `Hello.` message back! + + Try the following messages: `Hi`, `Call me bob`, `what's my name?` + +## Usage + +*Note: This document assumes that you are familiarized with Botkit and Twilio's Programmable SMS API* + +To connect your bot to Twilio you must point a Messaging webhook to http://your_host/sms/receive, after doing so, every Twilio message will be sent to that address. + +Then you need to write your bot. First, create a TwilioSMSBot instance and pass an object with your configuration properties: + +* `account_sid`: found in your [Twilio Console Dashboard](https://www.twilio.com/console) +* `auth_token`: found in your [Twilio Console Dashboard](https://www.twilio.com/console) +* `twilio_number`: your app's phone number, found in your [Phone Numbers Dashboard](https://www.twilio.com/console/phone-numbers/dashboard) **The phone number format must be: `+15551235555`** + +```js +const TwilioSMSBot = require('botkit-sms') +const controller = TwilioSMSBot({ + account_sid: process.env.TWILIO_ACCOUNT_SID, + auth_token: process.env.TWILIO_AUTH_TOKEN, + twilio_number: process.env.TWILIO_NUMBER +}) +``` + +`spawn()` your bot instance: + +```js +let bot = controller.spawn({}) +``` + +Then you need to set up your Web server and create the webhook endpoints so your app can receive Twilio's messages: + +```js +controller.setupWebserver(process.env.PORT, function (err, webserver) { + controller.createWebhookEndpoints(controller.webserver, bot, function () { + console.log('TwilioSMSBot is online!') + }) +}) +``` + +And finally, you can setup listeners for specific messages, like you would in any other `botkit` bot: + +```js +controller.hears(['hi', 'hello'], 'message_received', (bot, message) => { + bot.startConversation(message, (err, convo) => { + convo.say('Hi, I am Oliver, an SMS bot! :D') + convo.ask('What is your name?', (res, convo) => { + convo.say(`Nice to meet you, ${res.text}!`) + convo.next() + }) + }) +}) + +controller.hears('.*', 'message_received', (bot, message) => { + bot.reply(message, 'huh?') +}) +``` + +See full example in the `examples` directory of this repo. + + +## Documentation + +* [Get Started](readme.md) +* [Botkit Studio API](readme-studio.md) +* [Function index](readme.md#developing-with-botkit) +* [Extending Botkit with Plugins and Middleware](middleware.md) + * [List of current plugins](readme-middlewares.md) +* [Storing Information](storage.md) +* [Logging](logging.md) +* Platforms + * [Slack](readme-slack.md) + * [Cisco Spark](readme-ciscospark.md) + * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) + * [Twilio IPM](readme-twilioipm.md) + * [Microsoft Bot Framework](readme-botframework.md) +* Contributing to Botkit + * [Contributing to Botkit Core](../CONTRIBUTING.md) + * [Building Middleware/plugins](howto/build_middleware.md) + * [Building platform connectors](howto/build_connector.md) diff --git a/docs/readme.md b/docs/readme.md index c8d18371f..721b3f859 100755 --- a/docs/readme.md +++ b/docs/readme.md @@ -18,10 +18,11 @@ it is ready to be connected to a stream of incoming messages. Currently, Botkit * [Slack Slash Commands](http://api.slack.com/slash-commands) * [Cisco Spark Webhooks](https://developer.ciscospark.com/webhooks-explained.html) * [Facebook Messenger Webhooks](https://developers.facebook.com/docs/messenger-platform/implementation) -* [Twilio IP Messaging](https://www.twilio.com/user/account/ip-messaging/getting-started) +* [Twilio SMS](https://www.twilio.com/console/sms/dashboard) +* [Twilio IP Messaging](https://www.twilio.com/console/chat/dashboard) * [Microsoft Bot Framework](http://botframework.com/) -Read more about [connecting your bot to Slack](readme-slack.md#connecting-your-bot-to-slack), [connecting your bot to Cisco Spark](readme-slack.md#getting-started), [connecting your bot to Facebook](readme-facebook.md#getting-started), [connecting your bot to Twilio](readme-twilioipm.md#getting-started), +Read more about [connecting your bot to Slack](readme-slack.md#connecting-your-bot-to-slack), [connecting your bot to Cisco Spark](readme-ciscospark.md#getting-started), [connecting your bot to Facebook](readme-facebook.md#getting-started), [connecting your bot to Twilio](readme-twilioipm.md#getting-started), or [connecting your bot to Microsoft Bot Framework](readme-botframework.md#getting-started) ## Basic Usage @@ -941,6 +942,7 @@ Here is an example of [using an Express web server alongside Botkit](https://git * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/docs/storage.md b/docs/storage.md index b55b8c684..4323dfcc5 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -47,7 +47,6 @@ var controller = Botkit.slackbot({ ``` - ## Documentation * [Get Started](readme.md) @@ -61,6 +60,7 @@ var controller = Botkit.slackbot({ * [Slack](readme-slack.md) * [Cisco Spark](readme-ciscospark.md) * [Facebook Messenger](readme-facebook.md) + * [Twilio SMS](readme-twiliosms.md) * [Twilio IPM](readme-twilioipm.md) * [Microsoft Bot Framework](readme-botframework.md) * Contributing to Botkit diff --git a/botframework_bot.js b/examples/botframework_bot.js similarity index 99% rename from botframework_bot.js rename to examples/botframework_bot.js index 904091830..ed76d4dcf 100644 --- a/botframework_bot.js +++ b/examples/botframework_bot.js @@ -64,7 +64,7 @@ This bot demonstrates many of the core features of Botkit: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -var Botkit = require('./lib/Botkit.js'); +var Botkit = require('../lib/Botkit.js'); var os = require('os'); var commandLineArgs = require('command-line-args'); var localtunnel = require('localtunnel'); diff --git a/console_bot.js b/examples/console_bot.js similarity index 96% rename from console_bot.js rename to examples/console_bot.js index 885ad2c53..6f6b38e7a 100644 --- a/console_bot.js +++ b/examples/console_bot.js @@ -56,7 +56,7 @@ This bot demonstrates many of the core features of Botkit: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -var Botkit = require('./lib/Botkit.js'); +var Botkit = require('../lib/Botkit.js'); var os = require('os'); var controller = Botkit.consolebot({ @@ -189,14 +189,13 @@ controller.hears(['shutdown'], 'message_received', function(bot, message) { controller.hears(['uptime', 'identify yourself', 'who are you', 'what is your name'], - 'direct_message,direct_mention,mention', function(bot, message) { + 'message_received', function(bot, message) { var hostname = os.hostname(); var uptime = formatUptime(process.uptime()); bot.reply(message, - ':robot_face: I am a bot named <@' + bot.identity.name + - '>. I have been running for ' + uptime + ' on ' + hostname + '.'); + ':robot_face: I am ConsoleBot. I have been running for ' + uptime + ' on ' + hostname + '.'); }); diff --git a/facebook_bot.js b/examples/facebook_bot.js similarity index 95% rename from facebook_bot.js rename to examples/facebook_bot.js index c4efd4b18..c5ee94b1e 100755 --- a/facebook_bot.js +++ b/examples/facebook_bot.js @@ -81,7 +81,7 @@ if (!process.env.app_secret) { process.exit(1); } -var Botkit = require('./lib/Botkit.js'); +var Botkit = require('../lib/Botkit.js'); var os = require('os'); var commandLineArgs = require('command-line-args'); var localtunnel = require('localtunnel'); @@ -132,6 +132,32 @@ controller.setupWebserver(process.env.port || 3000, function(err, webserver) { }); +controller.hears(['attachment_upload'], 'message_received', function(bot, message) { + var attachment = { + "type":"image", + "payload":{ + "url":"https://pbs.twimg.com/profile_images/803642201653858305/IAW1DBPw_400x400.png", + "is_reusable": true + } + }; + + controller.api.attachment_upload.upload(attachment, function (err, attachmentId) { + if(err) { + // Error + } else { + var image = { + "attachment":{ + "type":"image", + "payload": { + "attachment_id": attachmentId + } + } + }; + bot.reply(message, image); + } + }); +}); + controller.api.messenger_profile.greeting('Hello! I\'m a Botkit bot!'); controller.api.messenger_profile.get_started('sample_get_started_payload'); controller.api.messenger_profile.menu([{ diff --git a/examples/convo_bot.js b/examples/slack/convo_bot.js similarity index 98% rename from examples/convo_bot.js rename to examples/slack/convo_bot.js index 68b13e224..cb10d1244 100644 --- a/examples/convo_bot.js +++ b/examples/slack/convo_bot.js @@ -52,7 +52,7 @@ This bot demonstrates a multi-stage conversation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); if (!process.env.token) { console.log('Error: Specify token in environment'); diff --git a/examples/demo_bot.js b/examples/slack/demo_bot.js similarity index 98% rename from examples/demo_bot.js rename to examples/slack/demo_bot.js index d5d18ba0a..e6a9a8315 100755 --- a/examples/demo_bot.js +++ b/examples/slack/demo_bot.js @@ -53,7 +53,7 @@ This bot demonstrates many of the core features of Botkit: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); if (!process.env.token) { diff --git a/examples/incoming_webhooks.js b/examples/slack/incoming_webhooks.js similarity index 100% rename from examples/incoming_webhooks.js rename to examples/slack/incoming_webhooks.js diff --git a/examples/middleware_example.js b/examples/slack/middleware_example.js similarity index 99% rename from examples/middleware_example.js rename to examples/slack/middleware_example.js index de4f8416e..2fdfdc62d 100644 --- a/examples/middleware_example.js +++ b/examples/slack/middleware_example.js @@ -69,7 +69,7 @@ if (!process.env.token) { process.exit(1); } -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); var os = require('os'); var controller = Botkit.slackbot({ diff --git a/examples/sentiment_analysis.js b/examples/slack/sentiment_analysis.js similarity index 100% rename from examples/sentiment_analysis.js rename to examples/slack/sentiment_analysis.js diff --git a/examples/slack_app.js b/examples/slack/slack_app.js similarity index 100% rename from examples/slack_app.js rename to examples/slack/slack_app.js diff --git a/examples/slackbutton_bot.js b/examples/slack/slackbutton_bot.js similarity index 99% rename from examples/slackbutton_bot.js rename to examples/slack/slackbutton_bot.js index 89c8ac92f..b86dff0fe 100755 --- a/examples/slackbutton_bot.js +++ b/examples/slack/slackbutton_bot.js @@ -25,7 +25,7 @@ This is a sample Slack Button application that adds a bot to one or many slack t ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* Uses the slack button feature to offer a real time bot to multiple teams */ -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); if (!process.env.clientId || !process.env.clientSecret || !process.env.port) { console.log('Error: Specify clientId clientSecret and port in environment'); diff --git a/examples/slackbutton_bot_interactivemsg.js b/examples/slack/slackbutton_bot_interactivemsg.js similarity index 99% rename from examples/slackbutton_bot_interactivemsg.js rename to examples/slack/slackbutton_bot_interactivemsg.js index 03f79f6fd..6dbc04404 100644 --- a/examples/slackbutton_bot_interactivemsg.js +++ b/examples/slack/slackbutton_bot_interactivemsg.js @@ -25,7 +25,7 @@ This is a sample Slack Button application that adds a bot to one or many slack t ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* Uses the slack button feature to offer a real time bot to multiple teams */ -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); if (!process.env.clientId || !process.env.clientSecret || !process.env.port) { console.log('Error: Specify clientId clientSecret and port in environment'); diff --git a/examples/slackbutton_incomingwebhooks.js b/examples/slack/slackbutton_incomingwebhooks.js similarity index 98% rename from examples/slackbutton_incomingwebhooks.js rename to examples/slack/slackbutton_incomingwebhooks.js index 76441caf6..047f8f3ea 100644 --- a/examples/slackbutton_incomingwebhooks.js +++ b/examples/slack/slackbutton_incomingwebhooks.js @@ -49,7 +49,7 @@ This bot demonstrates many of the core features of Botkit: -> http://howdy.ai/botkit ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); if (!process.env.clientId || !process.env.clientSecret || !process.env.port) { console.log('Error: Specify clientId clientSecret and port in environment'); diff --git a/examples/slackbutton_slashcommand.js b/examples/slack/slackbutton_slashcommand.js similarity index 98% rename from examples/slackbutton_slashcommand.js rename to examples/slack/slackbutton_slashcommand.js index 546f94868..ef0368a54 100755 --- a/examples/slackbutton_slashcommand.js +++ b/examples/slack/slackbutton_slashcommand.js @@ -39,7 +39,7 @@ This bot demonstrates many of the core features of Botkit: -> http://howdy.ai/botkit ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); if (!process.env.clientId || !process.env.clientSecret || !process.env.port) { console.log('Error: Specify clientId clientSecret and port in environment'); diff --git a/examples/team_outgoingwebhook.js b/examples/slack/team_outgoingwebhook.js similarity index 87% rename from examples/team_outgoingwebhook.js rename to examples/slack/team_outgoingwebhook.js index 8d8490daa..7e4813625 100755 --- a/examples/team_outgoingwebhook.js +++ b/examples/slack/team_outgoingwebhook.js @@ -1,4 +1,4 @@ -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); var controller = Botkit.slackbot({ debug: true diff --git a/examples/team_slashcommand.js b/examples/slack/team_slashcommand.js similarity index 93% rename from examples/team_slashcommand.js rename to examples/slack/team_slashcommand.js index f4ae87e2f..286ce7040 100755 --- a/examples/team_slashcommand.js +++ b/examples/slack/team_slashcommand.js @@ -1,4 +1,4 @@ -var Botkit = require('../lib/Botkit.js'); +var Botkit = require('../../lib/Botkit.js'); var controller = Botkit.slackbot({ debug: true diff --git a/slack_bot.js b/examples/slack_bot.js similarity index 99% rename from slack_bot.js rename to examples/slack_bot.js index 941ab4a1f..3bcaab063 100644 --- a/slack_bot.js +++ b/examples/slack_bot.js @@ -69,7 +69,7 @@ if (!process.env.token) { process.exit(1); } -var Botkit = require('./lib/Botkit.js'); +var Botkit = require('../lib/Botkit.js'); var os = require('os'); var controller = Botkit.slackbot({ diff --git a/spark_bot.js b/examples/spark_bot.js similarity index 98% rename from spark_bot.js rename to examples/spark_bot.js index a44b27199..e1d41e4c6 100644 --- a/spark_bot.js +++ b/examples/spark_bot.js @@ -27,7 +27,7 @@ This bot demonstrates many of the core features of Botkit: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -var Botkit = require('./lib/Botkit.js'); +var Botkit = require('../lib/Botkit.js'); var controller = Botkit.sparkbot({ debug: false, diff --git a/twilio_ipm_bot.js b/examples/twilio_ipm_bot.js similarity index 98% rename from twilio_ipm_bot.js rename to examples/twilio_ipm_bot.js index b8636f18a..2dcd4d935 100644 --- a/twilio_ipm_bot.js +++ b/examples/twilio_ipm_bot.js @@ -1,4 +1,4 @@ -var Botkit = require('./lib/Botkit.js'); +var Botkit = require('../lib/Botkit.js'); var os = require('os'); var controller = Botkit.twilioipmbot({ debug: false, diff --git a/examples/twilio_sms_bot.js b/examples/twilio_sms_bot.js new file mode 100644 index 000000000..ade546dd4 --- /dev/null +++ b/examples/twilio_sms_bot.js @@ -0,0 +1,108 @@ +var Botkit = require('./lib/Botkit.js'); +var os = require('os'); + +var controller = Botkit.twiliosmsbot({ + account_sid: process.env.TWILIO_ACCOUNT_SID, + auth_token: process.env.TWILIO_AUTH_TOKEN, + twilio_number: process.env.TWILIO_NUMBER, + debug: true +}); + +var bot = controller.spawn({}); + +controller.setupWebserver(5000, function(err, server) { + server.get('/', function(req, res) { + res.send(':)'); + }); + + controller.createWebhookEndpoints(server, bot); +}) + +controller.hears(['hello', 'hi'], 'message_received', function(bot, message) { + controller.storage.users.get(message.user, function(err, user) { + if (user && user.name) { + bot.reply(message, 'Hello ' + user.name + '!!'); + } else { + bot.reply(message, 'Hello.'); + } + }); +}); + +controller.hears(['call me (.*)'], 'message_received', function(bot, message) { + var matches = message.text.match(/call me (.*)/i); + var name = matches[1]; + controller.storage.users.get(message.user, function(err, user) { + if (!user) { + user = { + id: message.user, + }; + } + user.name = name; + controller.storage.users.save(user, function(err, id) { + bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); + }); + }); +}); + +controller.hears(['what is my name', 'who am i'], 'message_received', function(bot, message) { + controller.storage.users.get(message.user, function(err, user) { + if (user && user.name) { + bot.reply(message, 'Your name is ' + user.name); + } else { + bot.reply(message, 'I don\'t know yet!'); + } + }); +}); + + +controller.hears(['shutdown'], 'message_received', function(bot, message) { + bot.startConversation(message, function(err, convo) { + convo.ask('Are you sure you want me to shutdown?', [{ + pattern: bot.utterances.yes, + callback: function(response, convo) { + convo.say('Bye!'); + convo.next(); + setTimeout(function() { + process.exit(); + }, 3000); + } + }, + { + pattern: bot.utterances.no, + default: true, + callback: function(response, convo) { + convo.say('*Phew!*'); + convo.next(); + } + } + ]); + }); +}); + + +controller.hears(['uptime', 'identify yourself', 'who are you', 'what is your name'], 'message_received', function(bot, message) { + + var hostname = os.hostname(); + var uptime = formatUptime(process.uptime()); + + bot.reply(message, 'I am a bot! I have been running for ' + uptime + ' on ' + hostname + '.'); + +}); + +function formatUptime(uptime) { + var unit = 'second'; + if (uptime > 60) { + uptime = uptime / 60; + unit = 'minute'; + } + if (uptime > 60) { + uptime = uptime / 60; + unit = 'hour'; + } + if (uptime != 1) { + unit = unit + 's'; + } + + uptime = uptime + ' ' + unit; + return uptime; +} diff --git a/lib/Botkit.js b/lib/Botkit.js index 2036e826a..f47b8ce48 100755 --- a/lib/Botkit.js +++ b/lib/Botkit.js @@ -2,6 +2,7 @@ var CoreBot = require(__dirname + '/CoreBot.js'); var Slackbot = require(__dirname + '/SlackBot.js'); var Facebookbot = require(__dirname + '/Facebook.js'); var TwilioIPMbot = require(__dirname + '/TwilioIPMBot.js'); +var TwilioSMSbot = require(__dirname + '/TwilioSMSBot.js'); var BotFrameworkBot = require(__dirname + '/BotFramework.js'); var SparkBot = require(__dirname + '/CiscoSparkbot.js'); var ConsoleBot = require(__dirname + '/ConsoleBot.js'); @@ -13,6 +14,7 @@ module.exports = { sparkbot: SparkBot, facebookbot: Facebookbot, twilioipmbot: TwilioIPMbot, + twiliosmsbot: TwilioSMSbot, botframeworkbot: BotFrameworkBot, consolebot: ConsoleBot, gactionsbot: GActionsBot diff --git a/lib/CiscoSparkbot.js b/lib/CiscoSparkbot.js index ca2a72671..5e64d76fb 100644 --- a/lib/CiscoSparkbot.js +++ b/lib/CiscoSparkbot.js @@ -76,7 +76,7 @@ function Sparkbot(configuration) { '** Serving webhook endpoints for Cisco Spark Platform at: ' + 'http://' + controller.config.hostname + ':' + controller.config.port + '/ciscospark/receive'); webserver.post('/ciscospark/receive', function(req, res) { - + res.sendStatus(200); controller.handleWebhookPayload(req, res, bot); }); @@ -210,9 +210,20 @@ function Sparkbot(configuration) { if (message.original_message.html) { // strip the mention & HTML from the message - var pattern = new RegExp('^\\.*?\<\/spark\-mention\>'); + var pattern = new RegExp('^(\)?\.*?\<\/spark\-mention\>', 'im'); + if (!message.original_message.html.match(pattern)) { + var encoded_id = controller.identity.id; + var decoded = new Buffer(encoded_id, 'base64').toString('ascii'); + + // this should look like ciscospark://us/PEOPLE/ + var matches; + if (matches = decoded.match(/ciscospark\:\/\/.*\/(.*)/im)) { + pattern = new RegExp('^(\)?\.*?\<\/spark\-mention\>', 'im'); + } + } var action = message.original_message.html.replace(pattern, ''); + // strip the remaining HTML tags action = action.replace(/\<.*?\>/img, ''); @@ -331,8 +342,12 @@ function Sparkbot(configuration) { convo.on('sent', function(sent_message) { // update this convo so that future messages will match // since the source message did not have this info in it. - convo.source_message.user = message.user; + convo.source_message.user = message_options.toPersonEmail; convo.source_message.channel = sent_message.roomId; + + convo.context.user = convo.source_message.user; + convo.context.channel = convo.source_message.channel; + }); cb(null, convo); }); @@ -344,18 +359,10 @@ function Sparkbot(configuration) { */ bot.startPrivateConversationWithPersonId = function(personId, cb) { - var message_options = {}; - - message_options.toPersonId = personId; - - botkit.startTask(bot, message_options, function(task, convo) { - convo.on('sent', function(sent_message) { - // update this convo so that future messages will match - // since the source message did not have this info in it. - convo.source_message.user = message.user; - convo.source_message.channel = sent_message.roomId; - }); - cb(null, convo); + controller.api.people.get(personId).then(function(identity) { + bot.startPrivateConversation({user: identity.emails[0]}, cb); + }).catch(function(err) { + cb(err); }); }; diff --git a/lib/ConsoleBot.js b/lib/ConsoleBot.js index ff581489d..98cee7aa9 100644 --- a/lib/ConsoleBot.js +++ b/lib/ConsoleBot.js @@ -23,12 +23,19 @@ function TextBot(configuration) { utterances: botkit.utterances, }; + bot.createConversation = function(message, cb) { + botkit.createConversation(this, message, cb); + }; + bot.startConversation = function(message, cb) { botkit.startConversation(this, message, cb); }; bot.send = function(message, cb) { console.log('BOT:', message.text); + if (cb) { + cb(); + } }; bot.reply = function(src, resp, cb) { diff --git a/lib/CoreBot.js b/lib/CoreBot.js index b6068974f..15eba0dad 100755 --- a/lib/CoreBot.js +++ b/lib/CoreBot.js @@ -1008,7 +1008,7 @@ function Botkit(configuration) { } if (typeof(events) == 'string') { - events = events.split(/\,/g); + events = events.split(/\,/g).map(function(str) { return str.trim(); }); } for (var e = 0; e < events.length; e++) { diff --git a/lib/Facebook.js b/lib/Facebook.js index 78cf8f208..db977a773 100644 --- a/lib/Facebook.js +++ b/lib/Facebook.js @@ -610,9 +610,50 @@ function Facebookbot(configuration) { } }; + var attachment_upload_api = { + upload: function(attachment, cb) { + var message = { + message: { + attachment: attachment + } + }; + + request.post('https://' + api_host + '/v2.6/me/message_attachments?access_token=' + configuration.access_token, + { form: message }, + function(err, res, body) { + if (err) { + facebook_botkit.log('Could not upload attachment'); + cb(err); + } else { + + var results = null; + try { + results = JSON.parse(body); + } catch (err) { + facebook_botkit.log('ERROR in attachment upload API call: Could not parse JSON', err, body); + cb(err); + } + + if (results) { + if (results.error) { + facebook_botkit.log('ERROR in attachment upload API call: ', results.error.message); + cb(results.error); + } else { + var attachment_id = results.attachment_id; + facebook_botkit.log('Successfully got attachment id ', attachment_id); + cb(null, attachment_id); + } + } + } + }); + } + + }; + facebook_botkit.api = { 'messenger_profile': messenger_profile_api, - 'thread_settings': messenger_profile_api + 'thread_settings': messenger_profile_api, + 'attachment_upload': attachment_upload_api }; // Verifies the SHA1 signature of the raw request payload before bodyParser parses it diff --git a/lib/SlackBot.js b/lib/SlackBot.js index 8b3aebaa5..b6d11b85b 100755 --- a/lib/SlackBot.js +++ b/lib/SlackBot.js @@ -168,6 +168,8 @@ function Slackbot(configuration) { cb(null, found_team); } }); + } else { + cb(new Error(`could not find team ${team_id}`)); } } }); @@ -262,6 +264,11 @@ function Slackbot(configuration) { // this allows button clicks to respond to asks message.text = message.actions[0].value; + // handle message menus + if (message.actions[0].selected_options) { + message.text = message.actions[0].selected_options[0].value; + } + message.type = 'interactive_message_callback'; slack_botkit.trigger('interactive_message_callback', [bot, message]); diff --git a/lib/Slack_web_api.js b/lib/Slack_web_api.js index 86b6911f7..030197237 100755 --- a/lib/Slack_web_api.js +++ b/lib/Slack_web_api.js @@ -3,7 +3,7 @@ var request = require('request'); /** * Does nothing. Takes no params, returns nothing. It's a no-op! */ -function noop() {} +function noop() { } /** * Returns an interface to the Slack API in the context of the given bot @@ -109,6 +109,7 @@ module.exports = function(bot, config) { 'team.profile.get', 'users.getPresence', 'users.info', + 'users.identity', 'users.list', 'users.setActive', 'users.setPresence', @@ -187,7 +188,7 @@ module.exports = function(bot, config) { }; function sanitizeOptions(options) { - if (options.attachments && typeof(options.attachments) != 'string') { + if (options.attachments && typeof (options.attachments) != 'string') { try { options.attachments = JSON.stringify(options.attachments); } catch (err) { @@ -227,7 +228,11 @@ module.exports = function(bot, config) { request.post(params, function(error, response, body) { bot.debug('Got response', error, body); - if (!error && response.statusCode == 200) { + if (error) { + return cb(error); + } + + if (response.statusCode == 200) { var json; try { json = JSON.parse(body); @@ -236,8 +241,11 @@ module.exports = function(bot, config) { } return cb((json.ok ? null : json.error), json); + } else if (response.statusCode == 429) { + return cb(new Error('Rate limit exceeded')); + } else { + return cb(new Error('Invalid response')); } - return cb(error || new Error('Invalid response')); }); } }; diff --git a/lib/Slackbot_worker.js b/lib/Slackbot_worker.js index 86bd5d879..25f1512ab 100755 --- a/lib/Slackbot_worker.js +++ b/lib/Slackbot_worker.js @@ -156,7 +156,7 @@ module.exports = function(botkit, config) { } bot.rtm = new Ws(res.url, null, { - agent: agent + agent: agent }); bot.msgcount = 1; @@ -735,12 +735,12 @@ module.exports = function(botkit, config) { if (message.text) { message.text = message.text.trim(); - } - var direct_mention = new RegExp('^\<\@' + bot.identity.id + '\>', 'i'); + var direct_mention = new RegExp('^\<\@' + bot.identity.id + '\>', 'i'); - message.text = message.text.replace(direct_mention, '') - .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); + message.text = message.text.replace(direct_mention, '') + .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); + } cb(botkit.tasks[t].convos[c]); return; diff --git a/lib/Studio.js b/lib/Studio.js index cf71168f5..69213dc98 100644 --- a/lib/Studio.js +++ b/lib/Studio.js @@ -402,6 +402,15 @@ module.exports = function(controller) { // blank text, not allowed with attachment message.text = null; + // remove blank button array if specified + if (message.attachment.payload.elements) { + for (var e = 0; e < message.attachment.payload.elements.length; e++) { + if (!message.attachment.payload.elements[e].buttons || !message.attachment.payload.elements[e].buttons.length) { + delete(message.attachment.payload.elements[e].buttons); + } + } + } + } // handle Facebook quick replies @@ -611,6 +620,11 @@ module.exports = function(controller) { x = md5(bot.config.TWILIO_IPM_SERVICE_SID); break; + case 'twiliosms': + x = md5(bot.botkit.config.account_sid); + break; + + case 'ciscospark': x = md5(bot.botkit.config.ciscospark_access_token); break; diff --git a/lib/TwilioSMSBot.js b/lib/TwilioSMSBot.js new file mode 100644 index 000000000..b2f5ea7a5 --- /dev/null +++ b/lib/TwilioSMSBot.js @@ -0,0 +1,209 @@ +var path = require('path'); +var os = require('os'); +var Botkit = require('./CoreBot'); +var express = require('express'); +var bodyParser = require('body-parser'); +var twilio = require('twilio'); + +function TwilioSMS(configuration) { + + var twilioSMS = Botkit(configuration || {}); + + if (!configuration) { + throw Error('Specify your \'account_sid\', \'auth_token\', and ' + + '\'twilio_number\' as properties of the \'configuration\' object'); + } + + if (configuration && !configuration.account_sid) { + throw Error('Specify an \'account_sid\' in your configuration object'); + } + + if (configuration && !configuration.auth_token) { + throw Error('Specify an \'auth_token\''); + } + + if (configuration && !configuration.twilio_number) { + throw Error('Specify a \'twilio_number\''); + } + + twilioSMS.defineBot(function(botkit, config) { + + var bot = { + type: 'twiliosms', + botkit: botkit, + config: config || {}, + utterances: botkit.utterances + }; + + bot.startConversation = function(message, cb) { + botkit.startConversation(bot, message, cb); + }; + + bot.createConversation = function(message, cb) { + botkit.createConversation(bot, message, cb); + }; + + + bot.send = function(message, cb) { + + var client = new twilio.RestClient( + configuration.account_sid, + configuration.auth_token + ); + + var sms = { + body: message.text, + from: configuration.twilio_number, + to: message.channel + }; + + client.messages.create(sms, function(err, message) { + + if (err) { + cb(err); + } else { + cb(null, message); + } + + }); + + }; + + bot.reply = function(src, resp, cb) { + var msg = {}; + + if (typeof resp === 'string') { + msg.text = resp; + } else { + msg = resp; + } + + msg.channel = src.channel; + + if (typeof cb === 'function') { + bot.say(msg, cb); + } else { + bot.say(msg, function() {}); + } + + }; + + bot.findConversation = function(message, cb) { + + botkit.debug('CUSTOM FIND CONVO', message.user, message.channel); + + for (var t = 0; t < botkit.tasks.length; t++) { + for (var c = 0; c < botkit.tasks[t].convos.length; c++) { + + var convo = botkit.tasks[t].convos[c]; + var matchesConvo = ( + convo.source_message.channel === message.channel || + convo.source_message.user === message.user + ); + + if (convo.isActive() && matchesConvo) { + botkit.debug('FOUND EXISTING CONVO!'); + cb(botkit.tasks[t].convos[c]); + return; + } + + } + } + + cb(); + }; + + return bot; + }); + + twilioSMS.handleWebhookPayload = function(req, res, bot) { + + twilioSMS.log('=> Got a message hook'); + + var message = { + text: req.body.Body, + from: req.body.From, + to: req.body.To, + user: req.body.From, + channel: req.body.From, + timestamp: Date.now(), + sid: req.body.MessageSid, + NumMedia: req.body.NumMedia, + MediaUrl0: req.body.MediaUrl0, + MediaUrl1: req.body.MediaUrl1, + MediaUrl2: req.body.MediaUrl2, + MediaUrl3: req.body.MediaUrl3, + MediaUrl4: req.body.MediaUrl4, + MediaUrl5: req.body.MediaUrl5, + MediaUrl6: req.body.MediaUrl6, + MediaUrl7: req.body.MediaUrl7, + MediaUrl9: req.body.MediaUrl9, + MediaUrl10: req.body.MediaUrl10, + }; + + twilioSMS.receiveMessage(bot, message); + + }; + + // set up a web route for receiving outgoing webhooks + twilioSMS.createWebhookEndpoints = function(webserver, bot, cb) { + + twilioSMS.log('** Serving webhook endpoints for Twilio Programmable SMS' + + ' at: ' + os.hostname() + ':' + twilioSMS.config.port + '/sms/receive'); + + var endpoint = twilioSMS.config.endpoint || '/sms/receive'; + + webserver.post(endpoint, function(req, res) { + twilioSMS.handleWebhookPayload(req, res, bot); + + // Send empty TwiML response to Twilio + var twiml = new twilio.TwimlResponse(); + res.type('text/xml'); + res.send(twiml.toString()); + }); + + if (cb) cb(); + + return twilioSMS; + }; + + twilioSMS.setupWebserver = function(port, cb) { + + if (!port) { + throw new Error('Cannot start webserver without a \'port\' parameter'); + } + + if (isNaN(port)) { + throw new TypeError('Specified \'port\' parameter is not a valid number'); + } + + var static_dir = path.join(__dirname, '/public'); + + var config = twilioSMS.config; + + if (config && config.webserver && config.webserver.static_dir) { + static_dir = twilioSMS.config.webserver.static_dir; + } + + twilioSMS.config.port = port; + + twilioSMS.webserver = express(); + twilioSMS.webserver.use(bodyParser.json()); + twilioSMS.webserver.use(bodyParser.urlencoded({extended: true})); + twilioSMS.webserver.use(express.static(static_dir)); + + twilioSMS.webserver.listen(twilioSMS.config.port, function() { + + twilioSMS.log('*> Listening on port ' + twilioSMS.config.port); + twilioSMS.startTicking(); + if (cb) cb(null, twilioSMS.webserver); + + }); + + return twilioSMS; + }; + + return twilioSMS; +} + +module.exports = TwilioSMS; diff --git a/lib/storage/simple_storage.js b/lib/storage/simple_storage.js index 795298443..fa6c6e01e 100755 --- a/lib/storage/simple_storage.js +++ b/lib/storage/simple_storage.js @@ -57,7 +57,7 @@ module.exports = function(config) { teams_db.save(team_data.id, team_data, cb); }, delete: function(team_id, cb) { - teams_db.delete(team_id.id, cb); + teams_db.delete(team_id, cb); }, all: function(cb) { teams_db.all(objectsToList(cb)); @@ -71,7 +71,7 @@ module.exports = function(config) { users_db.save(user.id, user, cb); }, delete: function(user_id, cb) { - users_db.delete(user_id.id, cb); + users_db.delete(user_id, cb); }, all: function(cb) { users_db.all(objectsToList(cb)); @@ -85,7 +85,7 @@ module.exports = function(config) { channels_db.save(channel.id, channel, cb); }, delete: function(channel_id, cb) { - channels_db.delete(channel_id.id, cb); + channels_db.delete(channel_id, cb); }, all: function(cb) { channels_db.all(objectsToList(cb)); diff --git a/package.json b/package.json index 1402be46b..3c7378d2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "botkit", - "version": "0.5.4", + "version": "0.5.6", "description": "Building blocks for Building Bots", "main": "lib/Botkit.js", "dependencies": { @@ -10,23 +10,24 @@ "body-parser": "^1.17.1", "botbuilder": "^3.7.0", "botkit-studio-sdk": "^1.0.2", - "ciscospark": "^0.7.69", + "ciscospark": "^1.1.6", "clone": "2.1.1", "command-line-args": "^4.0.2", "crypto": "0.0.3", "express": "^4.15.2", - "https-proxy-agent": "^1.0.0", + "https-proxy-agent": "^2.0.0", "jfs": "^0.2.6", "localtunnel": "^1.8.2", "md5": "^2.2.1", "mustache": "^2.3.0", - "promise": "^7.1.1", + "promise": "^8.0.0", "request": "^2.81.0", "twilio": "^2.11.1", "ware": "^1.3.0", "ws": "^2.2.2" }, "devDependencies": { + "jest-cli": "^20.0.1", "jscs": "^3.0.7", "mocha": "^3.2.0", "should": "^11.2.1", @@ -35,8 +36,10 @@ "winston": "^2.3.1" }, "scripts": { - "pretest": "jscs ./lib/", - "test": "mocha tests/*.js" + "pretest": "jscs ./lib/ ./__test__ --fix", + "test": "jest --coverage", + "test-legacy": "mocha ./tests/*.js", + "test-watch": "jest --watch" }, "repository": { "type": "git", @@ -56,5 +59,8 @@ "microsoft bot framework" ], "author": "ben@howdy.ai", - "license": "MIT" + "license": "MIT", + "jest": { + "testEnvironment": "node" + } } diff --git a/readme.md b/readme.md index c7462a0e7..0ff5027cf 100644 --- a/readme.md +++ b/readme.md @@ -24,6 +24,7 @@ Botkit features a comprehensive set of tools to deal with popular messaging plat * [Slack](docs/readme-slack.md) * [Cisco Spark](docs/readme-ciscospark.md) * [Facebook Messenger and Facebook @Workplace](docs/readme-facebook.md) +* [Twilio SMS Messaging](docs/readme-twiliosms.md) * [Twilio IP Messaging](docs/readme-twilioipm.md) * [Microsoft Bot Framework](docs/readme-botframework.md) * Yours? [info@howdy.ai](mailto:info@howdy.ai) @@ -140,6 +141,27 @@ Use the `--production` flag to skip the installation of devDependencies from Bot npm install --production ``` +## Running Tests + +To run tests, use the npm `test` command. Note: you will need dev dependencies installed using `npm install`. + +```bash +npm test +``` + +To run tests in watch mode run: + +```bash +npm run test-watch +``` + +Tests are run with [Jest](https://facebook.github.io/jest/docs/getting-started.html). You can pass Jest command line options after a `--`. +For example to have Jest bail on the first error you can run + +```bash +npm test -- --bail +``` + ## Documentation * [Get Started](docs/readme.md) @@ -153,6 +175,7 @@ npm install --production * [Slack](docs/readme-slack.md) * [Cisco Spark](docs/readme-ciscospark.md) * [Facebook Messenger](docs/readme-facebook.md) + * [Twilio SMS](docs/readme-twiliosms.md) * [Twilio IPM](docs/readme-twilioipm.md) * [Microsoft Bot Framework](docs/readme-botframework.md) * Contributing to Botkit