├── .prettierrc.json ├── images ├── message.png ├── missing-avatar.jpg ├── webexbotstarter.gif └── webexbotstarter_small.gif ├── .env.local ├── .editorconfig ├── .vscode └── launch.json ├── .gitignore ├── package.json ├── README.md ├── LICENSE └── index.js /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /images/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebexSamples/webex-bot-starter/HEAD/images/message.png -------------------------------------------------------------------------------- /images/missing-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebexSamples/webex-bot-starter/HEAD/images/missing-avatar.jpg -------------------------------------------------------------------------------- /images/webexbotstarter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebexSamples/webex-bot-starter/HEAD/images/webexbotstarter.gif -------------------------------------------------------------------------------- /images/webexbotstarter_small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebexSamples/webex-bot-starter/HEAD/images/webexbotstarter_small.gif -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | # Copy this file to .env and replace with live values 2 | BOTTOKEN="REPLACE_THIS_WITH_YOUR_BOT_API_KEY" 3 | 4 | # Optional: If you want to use webhooks instead of websockets uncomment the following lines 5 | # WEBHOOKURL="http://_REPLACE_THIS_WITH_NGROK_URL_.ngrok.io" 6 | # PORT=7001 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/index.js" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | #config 40 | config.json 41 | 42 | #DS Store 43 | .DS_Store 44 | 45 | #Environment Secrets 46 | .env -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Webex-Bot-Starter", 3 | "version": "1.0.0", 4 | "description": "Starter template for creating a Webex Teams bot", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm install && node --use_strict index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/tbd/webexbotstarter.git" 12 | }, 13 | "keywords": [ 14 | "Cisco", 15 | "Webex", 16 | "webex-bot-framework" 17 | ], 18 | "author": "Phil Bellanti", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/tbd/webexbotstarter/issues" 22 | }, 23 | "homepage": "https://github.com/tbd/webexbotstarter#readme", 24 | "dependencies": { 25 | "body-parser": "^1.15.2", 26 | "dotenv": "^16.0.3", 27 | "express": "^4.14.0", 28 | "webex-node-bot-framework": "^2.3.14" 29 | }, 30 | "devDependencies": { 31 | "prettier": "2.7.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webex-Bot-Starter 2 | 3 | ### Starter kit and template for a simple Webex bot 4 | 5 | _For a more detailed walkthorugh, see the [companion blog post here](https://developer.webex.com/blog/from-zero-to-webex-teams-chatbot-in-15-minutes)_ 6 | 7 | This is a very simple Webex node.JS bot application that serves as a template to be further extended. It features the [webex-node-bot-framework](https://github.com/WebexCommunity/webex-node-bot-framework) that simplifies development for Webex bots by abstracting away some of the complexity of the API calls and registering for events. 8 | 9 | Here is the bot in action: 10 | 11 | ![What we're making](./images/webexbotstarter.gif) 12 | 13 | ## Prerequisites: 14 | 15 | - [ ] node.js (minimum supported v8.0.0 & npm 2.14.12 and up) 16 | 17 | - [ ] [Sign up for Webex Developer Account](https://developer.webex.com/signup) 18 | 19 | --- 20 | 21 | ## Steps to get the bot working 22 | 23 | 1. Create a Webex bot (save the API access token and username): https://developer.webex.com/my-apps/new/bot 24 | 25 | 1. Copy the `.env.local` file to a file called `.env` 26 | 27 | 1. Edit `.env` with the following values: 28 | 29 | - BOTTOKEN - Set this to the token for your bot that you got in step 1 30 | 31 | 1. Turn on your bot server with `npm start` 32 | 33 | 1. Create a space in Webex 34 | 35 | 1. Add the bot (by its username) to the space in Webex 36 | 37 | 1. @ mention your bot with `help` to get your first response: `@mybotname help` 38 | 39 | ![First Response](./images/message.png) 40 | 41 | ## Optional: Webhooks 42 | 43 | The [webex-node-bot-framework](https://github.com/WebexCommunity/webex-node-bot-framework) supports WebSockets by default. If you would prefer your bot to operate via [Webhooks](https://developer.webex.com/docs/api/guides/webhooks), use these steps. 44 | 45 | 1. Sign up for nGrok, then connect and start it on your machine (save the port number and public web address): https://ngrok.com/download 46 | 47 | 1. After installing ngrok, run it on your local machine to get a public ip address, eg `ngrok http 3000 --region=eu` 48 | 49 | 1. Copy the ip address displayed in the ngrok window, ie: : https://1234.eu.ngrok.io 50 | 51 | 1. Edit `.env` with the following values: 52 | 53 | - PORT - Set this to the port you set when you started ngrok 54 | - WEBHOOKURL - Set this to the ip address that you copied 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CISCO SAMPLE CODE LICENSE 2 | Version 1.1 3 | Copyright (c) 2018 Cisco and/or its affiliates 4 | 5 | These terms govern this Cisco Systems, Inc. ("Cisco"), example or demo 6 | source code and its associated documentation (together, the "Sample 7 | Code"). By downloading, copying, modifying, compiling, or redistributing 8 | the Sample Code, you accept and agree to be bound by the following terms 9 | and conditions (the "License"). If you are accepting the License on 10 | behalf of an entity, you represent that you have the authority to do so 11 | (either you or the entity, "you"). Sample Code is not supported by Cisco 12 | TAC and is not tested for quality or performance. This is your only 13 | license to the Sample Code and all rights not expressly granted are 14 | reserved. 15 | 16 | 1. LICENSE GRANT: Subject to the terms and conditions of this License, 17 | Cisco hereby grants to you a perpetual, worldwide, non-exclusive, non- 18 | transferable, non-sublicensable, royalty-free license to copy and 19 | modify the Sample Code in source code form, and compile and 20 | redistribute the Sample Code in binary/object code or other executable 21 | forms, in whole or in part, solely for use with Cisco products and 22 | services. For interpreted languages like Java and Python, the 23 | executable form of the software may include source code and 24 | compilation is not required. 25 | 26 | 2. CONDITIONS: You shall not use the Sample Code independent of, or to 27 | replicate or compete with, a Cisco product or service. Cisco products 28 | and services are licensed under their own separate terms and you shall 29 | not use the Sample Code in any way that violates or is inconsistent 30 | with those terms (for more information, please visit: 31 | www.cisco.com/go/terms). 32 | 33 | 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample 34 | Code, including all intellectual property rights therein, except with 35 | respect to any third-party material that may be used in or by the 36 | Sample Code. Any such third-party material is licensed under its own 37 | separate terms (such as an open source license) and all use must be in 38 | full accordance with the applicable license. This License does not 39 | grant you permission to use any trade names, trademarks, service 40 | marks, or product names of Cisco. If you provide any feedback to Cisco 41 | regarding the Sample Code, you agree that Cisco, its partners, and its 42 | customers shall be free to use and incorporate such feedback into the 43 | Sample Code, and Cisco products and services, for any purpose, and 44 | without restriction, payment, or additional consideration of any kind. 45 | If you initiate or participate in any litigation against Cisco, its 46 | partners, or its customers (including cross-claims and counter-claims) 47 | alleging that the Sample Code and/or its use infringe any patent, 48 | copyright, or other intellectual property right, then all rights 49 | granted to you under this License shall terminate immediately without 50 | notice. 51 | 52 | 4. LIMITATION OF LIABILITY: CISCO SHALL HAVE NO LIABILITY IN CONNECTION 53 | WITH OR RELATING TO THIS LICENSE OR USE OF THE SAMPLE CODE, FOR 54 | DAMAGES OF ANY KIND, INCLUDING BUT NOT LIMITED TO DIRECT, INCIDENTAL, 55 | AND CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF USE, DATA, INFORMATION, 56 | PROFITS, BUSINESS, OR GOODWILL, HOWEVER CAUSED, EVEN IF ADVISED OF THE 57 | POSSIBILITY OF SUCH DAMAGES. 58 | 59 | 5. DISCLAIMER OF WARRANTY: SAMPLE CODE IS INTENDED FOR EXAMPLE PURPOSES 60 | ONLY AND IS PROVIDED BY CISCO "AS IS" WITH ALL FAULTS AND WITHOUT 61 | WARRANTY OR SUPPORT OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY 62 | LAW, ALL EXPRESS AND IMPLIED CONDITIONS, REPRESENTATIONS, AND 63 | WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR 64 | CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON- 65 | INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, AND ACCURACY, 66 | ARE HEREBY EXCLUDED AND EXPRESSLY DISCLAIMED BY CISCO. CISCO DOES NOT 67 | WARRANT THAT THE SAMPLE CODE IS SUITABLE FOR PRODUCTION OR COMMERCIAL 68 | USE, WILL OPERATE PROPERLY, IS ACCURATE OR COMPLETE, OR IS WITHOUT 69 | ERROR OR DEFECT. 70 | 71 | 6. GENERAL: This License shall be governed by and interpreted in 72 | accordance with the laws of the State of California, excluding its 73 | conflict of laws provisions. You agree to comply with all applicable 74 | United States export laws, rules, and regulations. If any provision of 75 | this License is judged illegal, invalid, or otherwise unenforceable, 76 | that provision shall be severed and the rest of the License shall 77 | remain in full force and effect. No failure by Cisco to enforce any of 78 | its rights related to the Sample Code or to a breach of this License 79 | in a particular situation will act as a waiver of such rights. In the 80 | event of any inconsistencies with any other terms, this License shall 81 | take precedence. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | //Webex Bot Starter - featuring the webex-node-bot-framework - https://www.npmjs.com/package/webex-node-bot-framework 2 | require("dotenv").config(); 3 | var framework = require("webex-node-bot-framework"); 4 | var webhook = require("webex-node-bot-framework/webhook"); 5 | var express = require("express"); 6 | var bodyParser = require("body-parser"); 7 | var app = express(); 8 | app.use(bodyParser.json()); 9 | app.use(express.static("images")); 10 | 11 | const config = { 12 | token: process.env.BOTTOKEN, 13 | }; 14 | 15 | // Only pass the webhook URL and port if it has been set in the environment 16 | if (process.env.WEBHOOKURL && process.env.PORT) { 17 | config.webhookUrl = process.env.WEBHOOKURL; 18 | config.port = process.env.PORT; 19 | } 20 | 21 | 22 | // init framework 23 | var framework = new framework(config); 24 | framework.start(); 25 | console.log("Starting framework, please wait..."); 26 | 27 | framework.on("initialized", () => { 28 | console.log("framework is all fired up! [Press CTRL-C to quit]"); 29 | }); 30 | 31 | // A spawn event is generated when the framework finds a space with your bot in it 32 | // If actorId is set, it means that user has just added your bot to a new space 33 | // If not, the framework has discovered your bot in an existing space 34 | framework.on("spawn", (bot, id, actorId) => { 35 | if (!actorId) { 36 | // don't say anything here or your bot's spaces will get 37 | // spammed every time your server is restarted 38 | console.log( 39 | `While starting up, the framework found our bot in a space called: ${bot.room.title}` 40 | ); 41 | } else { 42 | // When actorId is present it means someone added your bot got added to a new space 43 | // Lets find out more about them.. 44 | var msg = 45 | "You can say `help` to get the list of words I am able to respond to."; 46 | bot.webex.people 47 | .get(actorId) 48 | .then((user) => { 49 | msg = `Hello there ${user.displayName}. ${msg}`; 50 | }) 51 | .catch((e) => { 52 | console.error( 53 | `Failed to lookup user details in framwork.on("spawn"): ${e.message}` 54 | ); 55 | msg = `Hello there. ${msg}`; 56 | }) 57 | .finally(() => { 58 | // Say hello, and tell users what you do! 59 | if (bot.isDirect) { 60 | bot.say("markdown", msg); 61 | } else { 62 | let botName = bot.person.displayName; 63 | msg += `\n\nDon't forget, in order for me to see your messages in this group space, be sure to *@mention* ${botName}.`; 64 | bot.say("markdown", msg); 65 | } 66 | }); 67 | } 68 | }); 69 | 70 | // Implementing a framework.on('log') handler allows you to capture 71 | // events emitted from the framework. Its a handy way to better understand 72 | // what the framework is doing when first getting started, and a great 73 | // way to troubleshoot issues. 74 | // You may wish to disable this for production apps 75 | framework.on("log", (msg) => { 76 | console.log(msg); 77 | }); 78 | 79 | // Process incoming messages 80 | // Each hears() call includes the phrase to match, and the function to call if webex mesages 81 | // to the bot match that phrase. 82 | // An optional 3rd parameter can be a help string used by the frameworks.showHelp message. 83 | // An optional fourth (or 3rd param if no help message is supplied) is an integer that 84 | // specifies priority. If multiple handlers match they will all be called unless the priority 85 | // was specified, in which case, only the handler(s) with the lowest priority will be called 86 | 87 | /* On mention with command 88 | ex User enters @botname framework, the bot will write back in markdown 89 | */ 90 | framework.hears( 91 | "framework", 92 | (bot) => { 93 | console.log("framework command received"); 94 | bot.say( 95 | "markdown", 96 | "The primary purpose for the [webex-node-bot-framework](https://github.com/WebexCommunity/webex-node-bot-framework) was to create a framework based on the [webex-jssdk](https://webex.github.io/webex-js-sdk) which continues to be supported as new features and functionality are added to Webex. This version of the project was designed with two themes in mind: \n\n\n * Mimimize Webex API Calls. The original flint could be quite slow as it attempted to provide bot developers rich details about the space, membership, message and message author. This version eliminates some of that data in the interests of efficiency, (but provides convenience methods to enable bot developers to get this information if it is required)\n * Leverage native Webex data types. The original flint would copy details from the webex objects such as message and person into various flint objects. This version simply attaches the native Webex objects. This increases the framework's efficiency and makes it future proof as new attributes are added to the various webex DTOs " 97 | ); 98 | }, 99 | "**framework**: (learn more about the Webex Bot Framework)", 100 | 0 101 | ); 102 | 103 | /* On mention with command, using other trigger data, can use lite markdown formatting 104 | ex User enters @botname 'info' phrase, the bot will provide personal details 105 | */ 106 | framework.hears( 107 | "info", 108 | (bot, trigger) => { 109 | console.log("info command received"); 110 | //the "trigger" parameter gives you access to data about the user who entered the command 111 | let personAvatar = trigger.person.avatar; 112 | let personEmail = trigger.person.emails[0]; 113 | let personDisplayName = trigger.person.displayName; 114 | let outputString = `Here is your personal information: \n\n\n **Name:** ${personDisplayName} \n\n\n **Email:** ${personEmail} \n\n\n **Avatar URL:** ${personAvatar}`; 115 | bot.say("markdown", outputString); 116 | }, 117 | "**info**: (get your personal details)", 118 | 0 119 | ); 120 | 121 | /* On mention with bot data 122 | ex User enters @botname 'space' phrase, the bot will provide details about that particular space 123 | */ 124 | framework.hears( 125 | "space", 126 | (bot) => { 127 | console.log("space. the final frontier"); 128 | let roomTitle = bot.room.title; 129 | let spaceID = bot.room.id; 130 | let roomType = bot.room.type; 131 | 132 | let outputString = `The title of this space: ${roomTitle} \n\n The roomID of this space: ${spaceID} \n\n The type of this space: ${roomType}`; 133 | 134 | console.log(outputString); 135 | bot 136 | .say("markdown", outputString) 137 | .catch((e) => console.error(`bot.say failed: ${e.message}`)); 138 | }, 139 | "**space**: (get details about this space) ", 140 | 0 141 | ); 142 | 143 | /* 144 | Say hi to every member in the space 145 | This demonstrates how developers can access the webex 146 | sdk to call any Webex API. API Doc: https://webex.github.io/webex-js-sdk/api/ 147 | */ 148 | framework.hears( 149 | "say hi to everyone", 150 | (bot) => { 151 | console.log("say hi to everyone. Its a party"); 152 | // Use the webex SDK to get the list of users in this space 153 | bot.webex.memberships 154 | .list({ roomId: bot.room.id }) 155 | .then((memberships) => { 156 | for (const member of memberships.items) { 157 | if (member.personId === bot.person.id) { 158 | // Skip myself! 159 | continue; 160 | } 161 | let displayName = member.personDisplayName 162 | ? member.personDisplayName 163 | : member.personEmail; 164 | bot.say(`Hello ${displayName}`); 165 | } 166 | }) 167 | .catch((e) => { 168 | console.error(`Call to sdk.memberships.get() failed: ${e.messages}`); 169 | bot.say("Hello everybody!"); 170 | }); 171 | }, 172 | "**say hi to everyone**: (everyone gets a greeting using a call to the Webex SDK)", 173 | 0 174 | ); 175 | 176 | // Buttons & Cards data 177 | let cardJSON = { 178 | $schema: "http://adaptivecards.io/schemas/adaptive-card.json", 179 | type: "AdaptiveCard", 180 | version: "1.0", 181 | body: [ 182 | { 183 | type: "ColumnSet", 184 | columns: [ 185 | { 186 | type: "Column", 187 | width: "5", 188 | items: [ 189 | { 190 | type: "Image", 191 | url: "Your avatar appears here!", 192 | size: "large", 193 | horizontalAlignment: "Center", 194 | style: "person", 195 | }, 196 | { 197 | type: "TextBlock", 198 | text: "Your name will be here!", 199 | size: "medium", 200 | horizontalAlignment: "Center", 201 | weight: "Bolder", 202 | }, 203 | { 204 | type: "TextBlock", 205 | text: "And your email goes here!", 206 | size: "small", 207 | horizontalAlignment: "Center", 208 | isSubtle: true, 209 | wrap: false, 210 | }, 211 | ], 212 | }, 213 | ], 214 | }, 215 | ], 216 | }; 217 | 218 | /* On mention with card example 219 | ex User enters @botname 'card me' phrase, the bot will produce a personalized card - https://developer.webex.com/docs/api/guides/cards 220 | */ 221 | framework.hears( 222 | "card me", 223 | (bot, trigger) => { 224 | console.log("someone asked for a card"); 225 | let avatar = trigger.person.avatar; 226 | 227 | cardJSON.body[0].columns[0].items[0].url = avatar 228 | ? avatar 229 | : `${config.webhookUrl}/missing-avatar.jpg`; 230 | cardJSON.body[0].columns[0].items[1].text = trigger.person.displayName; 231 | cardJSON.body[0].columns[0].items[2].text = trigger.person.emails[0]; 232 | bot.sendCard( 233 | cardJSON, 234 | "This is customizable fallback text for clients that do not support buttons & cards" 235 | ); 236 | }, 237 | "**card me**: (a cool card!)", 238 | 0 239 | ); 240 | 241 | /* On mention reply example 242 | ex User enters @botname 'reply' phrase, the bot will post a threaded reply 243 | */ 244 | framework.hears( 245 | "reply", 246 | (bot, trigger) => { 247 | console.log("someone asked for a reply. We will give them two."); 248 | bot.reply( 249 | trigger.message, 250 | "This is threaded reply sent using the `bot.reply()` method.", 251 | "markdown" 252 | ); 253 | var msg_attach = { 254 | text: "This is also threaded reply with an attachment sent via bot.reply(): ", 255 | file: "https://media2.giphy.com/media/dTJd5ygpxkzWo/giphy-downsized-medium.gif", 256 | }; 257 | bot.reply(trigger.message, msg_attach); 258 | }, 259 | "**reply**: (have bot reply to your message)", 260 | 0 261 | ); 262 | 263 | /* On mention with command 264 | ex User enters @botname help, the bot will write back in markdown 265 | * 266 | * The framework.showHelp method will use the help phrases supplied with the previous 267 | * framework.hears() commands 268 | */ 269 | framework.hears( 270 | /help|what can i (do|say)|what (can|do) you do/i, 271 | (bot, trigger) => { 272 | console.log(`someone needs help! They asked ${trigger.text}`); 273 | bot 274 | .say(`Hello ${trigger.person.displayName}.`) 275 | // .then(() => sendHelp(bot)) 276 | .then(() => bot.say("markdown", framework.showHelp())) 277 | .catch((e) => console.error(`Problem in help hander: ${e.message}`)); 278 | }, 279 | "**help**: (what you are reading now)", 280 | 0 281 | ); 282 | 283 | /* On mention with unexpected bot command 284 | Its a good practice is to gracefully handle unexpected input 285 | Setting the priority to a higher number here ensures that other 286 | handlers with lower priority will be called instead if there is another match 287 | */ 288 | framework.hears( 289 | /.*/, 290 | (bot, trigger) => { 291 | // This will fire for any input so only respond if we haven't already 292 | console.log(`catch-all handler fired for user input: ${trigger.text}`); 293 | bot 294 | .say(`Sorry, I don't know how to respond to "${trigger.text}"`) 295 | .then(() => bot.say("markdown", framework.showHelp())) 296 | // .then(() => sendHelp(bot)) 297 | .catch((e) => 298 | console.error(`Problem in the unexepected command hander: ${e.message}`) 299 | ); 300 | }, 301 | 99999 302 | ); 303 | 304 | //Server config & housekeeping 305 | // Health Check 306 | app.get("/", (req, res) => { 307 | res.send(`I'm alive.`); 308 | }); 309 | 310 | app.post("/", webhook(framework)); 311 | 312 | var server = app.listen(config.port, () => { 313 | framework.debug("framework listening on port %s", config.port); 314 | }); 315 | 316 | // gracefully shutdown (ctrl-c) 317 | process.on("SIGINT", () => { 318 | framework.debug("stopping..."); 319 | server.close(); 320 | framework.stop().then(() => { 321 | process.exit(); 322 | }); 323 | }); 324 | --------------------------------------------------------------------------------