├── .gitignore ├── .travis.yml ├── index.js ├── .eslintrc.js ├── .editorconfig ├── test ├── index.js └── slack │ ├── request.js │ ├── index.js │ └── ws.js ├── src └── slack │ ├── index.js │ ├── request.js │ ├── conversation.js │ └── ws.js ├── package.json ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | test.js 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "6.1" 5 | - "5.11" 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Slack = require('./src/slack/index.js'); 4 | 5 | module.exports = { 6 | Slack: Slack 7 | }; 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "installedESLint": true, 4 | "plugins": [ 5 | "react", 6 | "jsx-a11y", 7 | "import" 8 | ] 9 | }; -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.json] 11 | indent_size = 2 -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const sinonChai = require('sinon-chai'); 6 | const chaiAsPromised = require('chai-as-promised'); 7 | const should = chai.should(); 8 | 9 | chai.use(sinonChai); 10 | chai.use(chaiAsPromised); 11 | 12 | const mainClass = require('../index'); 13 | const slackCore = require('../src/slack/index.js'); 14 | 15 | describe('SourceBot', () => { 16 | it('should offer slack class', () => { 17 | mainClass.Slack.should.exist; 18 | mainClass.Slack.should.deep.equal(slackCore); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/slack/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const sinonChai = require('sinon-chai'); 6 | const chaiAsPromised = require('chai-as-promised'); 7 | const should = chai.should(); 8 | chai.use(sinonChai); 9 | chai.use(chaiAsPromised); 10 | 11 | const Request = require('../../src/slack/request'); 12 | 13 | describe('Request', () => { 14 | beforeEach(() => { 15 | this.request = new Request('EXAMPLE_TOKEN'); 16 | this.opts = { 17 | uri: 'http://jsonplaceholder.typicode.com/posts' 18 | }; 19 | }); 20 | 21 | it('should accept api token', () => { 22 | this.request.token.should.equal('EXAMPLE_TOKEN'); 23 | }); 24 | 25 | it('should have a private request function', () => { 26 | this.request.request_.should.exist; 27 | }); 28 | 29 | it('should throw error on missing uri', () => { 30 | this.request.request_().should.be.rejected; 31 | }); 32 | 33 | it('should accept valid uri', () => { 34 | const instance = () => { 35 | return this.request.request_(this.opts); 36 | } 37 | instance().should.be.resolved; 38 | }); 39 | 40 | it('should merge optional parameters with existing ones', () => { 41 | this.request.request_(this.opts).uri.href.should.equal(this.opts.uri) 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/slack/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const sinonChai = require('sinon-chai'); 6 | const chaiAsPromised = require('chai-as-promised'); 7 | const should = chai.should(); 8 | chai.use(sinonChai); 9 | chai.use(chaiAsPromised); 10 | 11 | const SlackCore = require('../../src/slack/index'); 12 | const Request = require('../../src/slack/request'); 13 | 14 | describe('Core', () => { 15 | beforeEach(() => { 16 | this.request = new Request('EXAMPLE_TOKEN'); 17 | }); 18 | 19 | it('should enable debug mode', () => { 20 | let core = new SlackCore({debug: true}); 21 | 22 | process.env.DEBUG.should.be.an('string'); 23 | process.env.DEBUG.should.equal('slack:*'); 24 | }); 25 | 26 | it('should throw error on missing opts', () => { 27 | const instance = () => { 28 | let core = new SlackCore(); 29 | } 30 | 31 | instance.should.Throw(Error); 32 | }) 33 | 34 | it('should throw error on missing token', () => { 35 | let core = new SlackCore({}); 36 | 37 | core.connect().should.be.rejected; 38 | }); 39 | 40 | it('should return a valid request class', () => { 41 | let instance = new SlackCore({ 42 | token: 'EXAMPLE_TOKEN' 43 | }); 44 | 45 | return instance.requestSlack().should.deep.equal(this.request); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/slack/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Request = require('./request'); 4 | const SlackWebSocket = require('./ws.js'); 5 | const Promise = require('bluebird'); 6 | const debug = require('debug')('slack:core'); 7 | 8 | class SlackCore { 9 | /** 10 | * @constructor 11 | */ 12 | constructor(opts) { 13 | if (!opts) 14 | throw new Error('Missing opts.'); 15 | if (opts.debug) process.env.DEBUG = 'slack:*'; 16 | 17 | debug('Initialize'); 18 | 19 | this.request = new Request(opts && opts.token); 20 | this.token = opts && opts.token; 21 | } 22 | 23 | 24 | /** 25 | * Connects to Slack Web API. 26 | * 27 | * @returns {Promise} 28 | */ 29 | connect() { 30 | let that = this; 31 | const url = this.domain + 'rtm.start'; 32 | 33 | if (!this.token) 34 | return new Promise((resolve, reject) => reject('Token is missing.')) 35 | 36 | debug('Connect request sent'); 37 | 38 | return this 39 | .request.rtmStart() 40 | .then((response) => { 41 | if (!response.ok) { 42 | debug('Connect request failed due to', response.error.message); 43 | throw new Error(response.error.message) 44 | } 45 | 46 | return (new SlackWebSocket(response.url, that.request)).connect(); 47 | }); 48 | } 49 | 50 | /** 51 | * Returns Request object to query Slack's API. 52 | * 53 | * @returns {Request} 54 | */ 55 | requestSlack() { 56 | return this.request; 57 | } 58 | } 59 | 60 | module.exports = SlackCore; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sourcebot", 3 | "version": "0.4.0", 4 | "description": "SourceBot is a platform independent chat bot platform.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec ./test/* && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 8 | "lint": "eslint 'src/**/*.@(js)'" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sourcebot/sourcebot.git" 13 | }, 14 | "keywords": [ 15 | "chatbots", 16 | "slack", 17 | "skype", 18 | "facebook messenger", 19 | "bot" 20 | ], 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/sourcebot/sourcebot/issues" 24 | }, 25 | "homepage": "https://github.com/sourcebot/sourcebot#readme", 26 | "dependencies": { 27 | "bluebird": "^3.4.4", 28 | "debug": "^2.2.0", 29 | "lodash": "^4.15.0", 30 | "request": "^2.75.0", 31 | "request-promise": "^4.1.1", 32 | "ws": "^3.0.0" 33 | }, 34 | "devDependencies": { 35 | "chai": "^3.5.0", 36 | "chai-as-promised": "^7.0.0", 37 | "coveralls": "^2.11.13", 38 | "eslint": "^4.1.0", 39 | "eslint-config-airbnb": "^14.1.0", 40 | "eslint-plugin-import": "^2.2.0", 41 | "eslint-plugin-jsx-a11y": "^6.0.0", 42 | "eslint-plugin-react": "^7.0.0", 43 | "istanbul": "^0.4.5", 44 | "mocha": "^3.0.2", 45 | "sinon": "^2.2.0", 46 | "sinon-chai": "^2.8.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/slack/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const request = require('request-promise'); 5 | const _ = require('lodash'); 6 | 7 | 8 | class Request { 9 | /** 10 | * @constructor 11 | */ 12 | constructor(token) { 13 | this.token = token; 14 | this.apiUrl = 'https://slack.com/api/' 15 | } 16 | 17 | 18 | /** 19 | * Requests Slack Web API. 20 | * @private 21 | */ 22 | request_(opts) { 23 | opts = _.merge(opts, { 24 | form: { 25 | token: this.token 26 | }, 27 | json: true 28 | }) 29 | 30 | return request(opts) 31 | } 32 | 33 | 34 | /** 35 | * @param {string} channel Channel ID. 36 | * @returns {Promise} 37 | */ 38 | getChannelInfo(channel) { 39 | return this.request_({ 40 | method: 'POST', 41 | uri: this.apiUrl + '/channels.info', 42 | form: { 43 | channel: channel 44 | } 45 | }); 46 | } 47 | 48 | 49 | /** 50 | * @param {string} user User ID. 51 | * @returns {Promise} 52 | */ 53 | getUserInfo(user) { 54 | return this.request_({ 55 | method: 'POST', 56 | uri: this.apiUrl + '/users.info', 57 | form: { 58 | user: user 59 | } 60 | }); 61 | } 62 | 63 | 64 | /** 65 | * @returns {Promise} 66 | */ 67 | rtmStart() { 68 | return this.request_({ 69 | method: 'POST', 70 | uri: this.apiUrl + '/rtm.start' 71 | }); 72 | } 73 | 74 | 75 | /** 76 | * Post a message to a channel, even DM. 77 | */ 78 | openDirectMessageChannel(user) { 79 | return this.request_({ 80 | method: 'POST', 81 | uri: this.apiUrl + '/im.open', 82 | form: { 83 | user: user 84 | } 85 | }); 86 | } 87 | } 88 | 89 | module.exports = Request; 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] - 8 | 9 | ## [0.4.0] - 2016-09-19 10 | ### Added 11 | - Added tests for stability. 12 | - Added Travis CI for tests. 13 | - Added Coveralls.io integration for code coverage statistics. 14 | 15 | ### Fixed 16 | - Fixed startConversation not replying to anybody except the first responder bug. 17 | - Fixed documentation. 18 | 19 | ### Changed 20 | - Slack WebSocket now throws an error if keys/opts are invalid/missing. 21 | - Slack WebSocket constructor won't connect automatically unless you call ```connect()``` function. 22 | 23 | 24 | ## [0.3.0] - 2016-09-05 25 | ### Added 26 | - Added Slack's API interface to communicate with Slack. 27 | - Added the ability to have Private Conversations. 28 | - Added debug option to SlackBot constructor to enable debug mode. 29 | 30 | ### Changed 31 | - ```conversation.ask``` now supports replyPattern, and callback for failure cases. For more information, follow README. 32 | - ```conversation.askSerial``` now supports replyPattern and callback for failure cases. For more information, follow README. 33 | 34 | ## [0.2.0] - 2016-09-01 35 | ### Added 36 | - Added Conversation methods. 37 | 38 | ### Changed 39 | - Documentation improved. 40 | 41 | ## [0.1.2] - 2016-08-31 42 | ### Added 43 | - Added disconnect method. 44 | - Add RegExp to listen function. 45 | 46 | ### Fixed 47 | - Multiple listeners inconsistency solved. 48 | - Multiple typos. 49 | 50 | [Unreleased]: https://github.com/sourcebot/sourcebot/compare/0.4.0...HEAD 51 | [0.4.0]: https://github.com/sourcebot/sourcebot/compare/0.3.0...0.4.0 52 | [0.3.0]: https://github.com/sourcebot/sourcebot/compare/0.2.0...0.3.0 53 | [0.2.0]: https://github.com/sourcebot/sourcebot/compare/0.1.2...0.2.0 54 | [0.1.2]: https://github.com/sourcebot/sourcebot/compare/0.1.1...0.1.2 55 | -------------------------------------------------------------------------------- /test/slack/ws.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const sinonChai = require('sinon-chai'); 6 | const chaiAsPromised = require('chai-as-promised'); 7 | const should = chai.should(); 8 | 9 | chai.use(sinonChai); 10 | chai.use(chaiAsPromised); 11 | 12 | const Promise = require('bluebird'); 13 | const EventEmitter = require('events'); 14 | const WSInstance = require('../../src/slack/ws'); 15 | const Request = require('../../src/slack/request'); 16 | const ws = require('ws'); 17 | 18 | class MockWebSocket { 19 | constructor(url) {} 20 | 21 | on(input, callback) { 22 | callback(); 23 | } 24 | send(text, callback) { 25 | callback(); 26 | } 27 | } 28 | 29 | describe('Websocket Properties', () => { 30 | beforeEach(() => { 31 | this.websocket = new MockWebSocket(); 32 | 33 | sinon.spy(this.websocket, 'on'); 34 | sinon.spy(this.websocket, 'send'); 35 | 36 | this.request = new Request('EXAMPLE_TOKEN'); 37 | this.instance = new WSInstance('ws://echo.websocket.org/', this.request); 38 | }); 39 | 40 | it('should throw error on missing url or request instance', () => { 41 | const instance = () => { 42 | let core = new WSInstance(); 43 | }; 44 | 45 | instance.should.Throw(Error) 46 | }); 47 | 48 | it('should throw error on missing request instance', () => { 49 | const instance = () => { 50 | let core = new WSInstance('https://github.com/sourcebot/sourcebot'); 51 | }; 52 | 53 | instance.should.throw(Error); 54 | }); 55 | 56 | it('should have a valid ws instance', () => { 57 | return this.instance 58 | .connect() 59 | .then((bot) => { 60 | bot.should.be.an.instanceof(WSInstance); 61 | bot.websocket.should.exist; 62 | }); 63 | }); 64 | 65 | it('should return websocket instance on getSocketInstance', () => { 66 | return this.instance 67 | .connect() 68 | .then((bot) => { 69 | bot.should.exist; 70 | bot.getSocketInstance().should.be.an.instanceof(ws); 71 | }); 72 | }); 73 | 74 | it('should send a message and bump count', () => { 75 | let that = this; 76 | 77 | return this.instance 78 | .connect() 79 | .then((bot) => { 80 | bot.websocket = that.websocket; 81 | bot.messageCount.should.equal(1); 82 | 83 | bot.send({ 84 | text: 'Hello world', 85 | channel: 'TEST_CHANNEL' 86 | }); 87 | 88 | bot.messageCount.should.equal(2); 89 | bot.websocket.send.should.calledOnce; 90 | }); 91 | }); 92 | 93 | it('should reject if an error occured while sending a message', () => { 94 | let that = this; 95 | 96 | return this.instance 97 | .connect() 98 | .then((bot) => { 99 | that.websocket.send = (message, callback) => { 100 | callback(new Error()); 101 | }; 102 | 103 | bot.websocket = that.websocket; 104 | bot.messageCount.should.equal(1); 105 | 106 | bot.send({ 107 | text: 'Hello world', 108 | channel: 'TEST_CHANNEL' 109 | }).should.be.rejected; 110 | 111 | bot.messageCount.should.not.equal(2); 112 | }); 113 | }); 114 | 115 | it('should start conversation properly', () => { 116 | return this.instance 117 | .connect() 118 | .then((bot) => { 119 | bot.startConversation.should.exist; 120 | bot.conversations.should.be.an.instanceof(Array); 121 | bot.conversations.should.have.length(0); 122 | 123 | return bot 124 | .startConversation('CHANNEL', 'USER') 125 | .then((conversation) => { 126 | bot.conversations.should.have.length(1); 127 | 128 | conversation.channel.should.equal(bot.conversations[bot.conversations.length - 1].channel); 129 | conversation.user.should.equal(bot.conversations[bot.conversations.length - 1].user); 130 | }); 131 | }); 132 | }); 133 | 134 | it('should not start conversation if it already exist', () => { 135 | return this.instance 136 | .connect() 137 | .then((bot) => { 138 | bot.conversations.should.have.length(0); 139 | 140 | return bot 141 | .startConversation('CHANNEL', 'USER') 142 | .then((conversation) => { 143 | bot.conversations.should.have.length(1); 144 | 145 | return bot.startConversation('CHANNEL', 'USER').should.be.rejected; 146 | }); 147 | }); 148 | }) 149 | }); 150 | -------------------------------------------------------------------------------- /src/slack/conversation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | const EventEmitter = require('events'); 5 | const debug = require('debug')('slack:conversation'); 6 | 7 | class Conversation { 8 | 9 | /** 10 | * @Constructor 11 | * 12 | * @param {Object} websocket - Websocket instance. 13 | * @param {String=} user - User id. 14 | * @param {String} channel - Channel name. 15 | */ 16 | constructor(websocket, channel, user) { 17 | debug('Initialize conversation.'); 18 | this.websocket = websocket; 19 | this.eventEmitter = new EventEmitter(); 20 | this.messageCount = 1; 21 | this.user = user; 22 | this.channel = channel; 23 | this.listen_(); 24 | } 25 | 26 | 27 | destroy() { 28 | this.eventEmitter.removeAllListeners(); 29 | } 30 | 31 | 32 | /** 33 | * @private 34 | * Listens the channel for user message. 35 | */ 36 | listen_() { 37 | const that = this; 38 | 39 | debug('Listening for user message.'); 40 | this.websocket.on('message', (raw) => { 41 | const response = JSON.parse(raw); 42 | 43 | if (that.user) { 44 | if (response.user === that.user) { 45 | that.eventEmitter.emit(response.type, response); 46 | } 47 | } else { 48 | that.eventEmitter.emit(response.type, response); 49 | } 50 | }); 51 | } 52 | 53 | 54 | /** 55 | * Say something to a channel. 56 | * 57 | * @param {String} message - Message to say. 58 | * 59 | * @returns {Promise} 60 | */ 61 | say(message) { 62 | let that = this; 63 | 64 | const opts = { 65 | id: this.messageCount, 66 | type: 'message', 67 | channel: this.channel, 68 | text: message 69 | }; 70 | 71 | return new Promise((resolve, reject) => { 72 | debug('Send message initialize', opts.text); 73 | that.websocket.send(JSON.stringify(opts), (err) => { 74 | if (err) { 75 | debug('Send message failed due to', err.message); 76 | return reject(err); 77 | } 78 | 79 | that.messageCount += 1; 80 | 81 | debug('Send message successful'); 82 | resolve(); 83 | }); 84 | }); 85 | } 86 | 87 | 88 | /** 89 | * Asks the question and waits for the answer. 90 | * If format provided asks again until enforced replyPattern requirements met. 91 | * 92 | * @param {(string|Object)} opts - String, or object. 93 | * @param {string=} opts.text - Question. 94 | * @param {RegExp=} opts.replyPattern - Reply pattern as an instance of RegExp. 95 | * @param {(function|Promise)} cb - Callback. Has parameter `response`. 96 | * 97 | * @returns {Promise} 98 | */ 99 | ask(opts, cb) { 100 | if (!opts) return Promise.reject(new Error('Unknown question object/string.')); 101 | 102 | /** 103 | * Opts can be an object or a string. 104 | */ 105 | if (typeof(opts) == 'string') { 106 | opts = { 107 | text: opts 108 | }; 109 | } 110 | 111 | /** 112 | * Add a reply pattern to check if the response fits your needs. 113 | */ 114 | if (opts.replyPattern && !(opts.replyPattern instanceof RegExp)) { 115 | return Promise.reject(new Error('replyPattern is not valid. It should be an instance of RegExp.')); 116 | } 117 | 118 | let that = this; 119 | 120 | debug('Ask question to the user.'); 121 | return this 122 | .say(opts.text) 123 | .then(() => { 124 | return new Promise((resolve) => { 125 | debug('Wait for a response.'); 126 | that.eventEmitter.once('message', (response) => { 127 | debug('Response received', response.text); 128 | if (opts.replyPattern && opts.replyPattern.test(response.text)) { 129 | return resolve(response); 130 | } 131 | 132 | if (cb) { 133 | /** 134 | * Check if callback is promisified. If it's promisified, wait for it. 135 | */ 136 | if (typeof cb.then === 'function') { 137 | return cb(response).then(() => { 138 | return resolve(that.ask(opts, cb)); 139 | }); 140 | } 141 | 142 | cb(response); 143 | } 144 | 145 | resolve(response); 146 | }); 147 | }) 148 | }); 149 | } 150 | 151 | 152 | /** 153 | * Asks an array of questions while waiting for the answer of each. 154 | * 155 | * @param {Object[]|string} opts - Array of opt objects. 156 | * @param {string} opts[].text - Question to be asked. 157 | * @param {string=} opts[].replyPattern - Reply pattern to be enforced. 158 | * @param {Function=} opts[].callback - Callback to call upon faulty replies. 159 | * 160 | * @returns {Promise} 161 | */ 162 | askSerial(opts) { 163 | return Promise.mapSeries(opts, (opt) => { 164 | return that.ask(opt, opt.callback || null) 165 | .then((response) => { 166 | return response; 167 | }); 168 | }); 169 | } 170 | } 171 | 172 | module.exports = Conversation; 173 | -------------------------------------------------------------------------------- /src/slack/ws.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebSocket = require('ws'); 4 | const debug = require('debug')('slack:websocket'); 5 | const EventEmitter = require('events'); 6 | const Promise = require('bluebird'); 7 | const _ = require('lodash'); 8 | const Conversation = require('./conversation'); 9 | 10 | class SlackWebSocket { 11 | /** 12 | * @constructor 13 | * 14 | * @param {String} url 15 | * @param {Request} request class instance. 16 | */ 17 | constructor(url, request) { 18 | debug('Initialize'); 19 | 20 | if (!url) throw new Error('Missing url'); 21 | if (!request) throw new Error('Missing request instance'); 22 | 23 | this.request = request; 24 | this.url = url; 25 | this.messageCount = 1; 26 | this.eventEmitter = new EventEmitter(); 27 | this.conversations = []; 28 | } 29 | 30 | 31 | /** 32 | * @private 33 | */ 34 | listenAllEvents_() { 35 | let that = this; 36 | 37 | this.websocket.on('message', (raw) => { 38 | let response = JSON.parse(raw); 39 | 40 | that.eventEmitter.emit(response.type, response); 41 | }); 42 | } 43 | 44 | 45 | /** 46 | * Listens Slack's Real Time Messaging API for specific message. 47 | * 48 | * @param {String|RegExp} message - Message to listen to. 49 | * @param {Function} callback - Callback function. 50 | */ 51 | listen(message, callback) { 52 | if (!message) return (new Error('Message is missing to listen.')); 53 | 54 | debug('Listening for message ' + message); 55 | 56 | this.eventEmitter.on('message', (response) => { 57 | debug('Message received', response.text); 58 | 59 | //Used 2 if-else if statement to increase readability. 60 | if (typeof message == 'string' && (response.text).match(new RegExp('.*\\b' + message + '\\b.*', 'i'))) { 61 | //Searches for the word inside the string/sentence. 62 | callback(response); 63 | } else if (message instanceof RegExp && message.test(response.text)) { 64 | callback(response); 65 | } 66 | }) 67 | } 68 | 69 | 70 | /** 71 | * Sends a message to specific channel. 72 | * 73 | * @param {Object} opts 74 | * 75 | * @returns {Promise} 76 | */ 77 | send(opts) { 78 | let that = this; 79 | 80 | opts.id = this.messageCount; 81 | opts.type = 'message'; 82 | 83 | return new Promise((resolve, reject) => { 84 | debug('Send message initialize'); 85 | this.websocket.send(JSON.stringify(opts), (err) => { 86 | if (err) { 87 | debug('Send message failed due to', err.message); 88 | return reject(err); 89 | } 90 | 91 | that.messageCount += 1; 92 | 93 | debug('Send message successful'); 94 | resolve(); 95 | }); 96 | }) 97 | } 98 | 99 | 100 | /** 101 | * Returns socket instance. 102 | * 103 | * @returns {Object} 104 | */ 105 | getSocketInstance() { 106 | return this.websocket; 107 | } 108 | 109 | 110 | /** 111 | * Connects to Slack's Real Time Messaging API. 112 | * 113 | * @returns {Promise} 114 | */ 115 | connect() { 116 | this.websocket = new WebSocket(this.url); 117 | 118 | return new Promise((resolve, reject) => { 119 | this.websocket.on('open', (response) => { 120 | debug('Websocket connected'); 121 | resolve(); 122 | }); 123 | 124 | this.websocket.on('close', (response) => { 125 | debug('Websocket closed'); 126 | }); 127 | 128 | this.websocket.on('error', (response) => { 129 | debug('Websocket error', response); 130 | reject(); 131 | }); 132 | 133 | this.websocket.on('disconnect', (response) => { 134 | debug('Websocket disconnected.'); 135 | reject(); 136 | }); 137 | }) 138 | .then(() => { 139 | this.listenAllEvents_(); 140 | 141 | return Promise.resolve(this); 142 | }) 143 | } 144 | 145 | 146 | /** 147 | * Disconnects from server. 148 | * 149 | * @returns {Promise} 150 | */ 151 | disconnect() { 152 | let that = this; 153 | 154 | return new Promise((resolve, reject) => { 155 | this.websocket.close((err) => { 156 | if (err) { 157 | debug('Connection close failed due to', err.message); 158 | return reject(err); 159 | } 160 | 161 | that.eventEmitter.removeAllListeners(); 162 | 163 | debug('Connection closed.'); 164 | resolve(); 165 | }); 166 | }); 167 | } 168 | 169 | 170 | /** 171 | * Starts a conversation, if not-exist. 172 | * 173 | * @param {String} channel - Channel name. 174 | * @param {String=} user - User id. 175 | * 176 | * @returns {Promise} 177 | */ 178 | startConversation(channel, user) { 179 | const conversationExist = _.findIndex(this.conversations, (item) => { 180 | return item.user == (user || null) && item.channel == channel; 181 | }); 182 | 183 | if (conversationExist != -1) return Promise.reject(new Error('Conversation exist')); 184 | 185 | this.conversations.push({ 186 | user: user || null, 187 | channel: channel, 188 | conversation: new Conversation(this.websocket, channel, user) 189 | }); 190 | 191 | return Promise.resolve(_.last(this.conversations).conversation); 192 | } 193 | 194 | 195 | /** 196 | * Creates a private conversation between a user. 197 | * 198 | * @returns {Promise} 199 | */ 200 | startPrivateConversation(user) { 201 | return this.request 202 | .openDirectMessageChannel(user) 203 | .then((response) => { 204 | if (!response.ok) return Promise.reject(new Error('Failed to create a private conversation.')); 205 | 206 | const channel = response.channel.id; 207 | 208 | return this.startConversation(channel, user); 209 | }); 210 | } 211 | } 212 | 213 | module.exports = SlackWebSocket; 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![SourceBot Framework](https://avatars0.githubusercontent.com/u/21346235?v=3&s=200) 2 | 3 | SourceBot 4 | == 5 | 6 | [![David](https://img.shields.io/david/sourcebot/sourcebot.svg)](https://david-dm.org/sourcebot/sourcebot) 7 | [![Code Climate](https://codeclimate.com/github/sourcebot/sourcebot/badges/gpa.svg)](https://codeclimate.com/github/sourcebot/sourcebot) 8 | [![Build Status](https://travis-ci.org/sourcebot/sourcebot.svg?branch=master)](https://travis-ci.org/sourcebot/sourcebot) 9 | [![Coverage Status](https://coveralls.io/repos/github/sourcebot/sourcebot/badge.svg)](https://coveralls.io/github/sourcebot/sourcebot) 10 | [![Greenkeeper badge](https://badges.greenkeeper.io/sourcebot/sourcebot.svg)](https://greenkeeper.io/) 11 | 12 | 13 | SourceBot is a platform independent chat bot framework. It aims to connect Facebook Messenger, Slack and Skype with the same code. 14 | 15 | Benefits of SourceKit: 16 | - Uses EcmaScript 6 class architecture. 17 | - Easily debuggable. 18 | - Uses Promises, catches uncaught exceptions on the way. 19 | 20 | In order to install: 21 | 22 | ``` 23 | npm install sourcebot --save 24 | ``` 25 | 26 | In order to debug (Example): 27 | ``` 28 | DEBUG=* node index.js 29 | ``` 30 | * For Windows: Before running the app, run this command in order to debug: ```set Debug=slack:core,slack:websocket,slack:conversation``` 31 | 32 | [![sourcebot_cmd.jpg](https://s14.postimg.org/o7rf82ki9/sourcebot_cmd.jpg)](https://postimg.org/image/bt4n7qszx/) 33 | 34 | Examples 35 | == 36 | 37 | ### Typical 'hello world': 38 | 39 | ```javascript 40 | let SlackCore = require('sourcebot').Slack; 41 | let SlackBot = new SlackCore({ 42 | token: 'xoxb-17065016470-0O9T0P9zSuMVEG8yM6QTGAIB' 43 | }); 44 | 45 | 46 | SlackBot 47 | .connect() 48 | .then((bot) => { 49 | bot 50 | .listen('hello', (response) => { 51 | bot.send({ 52 | channel: response.channel, 53 | text: 'world' 54 | }); 55 | }) 56 | }) 57 | .catch((err) => console.error(err.message)) 58 | ``` 59 | 60 | ### An example conversation: 61 | 62 | ```javascript 63 | SlackBot 64 | .connect() 65 | .then((bot) => { 66 | bot 67 | .listen(new RegExp('start convo', 'i'), (response) => { 68 | 69 | bot 70 | .startConversation(response.channel, response.user) 71 | .then((conversation) => { 72 | return conversation 73 | .ask('How are you?') 74 | .then((reply) => { 75 | return conversation 76 | .say('Good!') 77 | .then(() => { 78 | return conversation.ask('What are you doing now?') 79 | }) 80 | .then((response) => { 81 | return conversation.askSerial(['What?', 'Where?', 'When?']); 82 | }) 83 | }) 84 | }); 85 | }) 86 | }).catch((err) => console.error(err.message)); 87 | ``` 88 | 89 | ### An example private conversation 90 | 91 | 92 | ```javascript 93 | SlackBot 94 | .connect() 95 | .then((bot) => { 96 | bot 97 | .listen(new RegExp('start convo', 'i'), (response) => { 98 | bot 99 | .startPrivateConversation(response.user) 100 | .then((conversation) => { 101 | conversation 102 | .ask('Hello world') 103 | .then((response) => { 104 | conversation.say('You said ' + response.text); 105 | }) 106 | }) 107 | }) 108 | }).catch((err) => console.error(err.message)); 109 | 110 | ``` 111 | 112 | ### Query Slack's API 113 | 114 | ```javascript 115 | SlackBot 116 | .connect() 117 | .then((bot) => { 118 | bot 119 | .listen(new RegExp('start convo', 'i'), (response) => { 120 | 121 | SlackBot 122 | .requestSlack() 123 | .getChannelInfo(response.channel) 124 | .then((channelInfo) => { 125 | bot.send({ 126 | channel: response.channel, 127 | text: 'Wow, wow, wow! We have ' + channelInfo.channel.members.length + ' users in here!' 128 | }); 129 | 130 | const tasks = channelInfo.channel.members.map((member) => { 131 | return SlackBot.requestSlack().getUserInfo(member) 132 | }); 133 | 134 | Promise 135 | .all(tasks) 136 | .then((users) => { 137 | users.forEach((item) => { 138 | bot.send({ 139 | channel: response.channel, 140 | text: 'Welcome <@' + item.user.id +'|' + item.user.name +'>, I\'ve missed you!' 141 | }); 142 | }) 143 | }) 144 | }) 145 | }) 146 | }).catch((err) => console.error(err.message)); 147 | ``` 148 | 149 | #### Ask series of questions 150 | 151 | 152 | 153 | ```javascript 154 | SlackBot 155 | .connect() 156 | .then((bot) => { 157 | bot 158 | .listen(new RegExp('start convo', 'i'), (response) => { 159 | 160 | bot 161 | .startConversation(response.channel, response.User) 162 | .then((conversation) => { 163 | return conversation 164 | .askSerial([ 165 | { 166 | text: 'How are you?', 167 | replyPattern: new RegExp('\\bfine\\b', 'i') //Asks until the given response contains 'fine'. 168 | }, 169 | { 170 | text: 'Where are you?', 171 | replyPattern: new RegExp('\\bistanbul\\b', 'i'), 172 | callback: (faultyReply) => { //Fires up if the response does not contain 'istanbul'. 173 | return conversation.say('Please indicate your city.'); 174 | } 175 | } 176 | ]).then((responses) => { 177 | console.log(responses); 178 | }); 179 | }); 180 | }) 181 | }).catch((err) => console.error(err.message)); 182 | ``` 183 | 184 | 185 | 186 | Methods 187 | === 188 | #### SlackBot 189 | * ```constructor(opts)``` 190 | * Constructs the SlackCore class with ```opts.token``` 191 | * If ```opts.debug``` is defined, SlackBot will enter in debug mode. 192 | * ```connect()``` 193 | * Connects to Slack Web API 194 | * ```requestSlack()``` 195 | * Returns Slack API endpoint. 196 | * ```rtmStart()``` 197 | * ```getChannelInfo(channelId)``` 198 | * ```getUserInfo(userId)``` 199 | * ```openDirectMessageChannel(userId)``` 200 | 201 | #### Bot 202 | * ```listen(message, callback)``` 203 | * Listens for the message. The message can be an instance of RegExp or a plain String. Returns promise containing the response. 204 | * ```send(opts)``` 205 | * Sends a message to specified channel, Takes ```opts``` object as a parameter containing text and channel fields. Returns empty promise. 206 | * ```startConversation(channelName, userId)``` 207 | * Starts a conversation with the specified user in a specified channel. Takes user's slack id and the id of the channel. Returns promise containing a ```conversation``` object. 208 | * ```startPrivateConversation(user)``` 209 | * Starts private conversation between a user. Returns promise containing a ```conversation``` object. 210 | * ```disconnect()``` 211 | * Disconnects and removes all event listeners. 212 | 213 | #### Conversation 214 | * ```ask(opts||message, callback)``` 215 | * Sends the given ```opts``` Object or ```question``` String and waits for a response. If ```opts.replyPattern``` is provided asks until the RegExp test succeeds, fires callback upon faulty replies with the ```faultyReply```. Returns a promise containing the ```response```. 216 | * ```let opts = {text: 'Question', replyPattern: new RegExp('')}``` 217 | * ```say(message)``` 218 | * Sends the given String ```message```. Returns empty promise. 219 | * ```askSerial(opts)``` 220 | * Behaves same as ```ask()``` but this method takes an array of objects that are asked sequentially. 221 | ```javascript 222 | let opts = { 223 | text: 'Question', 224 | replyPattern: new RegExp(''), 225 | callback: (faultyReply) => { 226 | return Promise.resolve() 227 | } 228 | } 229 | ``` 230 | 231 | 232 | The MIT License 233 | === 234 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 235 | 236 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 237 | 238 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 239 | --------------------------------------------------------------------------------