├── .gitignore ├── LICENSE ├── README.md ├── examples ├── botkit │ ├── bot.js │ └── package.json └── flint │ ├── bot.js │ └── package.json ├── index.js ├── lib ├── ConnectionService.js ├── EventHandler.js └── SparkWebSocket.js ├── package.json └── tests └── event-callback.js /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################ 2 | ############### .gitignore ################## 3 | ################################################ 4 | # 5 | # This file is only relevant if you are using git. 6 | # 7 | # Files which match the splat patterns below will 8 | # be ignored by git. This keeps random crap and 9 | # sensitive credentials from being uploaded to 10 | # your repository. It allows you to configure your 11 | # app for your machine without accidentally 12 | # committing settings which will smash the local 13 | # settings of other developers on your team. 14 | # 15 | # Some reasonable defaults are included below, 16 | # but, of course, you should modify/extend/prune 17 | # to fit your needs! 18 | ################################################ 19 | 20 | 21 | 22 | 23 | ################################################ 24 | # Local Configuration 25 | # 26 | # Explicitly ignore files which contain: 27 | # 28 | # 1. Sensitive information you'd rather not push to 29 | # your git repository. 30 | # e.g., your personal API keys or passwords. 31 | # 32 | # 2. Environment-specific configuration 33 | # Basically, anything that would be annoying 34 | # to have to change every time you do a 35 | # `git pull` 36 | # e.g., your local development database, or 37 | # the S3 bucket you're using for file uploads 38 | # development. 39 | # 40 | ################################################ 41 | 42 | config/local.js 43 | device.json 44 | 45 | 46 | 47 | 48 | ################################################ 49 | # Dependencies 50 | # 51 | # When releasing a production app, you may 52 | # consider including your node_modules and 53 | # bower_components directory in your git repo, 54 | # but during development, its best to exclude it, 55 | # since different developers may be working on 56 | # different kernels, where dependencies would 57 | # need to be recompiled anyway. 58 | # 59 | # More on that here about node_modules dir: 60 | # http://www.futurealoof.com/posts/nodemodules-in-git.html 61 | # (credit Mikeal Rogers, @mikeal) 62 | # 63 | # About bower_components dir, you can see this: 64 | # http://addyosmani.com/blog/checking-in-front-end-dependencies/ 65 | # (credit Addy Osmani, @addyosmani) 66 | # 67 | ################################################ 68 | 69 | node_modules 70 | 71 | 72 | 73 | 74 | ################################################ 75 | # Sails.js / Waterline / Grunt 76 | # 77 | # Files generated by Sails and Grunt, or related 78 | # tasks and adapters. 79 | ################################################ 80 | .tmp 81 | dump.rdb 82 | 83 | 84 | 85 | 86 | 87 | ################################################ 88 | # Node.js / NPM 89 | # 90 | # Common files generated by Node, NPM, and the 91 | # related ecosystem. 92 | ################################################ 93 | lib-cov 94 | *.seed 95 | *.log 96 | *.out 97 | *.pid 98 | npm-debug.log 99 | 100 | 101 | 102 | 103 | 104 | ################################################ 105 | # Miscellaneous 106 | # 107 | # Common files generated by text editors, 108 | # operating systems, file systems, etc. 109 | ################################################ 110 | 111 | *~ 112 | *# 113 | .DS_STORE 114 | .netbeans 115 | nbproject 116 | .idea 117 | .node_history 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marcello Federico 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cisco Spark Websocket Events 2 | =========================================== 3 | 4 | Provides a simple way to get events through Cisco Spark's native websocket. 5 | 6 | This module is useful when deploying a Cisco Spark bot behind a firewall with no way to get the traditional inbound webhooks back to the bot. 7 | 8 | 9 | ## Installation 10 | 11 | `npm install ciscospark-websocket-events` 12 | 13 | 14 | ## Usage 15 | 16 | This module can be used in two different ways. 17 | The first is by setting an event callback to handle the events directly in your code. 18 | The second is to define URL to the location you would like to post event data to. 19 | 20 | The current events supported are: 21 | 22 | * Message Created 23 | * Membership Created 24 | * Room Updated 25 | 26 | Here is an example event: 27 | ```json 28 | { 29 | "event": "created", 30 | "resource": "messages", 31 | "data": { 32 | "id": "Y2lzY29zcGFyazovL3VzL01FU1NBR0UvMzkyM2RiNDAtMTU4ZS0xMWU3LWI1OWItMjNiODI4NTFiY2Fh", 33 | "roomId": "Y2lzY29zcGFyazovL3VzL1JPT00vOTAwYjZiNTEtNDc2ZC0zMjkzLThlMTAtYmI1MTVjN2RjNDQy", 34 | "roomType": "direct", 35 | "text": "Hello", 36 | "personId": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS83M2YwNThiZS01MTRjLTQ5OTAtYTkyZi00MWNlY2M4NWFiMzc", 37 | "personEmail": "marfeder@cisco.com", 38 | "html": "
Hello
", 39 | "created": "2017-03-30T21:17:04.628Z" 40 | } 41 | } 42 | ``` 43 | 44 | 45 | ## Example 1: using the event handler callback 46 | 47 | Run the sample from a terminal: 48 | 49 | ```shell 50 | > cd ciscospark-websocket-events 51 | > npm install 52 | > cd tests 53 | > SPARK_TOKEN=XXXXXXXXXXXXXXXXX node event-callback.js 54 | ... 55 | ``` 56 | 57 | Check the code: 58 | 59 | ```javascript 60 | var SparkWebSocket = require('ciscospark-websocket-events'); 61 | var accessToken = process.env.SPARK_TOKEN; 62 | 63 | sparkwebsocket = new SparkWebSocket(accessToken); 64 | sparkwebsocket.connect(function (err, res){ 65 | if (!err) { 66 | sparkwebsocket.setEventCallback(function (event){ 67 | console.log("New Event"); 68 | console.log("---------"); 69 | console.log(JSON.stringify(event, null, 2)); 70 | 71 | // do something with the event 72 | }); 73 | 74 | } 75 | else { 76 | console.log("Error starting up websocket: " + err); 77 | } 78 | }): 79 | ``` 80 | 81 | 82 | ## Example 2: forwarding the event using the webhook_url 83 | 84 | ```javascript 85 | var SparkWebSocket = require('ciscospark-websocket-events'); 86 | var accessToken = process.env.SPARK_TOKEN; 87 | var webHookUrl = process.env.WEBHOOK_URL; // http://localhost:8080/mybot/incoming_event 88 | 89 | sparkwebsocket = new SparkWebSocket(accessToken); 90 | sparkwebsocket.connect(function(err, res){ 91 | if (!err) { 92 | if (webHookUrl) { 93 | sparkwebsocket.setWebHookURL(webHookUrl); 94 | } 95 | } 96 | else { 97 | console.log("Error starting up websocket: " + err); 98 | } 99 | }); 100 | ``` 101 | 102 | 103 | ## BotKit Example 104 | 105 | Run the sample from a terminal: 106 | 107 | ```shell 108 | > cd examples 109 | > cd botkit 110 | > npm install 111 | > SPARK_TOKEN=XXXXXXXXXXXXXXXXX node bot.js 112 | ... 113 | ``` 114 | 115 | Check the [BotKit code sample](examples/botkit/bot.js): 116 | 117 | ```javascript 118 | /// Setup the Cisco Spark Websocket 119 | 120 | var SparkWebSocket = require('ciscospark-websocket-events') 121 | 122 | var accessToken = process.env.SPARK_TOKEN 123 | var PORT = process.env.PORT || 3000 124 | 125 | var webHookUrl = "http://localhost:"+PORT+"/ciscospark/receive" 126 | 127 | sparkwebsocket = new SparkWebSocket(accessToken) 128 | sparkwebsocket.connect(function(err,res){ 129 | if (!err) { 130 | if(webHookUrl) 131 | sparkwebsocket.setWebHookURL(webHookUrl) 132 | } 133 | else { 134 | console.log("Error starting up websocket: "+err) 135 | } 136 | }) 137 | 138 | //////// Bot Kit ////// 139 | 140 | var Botkit = require('botkit'); 141 | 142 | var controller = Botkit.sparkbot({ 143 | debug: true, 144 | log: true, 145 | public_address: "https://localhost", 146 | ciscospark_access_token: process.env.SPARK_TOKEN 147 | }); 148 | 149 | 150 | var bot = controller.spawn({ 151 | }); 152 | 153 | controller.setupWebserver(PORT, function(err, webserver) { 154 | 155 | //setup incoming webhook handler 156 | webserver.post('/ciscospark/receive', function(req, res) { 157 | res.sendStatus(200) 158 | controller.handleWebhookPayload(req, res, bot); 159 | }); 160 | 161 | }); 162 | 163 | controller.hears('hello', 'direct_message,direct_mention', function(bot, message) { 164 | bot.reply(message, 'Hi'); 165 | }); 166 | 167 | controller.on('direct_mention', function(bot, message) { 168 | bot.reply(message, 'You mentioned me and said, "' + message.text + '"'); 169 | }); 170 | 171 | controller.on('direct_message', function(bot, message) { 172 | bot.reply(message, 'I got your private message. You said, "' + message.text + '"'); 173 | }); 174 | ``` 175 | 176 | 177 | ## Flint Example 178 | 179 | Run the sample from a terminal: 180 | 181 | ```shell 182 | > cd examples 183 | > cd flint 184 | > npm install 185 | > SPARK_TOKEN=XXXXXXXXXXXXXXXXX node bot.js 186 | ... 187 | ``` 188 | 189 | 190 | Check the [flint code sample](examples/flint/bot.js): 191 | 192 | ```javascript 193 | // Spark Websocket Intialization 194 | var SparkWebSocket = require('ciscospark-websocket-events') 195 | 196 | var accessToken = process.env.SPARK_TOKEN 197 | var PORT = process.env.PORT || 8080 198 | 199 | var webHookUrl = "http://localhost:"+PORT+"/flint" 200 | 201 | sparkwebsocket = new SparkWebSocket(accessToken) 202 | sparkwebsocket.connect(function(err,res){ 203 | if (!err) 204 | { 205 | sparkwebsocket.setWebHookURL(webHookUrl) 206 | } 207 | else { 208 | console.log("Error starting up websocket: "+err) 209 | } 210 | 211 | }) 212 | 213 | // Flint Bot Initialization 214 | 215 | var Flint = require('node-flint'); 216 | var webhook = require('node-flint/webhook'); 217 | var express = require('express'); 218 | var bodyParser = require('body-parser'); 219 | var app = express(); 220 | app.use(bodyParser.json()); 221 | 222 | // flint options 223 | var config = { 224 | token: accessToken, 225 | port: PORT, 226 | removeWebhooksOnStart: true, 227 | maxConcurrent: 5, 228 | minTime: 50 229 | }; 230 | 231 | // init flint 232 | var flint = new Flint(config); 233 | flint.start(); 234 | 235 | // say hello 236 | flint.hears('/hello', function(bot, trigger) { 237 | flind.debug('Flind hears /hello'); 238 | bot.say('I hear you, %s!', trigger.personDisplayName); 239 | }); 240 | 241 | // add flint event listeners 242 | flint.on('message', function(bot, trigger, id) { 243 | flint.debug('"%s" said "%s" in room "%s"', trigger.personEmail, trigger.text, trigger.roomTitle); 244 | if (trigger.text === '/hello') { 245 | bot.say('"%s" said keyword in room "%s".', trigger.personEmail, trigger.roomTitle); 246 | } 247 | }); 248 | 249 | flint.on('initialized', function() { 250 | flint.debug('initialized %s rooms', flint.bots.length); 251 | }); 252 | 253 | // define express path for incoming webhooks 254 | app.post('/flint', webhook(flint)); 255 | 256 | // start express server 257 | var server = app.listen(config.port, function () { 258 | flint.debug('Flint listening on port %s', config.port); 259 | }); 260 | 261 | // gracefully shutdown (ctrl-c) 262 | process.on('SIGINT', function() { 263 | flint.debug('stoppping...'); 264 | server.close(); 265 | flint.stop().then(function() { 266 | process.exit(); 267 | }); 268 | }); 269 | ``` 270 | 271 | ## Proxy Support 272 | 273 | Cisco Spark Websockets now support web proxies! 274 | 275 | To enable web proxy support set the following enviroment variables: 276 | * HTTP_PROXY=(to your web proxy URL) 277 | * NO_PROXY=localhost 278 | 279 | 280 | -------------------------------------------------------------------------------- /examples/botkit/bot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cisco Spark WebSocket example using BotKit 3 | */ 4 | 5 | var accessToken = process.env.SPARK_TOKEN; 6 | if (!accessToken) { 7 | console.log("No Cisco Spark access token found in env variable SPARK_TOKEN"); 8 | process.exit(2); 9 | } 10 | 11 | var PORT = process.env.PORT || 3000; 12 | 13 | 14 | // Spark Websocket Intialization 15 | var SparkWebSocket = require('ciscospark-websocket-events'); 16 | sparkwebsocket = new SparkWebSocket(accessToken); 17 | sparkwebsocket.connect(function (err, res) { 18 | if (!err) { 19 | sparkwebsocket.setWebHookURL("http://localhost:" + PORT + "/ciscospark/receive"); 20 | } 21 | else { 22 | console.log("Error starting up websocket: " + err); 23 | } 24 | }) 25 | 26 | //////// Bot Kit ////// 27 | 28 | var Botkit = require('botkit'); 29 | 30 | var controller = Botkit.sparkbot({ 31 | debug: true, 32 | log: true, 33 | public_address: "https://localhost", 34 | ciscospark_access_token: accessToken 35 | }); 36 | 37 | var bot = controller.spawn({ 38 | }); 39 | 40 | controller.setupWebserver(PORT, function (err, webserver) { 41 | 42 | //setup incoming webhook handler 43 | webserver.post('/ciscospark/receive', function (req, res) { 44 | res.sendStatus(200) 45 | controller.handleWebhookPayload(req, res, bot); 46 | }); 47 | 48 | }); 49 | 50 | 51 | // 52 | // Bot custom logic 53 | // 54 | 55 | controller.hears('hello', 'direct_message,direct_mention', function (bot, message) { 56 | bot.reply(message, 'Hi'); 57 | }); 58 | 59 | controller.on('direct_mention', function (bot, message) { 60 | bot.reply(message, 'You mentioned me and said, "' + message.text + '"'); 61 | }); 62 | 63 | controller.on('direct_message', function (bot, message) { 64 | bot.reply(message, 'I got your private message. You said, "' + message.text + '"'); 65 | }); 66 | -------------------------------------------------------------------------------- /examples/botkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botkit-sample", 3 | "version": "0.1.0", 4 | "description": "BotKit Sample for Cisco Spark Websocket Events", 5 | "main": "botkit.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "botkit": "^0.5.3", 13 | "ciscospark-websocket-events": "^1.0.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/flint/bot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cisco Spark WebSocket example using BotKit 3 | */ 4 | 5 | var accessToken = process.env.SPARK_TOKEN; 6 | if (!accessToken) { 7 | console.log("No Cisco Spark access token found in env variable SPARK_TOKEN"); 8 | process.exit(2); 9 | } 10 | 11 | var PORT = process.env.PORT || 8080; 12 | 13 | 14 | // Spark Websocket Intialization 15 | var SparkWebSocket = require('ciscospark-websocket-events'); 16 | sparkwebsocket = new SparkWebSocket(accessToken); 17 | sparkwebsocket.connect(function (err, res) { 18 | if (!err) { 19 | sparkwebsocket.setWebHookURL("http://localhost:" + PORT + "/flint"); 20 | } 21 | else { 22 | console.log("Error starting up websocket: " + err); 23 | } 24 | }); 25 | 26 | 27 | // Flint Bot Initialization /////////////////////////////// 28 | var Flint = require('node-flint'); 29 | var webhook = require('node-flint/webhook'); 30 | var express = require('express'); 31 | var bodyParser = require('body-parser'); 32 | var app = express(); 33 | app.use(bodyParser.json()); 34 | 35 | // flint options 36 | var config = { 37 | token: accessToken, 38 | port: PORT, 39 | removeWebhooksOnStart: true, 40 | maxConcurrent: 5, 41 | minTime: 50 42 | }; 43 | 44 | // init flint 45 | var flint = new Flint(config); 46 | flint.start(); 47 | 48 | flint.on('initialized', function () { 49 | flint.debug('initialized %s rooms', flint.bots.length); 50 | }); 51 | 52 | // define express path for incoming webhooks 53 | app.post('/flint', webhook(flint)); 54 | 55 | // start express server 56 | var server = app.listen(config.port, function () { 57 | flint.debug('Flint listening on port %s', config.port); 58 | }); 59 | 60 | // gracefully shutdown (ctrl-c) 61 | process.on('SIGINT', function () { 62 | flint.debug('stoppping...'); 63 | server.close(); 64 | flint.stop().then(function () { 65 | process.exit(); 66 | }); 67 | }); 68 | 69 | 70 | // 71 | // Bot custom logic 72 | // 73 | 74 | // say hello 75 | flint.hears('hello', function (bot, trigger) { 76 | flint.debug('Flint hears hello'); 77 | bot.say('I hear you, %s!', trigger.personDisplayName); 78 | }); 79 | 80 | 81 | // add flint event listeners 82 | flint.on('message', function (bot, trigger, id) { 83 | flint.debug('"%s" said "%s" in room "%s"', trigger.personEmail, trigger.text, trigger.roomTitle); 84 | if (trigger.text === 'hello') { 85 | bot.say('"%s" said keyword in room "%s".', trigger.personEmail, trigger.roomTitle); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /examples/flint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flint-sample", 3 | "version": "0.1.0", 4 | "description": "Flint Sample for Cisco Spark Websocket Events", 5 | "main": "flint.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "body-parser": "^1.17.1", 13 | "ciscospark-websocket-events": "^1.0.7", 14 | "express": "^4.15.2", 15 | "node-flint": "^4.5.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var SparkWebSocket = require('./lib/SparkWebSocket.js'); 5 | 6 | 7 | /** 8 | * Expose constructors. 9 | */ 10 | module.exports = SparkWebSocket; 11 | -------------------------------------------------------------------------------- /lib/ConnectionService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by marfeder on 3/30/17. 3 | */ 4 | 5 | var WebSocket = require('ws'); 6 | var HttpsProxyAgent = require('https-proxy-agent'); 7 | var urlUtil = require('url') 8 | var uuid = require('uuid'); 9 | var logger = require('winston'); 10 | 11 | function ConnectionService(url, token, eventHandler) { 12 | this.eh = eventHandler; 13 | if (process.env.HTTP_PROXY) 14 | { 15 | var options = urlUtil.parse(process.env.HTTP_PROXY); 16 | var agent = new HttpsProxyAgent(options); 17 | this.ws = new WebSocket(url, { agent: agent }); 18 | } 19 | else 20 | { 21 | this.ws = new WebSocket(url); 22 | } 23 | 24 | 25 | this.url = url; 26 | this.ws.binaryType = 'arraybuffer'; 27 | this.token = token; 28 | this.reconnectAttempts = 0; 29 | this.pingInterval = 10000; 30 | this.pingTimeout = this.pingInterval + 20000; 31 | this.lastPingId = ''; 32 | this.setupHandlers(); 33 | this.reconnecting = false; 34 | } 35 | 36 | 37 | ConnectionService.prototype.setToken = function (token) { 38 | this.token = token; 39 | } 40 | 41 | ConnectionService.prototype.setupHandlers = function () { 42 | 43 | var self = this; 44 | 45 | self.ws.on('open', function (connection) { 46 | 47 | logger.info('WebSocket client open'); 48 | 49 | self.ws.send(JSON.stringify( 50 | { 51 | id: uuid.v4(), 52 | type: 'authorization', 53 | data: { 54 | token: 'Bearer ' + self.token 55 | } 56 | })) 57 | 58 | self.pingIntervalTimer = setTimeout(function () { 59 | self.sendPing(self); 60 | }, self.pingInterval); 61 | self.pingTimeoutTimer = setTimeout(function () { 62 | clearTimeout(self.pingIntervalTimer) 63 | logger.info("ping timeout trying to reconnect....") 64 | self.reconnect() 65 | }, self.pingTimeout); 66 | }) 67 | 68 | self.ws.on('message', function (data, flags) { 69 | 70 | self.reconnectAttempts = 0; 71 | var message; 72 | message = new Uint8Array(data); 73 | message = String.fromCharCode.apply(null, message); 74 | try { 75 | message = JSON.parse(message); 76 | } 77 | catch (e) { 78 | logger.info("Failed to parse message.") 79 | } 80 | if (message.type == 'pong') //send another ping 81 | { 82 | if (self.lastPingId != message.id) { 83 | logger.info("Warning PONG ID doesn't match Ping id...") 84 | } 85 | 86 | clearTimeout(self.pingIntervalTimer) 87 | clearTimeout(self.pingTimeoutTimer) 88 | 89 | self.pingIntervalTimer = setTimeout(function () { 90 | self.sendPing(self); 91 | }, self.pingInterval); 92 | 93 | self.pingTimeoutTimer = setTimeout(function () { 94 | logger.info("ping timeout trying to reconnect....") 95 | clearTimeout(self.pingIntervalTimer) 96 | self.reconnect() 97 | }, self.pingTimeout); 98 | } 99 | else { 100 | // send message to the event handler 101 | self.eh.handleMessage(message) 102 | 103 | } 104 | }); 105 | 106 | self.ws.on('close', function close(err) { 107 | logger.info("web socket closed:", err) 108 | }); 109 | 110 | self.ws.on('error', function wserror(err) { 111 | 112 | clearTimeout(self.pingIntervalTimer) 113 | clearTimeout(self.pingTimeoutTimer) 114 | logger.error('web sockect error: ', err); 115 | setTimeout(function () { 116 | self.reconnect(); 117 | }, 5000); 118 | }); 119 | }; 120 | 121 | ConnectionService.prototype.reconnect = function () { 122 | logger.info("web socket reconnecting...") 123 | var self = this 124 | clearTimeout(self.pingIntervalTimer) 125 | clearTimeout(self.pingTimeoutTimer) 126 | self.ws.reconnecting = true 127 | self.ws.close(); 128 | self.ws = new WebSocket(self.url); 129 | self.ws.reconnecting = false 130 | self.ws.binaryType = 'arraybuffer'; 131 | self.setupHandlers() 132 | } 133 | 134 | ConnectionService.prototype.sendPing = function (self) { 135 | 136 | if (self.ws.readyState == WebSocket.CLOSING || self.ws.readyState == WebSocket.CLOSED) { 137 | if (!self.ws.reconnecting) { 138 | clearTimeout(self.pingIntervalTimer); 139 | clearTimeout(self.pingTimeoutTimer); 140 | self.reconnect(); 141 | } 142 | } 143 | else { 144 | self.lastPingId = uuid.v4(); 145 | self.ws.send(JSON.stringify({ 146 | type: 'ping', 147 | id: self.lastPingId 148 | })); 149 | } 150 | 151 | } 152 | 153 | // export the class 154 | module.exports = ConnectionService; 155 | -------------------------------------------------------------------------------- /lib/EventHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by marfeder on 3/30/17. 3 | */ 4 | 5 | var logger = require('winston'); 6 | var request = require('request'); 7 | var SparkClient = require('node-sparkclient'); 8 | 9 | function EventHandler(accessToken) { 10 | var self = this; 11 | this.webHookUrl = null; 12 | this.eventCallback = null; 13 | this.accessToken = accessToken; 14 | this.sparkclient = new SparkClient(accessToken); 15 | this.sparkclient.getMe(function (err, me) { 16 | if (!err) { 17 | self.me = me; 18 | } 19 | }); 20 | } 21 | 22 | EventHandler.prototype.formatActivity = function (activity, callback) { 23 | var self = this; 24 | var event = { data: {} }; 25 | 26 | if (activity.verb == 'post' || activity.verb == 'share') { 27 | this.sparkclient.getMessage(activity.id, function (err, message) { 28 | if (!err) { 29 | event.event = "created"; 30 | event.resource = "messages"; 31 | event.data = message; 32 | callback(null, event); 33 | } 34 | else { 35 | callback(err); 36 | } 37 | }); 38 | } 39 | 40 | else if (activity.verb == 'create') { 41 | var queryParams = {}; 42 | queryParams.personId = self.me.id; 43 | this.sparkclient.listMemberships(activity.object.id, queryParams, function (err, room) { 44 | if (!err) { 45 | event.event = "created"; 46 | event.resource = "memberships"; 47 | event.data = room.items[0]; 48 | callback(null, event); 49 | } 50 | else { 51 | callback(err); 52 | } 53 | }); 54 | } 55 | 56 | else if (activity.verb == 'add' && activity.object.objectType == 'person') { 57 | var queryParams = {}; 58 | queryParams.personEmail = activity.object.emailAddress; 59 | this.sparkclient.listMemberships(activity.target.id, queryParams, function (err, room) { 60 | if (!err) { 61 | event.event = "created"; 62 | event.resource = "memberships"; 63 | event.data = room.items[0]; 64 | callback(null, event); 65 | } 66 | else { 67 | callback(err); 68 | } 69 | }); 70 | } 71 | 72 | else if (activity.verb == 'lock' || activity.verb == 'unlock' || activity.verb == 'update') { 73 | this.sparkclient.getRoom(activity.object.id, function (err, room) { 74 | if (!err) { 75 | event.resource = "rooms"; 76 | event.event = "updated"; 77 | event.data = room; 78 | callback(null, event); 79 | } 80 | else { 81 | callback(err); 82 | } 83 | }); 84 | } 85 | 86 | else { 87 | logger.debug("Uknown Event:"); 88 | logger.debug(JSON.stringify(activity, null, 2)); 89 | } 90 | } 91 | 92 | EventHandler.prototype.setEventCallback = function (callback) { 93 | this.eventCallback = callback; 94 | } 95 | EventHandler.prototype.setWebHookURL = function (url) { 96 | this.webHookUrl = url; 97 | } 98 | EventHandler.prototype.postToWebHookUrl = function (activity, callback) { 99 | var self = this; 100 | var uri = self.webHookUrl; 101 | request( 102 | { 103 | method: 'POST' 104 | , headers: { 'content-type': 'application/json;charset=UTF-8' } 105 | , uri: uri 106 | , json: activity 107 | } 108 | , function (error, response, body) { 109 | if (error) { 110 | logger.error(error); 111 | } 112 | else if (response.statusCode == 200) { 113 | if (callback) { 114 | callback(JSON.parse(body)); 115 | } 116 | } 117 | else if (response.statusCode == 204) { 118 | if (callback) { 119 | callback({ message: "success" }) 120 | } 121 | } 122 | else { 123 | logger.error(response.statusCode); 124 | logger.error(body); 125 | if (callback) { 126 | callback(body); 127 | } 128 | } 129 | } 130 | ); 131 | } 132 | 133 | EventHandler.prototype.handleMessage = function (message) { 134 | 135 | var self = this; 136 | if (message.data && message.data.eventType) { 137 | var e = message.data.eventType; 138 | var m = message.data; 139 | 140 | //logger.info(JSON.stringify(message, null, 2)) 141 | if (e == 'conversation.activity') { 142 | act = message.data.activity; 143 | self.formatActivity(act, function (err, event) { 144 | if (!err) { 145 | if (self.webHookUrl != null) { 146 | self.postToWebHookUrl(event); 147 | } 148 | if (self.eventCallback != null) { 149 | self.eventCallback(event); 150 | } 151 | } 152 | else { 153 | logger.info("Error formating event: " + err); 154 | } 155 | }); 156 | } 157 | } 158 | 159 | else { 160 | logger.info("Unknown event!"); 161 | } 162 | } 163 | 164 | // export the class 165 | module.exports = EventHandler; 166 | -------------------------------------------------------------------------------- /lib/SparkWebSocket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by marfeder on 3/30/17. 3 | */ 4 | 5 | var logger = require('winston'); 6 | const low = require('lowdb') 7 | var crypto = require('crypto'); 8 | 9 | const device_db = low('device.json') 10 | var EventHandler = require('./EventHandler'); 11 | var ConnectionService = require('./ConnectionService'); 12 | var request = require('request'); 13 | 14 | devices_table = device_db.has('devices').value(); 15 | 16 | if (!devices_table) { 17 | device_db.defaults({ devices: [] }) 18 | .write(); 19 | } 20 | 21 | 22 | function sparkwebsocket(token) { 23 | this.id = crypto.createHash('md5').update(token).digest("hex"); 24 | this.accessToken = token 25 | this.connectionService = ""; 26 | this.eventHandler = ""; 27 | this.name = 'sparkwebsocket'; 28 | 29 | } 30 | sparkwebsocket.prototype.setEventCallback = function (callback) { 31 | this.eventHandler.setEventCallback(callback) 32 | } 33 | 34 | sparkwebsocket.prototype.setWebHookURL = function (url) { 35 | this.eventHandler.setWebHookURL(url) 36 | } 37 | sparkwebsocket.prototype.connect = function (callback) { 38 | var self = this; 39 | var deviceDesc = '{"deviceName":"nodewebscoket-client","deviceType":"DESKTOP","localizedModel":"nodeJS","model":"nodeJS","name":"node-spark-client","systemName":"node-spark-client","systemVersion":"0.1"}'; 40 | var headers = { 41 | Authorization: 'Bearer ' + this.accessToken, 42 | 'Content-Type': 'application/json; charset=UTF-8' 43 | }; 44 | 45 | var devices = device_db.get('devices').find({ id: self.id }) 46 | 47 | if (devices.value()) { 48 | //delete the device...so we don't run into the 100 device limit. 49 | var device = devices.value() 50 | device_db.get('devices') 51 | .remove({ id: self.id }) 52 | .write() 53 | 54 | request( 55 | { 56 | method: 'DELETE' 57 | , headers: headers 58 | , uri: device.device.url 59 | , 'auth': { 'bearer': self.accessToken } 60 | } 61 | , function (error, response, body) { 62 | if (response.statusCode == 200) { 63 | logger.info("Deleted old device") 64 | } else { 65 | logger.info("Error deleting old device") 66 | logger.info(body) 67 | } 68 | } 69 | ) 70 | } 71 | 72 | // create a new device. 73 | request( 74 | { 75 | method: 'POST' 76 | , headers: headers 77 | , uri: 'https://wdm-a.wbx2.com/wdm/api/v1/devices' 78 | , body: deviceDesc 79 | , 'auth': { 'bearer': self.accessToken } 80 | } 81 | , function (error, response, body) { 82 | if (response.statusCode == 200) { 83 | 84 | var device = JSON.parse(body); 85 | device_db.get('devices') 86 | .push({ id: self.id, device: device }) 87 | .write() 88 | 89 | var webSocketUrl = device.webSocketUrl; 90 | var deviceUrl = device.url; 91 | 92 | self.eventHandler = new EventHandler(self.accessToken); 93 | self.connectionService = new ConnectionService(webSocketUrl, self.accessToken, self.eventHandler); 94 | callback(null, "success") 95 | } else { 96 | callback("Could Not Create Device") 97 | } 98 | } 99 | ) 100 | } 101 | 102 | 103 | module.exports = sparkwebsocket; 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ciscospark-websocket-events", 3 | "version": "1.2.0", 4 | "description": "Cisco Spark Websocket Events", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/marchfederico/ciscospark-websocket-events.git" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "Cisco", 15 | "Spark", 16 | "Websocket" 17 | ], 18 | "author": "Marcello Federico", 19 | "license": "MIT", 20 | "dependencies": { 21 | "https-proxy-agent": "^2.0.0", 22 | "lowdb": "^0.16.0", 23 | "node-sparkclient": "^0.1.4", 24 | "request": "^2.81.0", 25 | "url": "^0.11.0", 26 | "uuid": "^3.0.1", 27 | "winston": "^2.3.1", 28 | "ws": "^2.2.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/event-callback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cisco Spark WebSocket example using the event handler callback 3 | */ 4 | 5 | var accessToken = process.env.SPARK_TOKEN; 6 | if (!accessToken) { 7 | console.log("No Cisco Spark access token found in env variable SPARK_TOKEN"); 8 | process.exit(2); 9 | } 10 | 11 | var SparkWebSocket = require('../index') 12 | var logger = require('winston'); 13 | 14 | sparkwebsocket = new SparkWebSocket(accessToken); 15 | sparkwebsocket.connect(function (err, res) { 16 | if (!err) { 17 | sparkwebsocket.setEventCallback(function (event) { 18 | logger.info("New Event"); 19 | logger.info("---------"); 20 | logger.info(JSON.stringify(event, null, 2)); 21 | }) 22 | 23 | // Forward to webhookURL 24 | if (process.env.WEBHOOK_URL) { 25 | sparkwebsocket.setWebHookURL(process.env.WEBHOOK_URL); 26 | } 27 | 28 | } 29 | else { 30 | logger.info("Error starting up websocket: " + err); 31 | } 32 | }); 33 | --------------------------------------------------------------------------------