├── .gitignore ├── www ├── monitor.png ├── stats.png └── events.json ├── assets └── images │ └── heroku_config-variables.png ├── features ├── z-fallback.js ├── welcome.js ├── help.js ├── hello.js ├── events.js ├── roomid-phantom.js_disabled ├── emoji.js ├── survey.js └── roomkit.js ├── .vscode └── launch.json ├── package.json ├── LICENSE ├── CHANGELOG.md ├── .env.example ├── README.md └── bot.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | /.eslintrc.js 4 | /.env 5 | -------------------------------------------------------------------------------- /www/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/botkit-webex-samples/HEAD/www/monitor.png -------------------------------------------------------------------------------- /www/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/botkit-webex-samples/HEAD/www/stats.png -------------------------------------------------------------------------------- /assets/images/heroku_config-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/botkit-webex-samples/HEAD/assets/images/heroku_config-variables.png -------------------------------------------------------------------------------- /features/z-fallback.js: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Fallback Command 4 | // 5 | module.exports = function (controller) { 6 | 7 | controller.on( 'message,direct_message', async ( bot, message ) => { 8 | 9 | let markDown = `Sorry, I did not understand. \nTry: ${ controller.checkAddMention( message.roomType, 'help' ) }`; 10 | 11 | await bot.reply( message, { markdown: markDown } ); 12 | }); 13 | } -------------------------------------------------------------------------------- /.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 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/bot.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botkit-webex-samples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "bot.js", 6 | "scripts": { 7 | "start": "node bot.js" 8 | }, 9 | "dependencies": { 10 | "botbuilder-adapter-webex": "^1.0.9", 11 | "botbuilder-storage-mongodb": "^1.0.8", 12 | "botbuilder-storage-redis": "^1.0.10", 13 | "botkit": "^4.10.0", 14 | "botkit-plugin-cms": "^1.0.3", 15 | "dotenv": "^10.0.0", 16 | "jsxapi": "^5.1.0", 17 | "node-emoji": "^1.10.0", 18 | "node-fetch": "^2.6.1", 19 | "redis": "^3.1.2", 20 | "uuid": "^8.3.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /features/welcome.js: -------------------------------------------------------------------------------- 1 | // 2 | // Welcome message 3 | // sent as the bot is added to a Room 4 | // 5 | module.exports = function (controller) { 6 | 7 | controller.on( 'memberships.created', async ( bot, message ) => { 8 | 9 | // If the bot created the space, don't do anything 10 | if ( message.actorId == controller.adapter.identity.id ) return; 11 | 12 | let markDown = `Hi, I am the **${ controller.adapter.identity.displayName }** bot! \n` 13 | markDown += 'Type `help` to learn more about my skills. '; 14 | 15 | if ( message.data.roomType == 'group' ) { 16 | 17 | markDown += `\n_Note that this is a "group" space.\n I will answer only if mentioned! \n` 18 | markDown += `For help, enter: ${ controller.checkAddMention( message.data.roomType, 'help' ) }_` 19 | } 20 | 21 | await bot.reply( message, { markdown : markDown} ); 22 | }); 23 | } -------------------------------------------------------------------------------- /features/help.js: -------------------------------------------------------------------------------- 1 | // 2 | // Command: help 3 | // 4 | module.exports = function (controller) { 5 | 6 | controller.hears( 'help', 'message,direct_message', async ( bot, message ) => { 7 | 8 | let markDown = '**Available commands:** \n'; 9 | 10 | controller.commandHelp.sort( ( a,b ) => { 11 | 12 | return ( ( a.command < b.command ) ? -1 : ( ( a.command > b.command ) ? 1 : 0 )); 13 | }); 14 | 15 | controller.commandHelp.forEach( element => { 16 | 17 | markDown += `**${ controller.checkAddMention( message.roomType, element.command ) }**: ${ element.text } \n` 18 | }); 19 | 20 | await bot.reply( message, { markdown: markDown } ); 21 | 22 | // text += "\n- " + bot.appendMention(message, "storage") + ": store picked color as a user preference"; 23 | }); 24 | 25 | controller.commandHelp.push( { command: 'help', text: 'Show available commands/descriptions' } ); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cisco DevNet 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 | -------------------------------------------------------------------------------- /features/hello.js: -------------------------------------------------------------------------------- 1 | // 2 | // Respond to various 'hello' words, attach file by URL and from local file system 3 | var fs = require('fs'); 4 | 5 | module.exports = function( controller ) { 6 | 7 | controller.hears( [ 'hi','hello','howdy','hey','aloha','hola','bonjour','oi' ], 'message,direct_message', async ( bot,message ) => { 8 | 9 | await bot.reply( message,'Greetings!' ); 10 | await bot.reply( message, { markdown: 'Try `help` to see available commands' } ); 11 | }); 12 | 13 | controller.hears( 'url', 'message,direct_message', async ( bot,message ) => { 14 | 15 | await bot.reply( message, { 16 | text: 'Aww!', 17 | files: [ 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Youngkitten.JPG/220px-Youngkitten.JPG' ] 18 | }); 19 | }) 20 | 21 | controller.hears( 'local', 'message,direct_message' , async ( bot,message ) => { 22 | await bot.reply( message, { 23 | text: 'The source code', 24 | files: [ fs.createReadStream( './bot.js' ) ] 25 | }) 26 | }) 27 | 28 | controller.commandHelp.push( { command: 'hello', text: 'Greetings!' } ); 29 | controller.commandHelp.push( { command: 'url', text: 'Attach a file via URL' } ); 30 | controller.commandHelp.push( { command: 'local', text: 'Attach a file from the local file system' } ); 31 | 32 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | **v0.5.0 (2020-03-06): Align with updated botkit-template** 4 | - Remove template/ sample - moved to CiscoDevNet/botkit-template 5 | - Remove learninglab/ sample - lab is now based on botkit-template 6 | - Emoji sample - convert to feature/update for Botkit 0.7 7 | - Emoji sample - remove unsupported websocket functionality (now a supported part of Webex JS SDK and botkit-template) 8 | - Use .env better for config/secrets 9 | - roomid-phantom.js - convert to a feature, rename as '*_disabled', update readme with caveats 10 | - redis/ - remove this sample as Botkit storage usage has changed focus 11 | - externalapi/ - convert to feature `events.js`; host the JSON data API on the local Botkit web server 12 | - roomkit/ - convert to a feature `roomkit.js`; add xStatus CLI command; remove dynamic connection 13 | - disturbed/ - removed; Botkit no longer supports timeouts in conversations 14 | 15 | 16 | **v0.4.0 (2018-12-17): Botkit framework update** 17 | - uses Botkit latest (v0.7.0) 18 | 19 | **v0.3.0 (2018-05-17): Webex Teams rebrand** 20 | - changing to new 'convos' code in the Botkit samples (after Webex rebrand) 21 | - uses Botkit latest (v0.6.14) 22 | - fixed issues when 'convo.repeat()' needs a pre-pended message (several skills involved) 23 | - fixed issue in 'timeout' skill 24 | - added 'storage' skill to demo Botkit capability to store data (in-memory or to a backend) 25 | - tested also with "github:ObjectIsAdvantag/botkit#non-breaking" 26 | - tip: `npm install --save ObjectIsAdvantag/botkit#non-breaking` 27 | 28 | -------------------------------------------------------------------------------- /features/events.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /* 7 | * Structure of an event 8 | * 9 | * { 10 | "id": "CodeMotionMilan2016", 11 | "name": "Code Motion Milan", 12 | "url": "https://communities.cisco.com/events/1875", 13 | "description": "What is it? Codemotion is one of the biggest tech conferences in EMEA for software developers, with an international network of 40.000 developers and 2,000 speakers. What are you waiting for to be a Codemotioner?", 14 | "beginDate": "2016-11-25T07:30:00.000Z", 15 | "beginDay": "Nov 25", 16 | "beginDayInWeek": "friday", 17 | "beginTime": "9:30AM", 18 | "endDate": "2016-11-26T16:00:00.000Z", 19 | "endDay": "Nov 26", 20 | "endDayInWeek": "saturday", 21 | "endTime": "6:00PM",public_url 22 | "category": "conference", 23 | "country": "Italy", 24 | "city": "Milan", 25 | "location_url": "http://milan2016.codemotionworld.com/" 26 | } 27 | */ 28 | 29 | const { BotkitConversation } = require( 'botkit' ); 30 | const fetch = require( 'node-fetch' ); 31 | 32 | module.exports = async function (controller) { 33 | 34 | controller.hears( 'events', 'message,direct_message', async ( bot, message ) => { 35 | await fetch( `http://localhost:${ process.env.PORT }/www/events.json`) 36 | .then(res => res.json()) 37 | .then( async json => { 38 | 39 | var nb = json.length; 40 | 41 | var msg = `**${ nb } events found:**\n`; 42 | 43 | for (var i = 0; i < nb; i++) { 44 | var current = json[i]; 45 | msg += `\n ${ i+1 }. `; 46 | msg += `${ current.beginDay } - ${ current.endDay }: [${ current.name }](${ current.url }), ${ current.city } (${ current.country })`; 47 | } 48 | 49 | await bot.reply( message, { markdown: msg } ); 50 | }) 51 | }); 52 | 53 | controller.commandHelp.push( { command: 'events', text: 'Retrieve DevNet event details from an HTTP API providing JSON data' } ); 54 | 55 | } 56 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 2 | # Store your bot settings in the variables below 3 | # or via environment variables of the same name 4 | # 5 | # DO NOT PUT SECRETS IN THIS FILE IF YOU'RE PUSHING THE CODE TO EXTERNAL REPOS 6 | # 7 | # - you can reference these variables from your code with process.env.SECRET for example 8 | # 9 | # - note that ".env" is formatted like a shell file, so you may (depending on your platform) need to add double quotes if strings contains spaces 10 | # 11 | 12 | # Webex Teams bot account access token 13 | 14 | WEBEX_ACCESS_TOKEN= 15 | 16 | # Internet facing URL where your bot can be reached for incoming webhooks or 17 | # HTTP web server requests. 18 | # Can be empty if WEBSOCKET_EVENTS is True and the Botkit web server is 19 | # not needed, e.g. if not serving images related to the buttons & cards feature 20 | # or the health check URL 21 | 22 | PUBLIC_URL= 23 | 24 | # Use websockets instead of webhooks for incoming Webex events (True/False) 25 | 26 | WEBSOCKET_EVENTS=True 27 | 28 | # Local port where your bot will be listening 29 | 30 | PORT=3000 31 | 32 | # Webex JavaScript SDK debug level 33 | # Values: error, warn, info, debug, trace 34 | 35 | WEBEX_LOG_LEVEL=error 36 | 37 | # Storage options - enable only one option (default is in-memory only) 38 | 39 | # Redis storage URL - enable to use Redis for Botkit conversation persistence/scalability 40 | 41 | REDIS_URL= 42 | 43 | # MongoDB storage URL - enable to use MongoDB for Botkit conversation persistence/scalability 44 | 45 | MONGO_URI= 46 | 47 | # Botkit CMS credentials 48 | 49 | CMS_URI= 50 | CMS_TOKEN= 51 | 52 | # Node environment setting 53 | # (development | production ) 54 | 55 | NODE_ENV=development 56 | 57 | # Bot meta info - displayed when browsing the bot healthcheck/public URL 58 | 59 | PLATFORM=Webex 60 | CODE=https://github.com/CiscoDevNet/botkit-template 61 | OWNER= 62 | SUPPORT= 63 | 64 | # Configurations for the roomkit.js feature 65 | 66 | ROOM_DEVICE_ADDRESS=10.10.20.157 67 | ROOM_DEVICE_USER=admin 68 | ROOM_DEVICE_PASSWORD=ciscopsdt 69 | 70 | # If the below is configured, access to the roomkit.js feature will be 71 | # restricted to the indicated User 72 | 73 | ROOM_DEVICE_AUTHORIZED_USER= 74 | 75 | # If the below is configured, access to the roomkit.js feature will be 76 | # restricted to requests originating from the indicated Space 77 | 78 | ROOM_DEVICE_AUTHORIZED_SPACE= 79 | 80 | # Configuration for the survey.js feature 81 | 82 | SURVEY_RESULTS_SPACE= -------------------------------------------------------------------------------- /features/roomid-phantom.js_disabled: -------------------------------------------------------------------------------- 1 | // When added to a space, this bot collects the space title and id 2 | // then creates a new space with the user that added the bot to the original 3 | // space. 4 | 5 | // In the new space, the room title and id are reported. The bot then immediately leaves 6 | // the original space. 7 | 8 | // Note, this feature is disabled by default (simply by adding '_disabled' to the file name) 9 | // to avoid confusion (adding the bot to a space only to have it immediately leave!) 10 | // To enable this feature, simply remove '_disabled' from the filename and restart the bot 11 | 12 | module.exports = function ( controller ) { 13 | 14 | controller.on('memberships.created', async( bot, message ) => { 15 | 16 | let membershipId = message.data.id; 17 | let roomId = message.data.roomId; 18 | let roomTitle = await bot.api.rooms.get( roomId ) 19 | .then( ( room ) => { 20 | return room.title; 21 | }); 22 | 23 | // If the bot created the space, don't do anything 24 | if ( message.actorId == controller.adapter.identity.id ) return; 25 | 26 | await bot.api.memberships.remove( membershipId ); 27 | 28 | // startPrivateConversation seems to be broken at the moment 29 | // https://github.com/howdyai/botkit/issues/1913 30 | 31 | // await bot.startPrivateConversation( userId ) 32 | // .then( () => { 33 | // let info = 'Info for the Space where you just added me: \n'; 34 | // info += `**Title:** ${ roomTitle } \n`; 35 | // info += `**Id:** ${ roomId } \n`; 36 | // bot.say( { markdown: info } ); 37 | // } ); 38 | 39 | // Instead create a regular group room, with just the two of us 40 | let room = await bot.api.rooms.create( { title: `Id for: ${ roomTitle }` } ); 41 | 42 | // Add user as member (bot is automatically added) 43 | await bot.api.memberships.create( { 44 | roomId: room.id, 45 | personId: message.actorId, 46 | }); 47 | 48 | // Start a Botkit conversation context in the space 49 | await bot.startConversationInRoom( room.id, message.actorId ); 50 | 51 | let info = 'Info for the room I was just added to: \n'; 52 | info += `**Title:** \`${ roomTitle }\` \n`; 53 | info += `**Room Id:** \`${ roomId }\` \n`; 54 | info += '_(removing myself from the room...)_'; 55 | 56 | await bot.say( { markdown: info } ) 57 | 58 | } ) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /features/emoji.js: -------------------------------------------------------------------------------- 1 | const { BotkitConversation } = require( 'botkit' ); 2 | 3 | const emoji = require('node-emoji'); 4 | 5 | module.exports = async function (controller) { 6 | 7 | const convo = new BotkitConversation( 'emoji_chat', controller ); 8 | 9 | let question = 'Hi, I am the Emoji bot!\n'; 10 | question += '* Type any sentence with an emoji tag in it (like: "`I :heart: U`") to see me in action.\n'; 11 | question += '* You can also use "`find {keyword}`" and "`random`" commands'; 12 | 13 | convo.ask( { channelData: { markdown: question } }, [ 14 | { 15 | pattern: 'random', 16 | handler: async ( response, convo, bot ) => { 17 | let random = emoji.random(); 18 | let answer = 'Here\'s a random emoji tag and representation:'; 19 | answer += `Type: "\`:${random.key}:\`", to get: ${random.emoji}`; 20 | await bot.say( { markdown: answer } ); 21 | } 22 | }, 23 | { 24 | pattern: '(?:find|search)\s*(.*)', 25 | handler: async ( response, convo, bot ) => { 26 | var keyword = response.split( ' ', 2 )[ 1 ]; 27 | if ( !keyword ) { 28 | await bot.say( { markdown: 'Find what, exactly..? Try: "`find smiley`"' } ); 29 | return; 30 | } 31 | 32 | var found = emoji.search( keyword ); 33 | 34 | switch ( found.length ) { 35 | 36 | case 0: 37 | await bot.say( `Sorry, no match. Try again...${ emoji.get('persevere') }` ); 38 | return; 39 | 40 | case 1: 41 | await bot.say( { 42 | markdown: `Type "\`:${ found[0].key }:\`" for ${ found[0].emoji }` 43 | } ) 44 | return; 45 | 46 | default: 47 | var max = ( found.length < 3 ) ? found.length : 3; 48 | var response = `Found ${ found.length }, showing ${ max } \n`; 49 | for ( i = 0; i < max; i++ ) { 50 | response += `* Type "\`:${ found[ i ].key }:\`" for ${ found[ i ].emoji } \n`; 51 | } 52 | await bot.say( { markdown: response } ); 53 | return; 54 | } 55 | } 56 | }, 57 | { 58 | default: true, 59 | handler: async ( response, convo, bot ) => { 60 | await bot.say( `Translation: ${ emoji.emojify( response ) }` ); 61 | } 62 | }, 63 | ]); 64 | 65 | controller.addDialog( convo ); 66 | 67 | controller.hears( 'emoji', 'message,direct_message', async ( bot, message ) => { 68 | await bot.beginDialog( 'emoji_chat' ); 69 | }); 70 | 71 | controller.commandHelp.push( { 72 | command: 'emoji', 73 | text: 'Converts emoji tags into unicode characters and returns the "emojified" phrase' 74 | } ); 75 | 76 | } 77 | -------------------------------------------------------------------------------- /features/survey.js: -------------------------------------------------------------------------------- 1 | // Threaded conversation illustrating a survey. 2 | // Responses are posted into the Webex Teams Space 3 | // configured via SURVEY_SPACE (the bot must be a member) 4 | // or simply back into the Space the survey was submitted from 5 | // if SURVEY_RESULTS_SPACE is empty 6 | 7 | const { BotkitConversation } = require( 'botkit' ); 8 | 9 | module.exports = function ( controller ) { 10 | 11 | const convo = new BotkitConversation( 'survey_chat', controller ); 12 | 13 | convo.before( 'default', async ( convo, bot ) => { 14 | 15 | if ( !convo.vars.survey_space ) { 16 | 17 | convo.setVar( 'survey_space', 18 | process.env.SURVEY_RESULTS_SPACE ? process.env.SURVEY_RESULTS_SPACE : convo.vars.channel ); 19 | 20 | if ( !process.env.SURVEY_RESULTS_SPACE ) { 21 | await convo.gotoThread( 'survey_warn_space' ); 22 | } 23 | } 24 | } ); 25 | 26 | let question = '(Survey) Please enter the session for which you\'d like to provide feedback \n'; 27 | question += '_Available sessions: DEVNET-1808 / DEVNET-1871 / DEVNET-2071 / DEVNET-2074 / DEVNET-2896_'; 28 | 29 | convo.ask( { channelData: { markdown: question } }, [ 30 | { 31 | pattern: '^DEVNET-1808|DEVNET-1871|DEVNET-2071|DEVNET-2074|DEVNET-2896&', 32 | handler: async ( response, convo ) => { 33 | await convo.gotoThread( 'survey_confirm' ); 34 | } 35 | }, 36 | { 37 | default: true, 38 | handler: ( async ( response, convo ) => { 39 | await convo.gotoThread( 'survey_cancel' ); 40 | }) 41 | } 42 | ], 'survey_session_id' ); 43 | 44 | convo.addMessage( { 45 | text: '(Survey) No survey results Space configured; using current Space', 46 | action: 'default' 47 | }, 'survey_warn_space'); 48 | 49 | convo.addMessage( { 50 | text: '(Survey) Unrecognized session Id...', 51 | action: 'default' 52 | }, 'survey_cancel' ); 53 | 54 | let rate = `How would you rate this session? \n`; 55 | rate += '_Options: 1|poor, 2|weak, 3|adequate, 4|good, 5|great_ \n'; 56 | rate += '_(or provide your own free-form response!)_'; 57 | 58 | convo.addQuestion( 59 | { channelData: { markdown: rate } }, 60 | async ( response, convo ) => { 61 | 62 | await convo.gotoThread( 'survey_submit' ); 63 | }, 64 | 'survey_rating', 65 | 'survey_confirm' ); 66 | 67 | convo.before( 'survey_submit', async ( convo, bot) => { 68 | 69 | let result = ''; 70 | 71 | await bot.api.messages.create( { 72 | roomId: convo.vars.survey_space, 73 | text: `(Survey) Session ${ convo.vars.survey_session_id } was rated: ${ convo.vars.survey_rating }` 74 | } ) 75 | .then( async () => { 76 | convo.setVar('survey_result', 'Thanks for providing your feedback!') 77 | } ) 78 | .catch( async ( err ) => { 79 | convo.setVar('survey_result', `Error submitting results: ${err.body.message}`) 80 | } ) 81 | } ); 82 | 83 | convo.addMessage( { 84 | text: '(Survey) {{vars.survey_result}}', 85 | action: 'complete' 86 | }, 'survey_submit' ) 87 | 88 | controller.addDialog( convo ); 89 | 90 | controller.hears( 'survey', 'message,direct_message', async ( bot, message ) => { 91 | 92 | await bot.beginDialog( 'survey_chat' ); 93 | }); 94 | 95 | controller.commandHelp.push( { command: 'survey', text: 'Ask a survey question, post results to a Webex Teams Space' } ); 96 | 97 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # botkit-webex-samples 2 | 3 | This project implements a Botkit + Webex Teams adapter bot, based on the DevNet [botkit-template](https://www.github.com/CiscoDevNet/botkit-template) project, providing some additional interesting samples and examples: 4 | 5 | - `emoji.js`- Converts emoji tags into unicode characters and returns the "emojified" phrase 6 | 7 | - `events.js` - Retrieve/display DevNet event details from an HTTP REST API providing JSON data 8 | 9 | - `roomid-phantom.js` - Helpful utility bot; when added to a room, it creates a separate space with the requestor and outputs the roomId of the original room 10 | 11 | - `roomkit.js` - Interact with a Cisco room device via xAPI/jsxapi. Query the device's 'PeopleCount' function, or execute an ad hoc 'xStatus' CLI command 12 | 13 | - `survey.js` - Implements a basic survey, posting survey data into a cloud service (i.e. Webex Teams) via an external REST API 14 | 15 | ## Websockets vs. Webhooks 16 | 17 | Most Botkit features can be implemented by using the Webex Teams JS SDK websockets functionality, which establishes a persistent connection to the Webex Teams cloud for outbound and inbound messages/events. 18 | 19 | Webex Teams also supports traditional HTTP webhooks for messages/events, which requires that your bot be accessible via a publically reachable URL. A public URL is also needed if your bot will be serving any web pages/files, e.g. images associated with the cards and buttons feature or the health check URL. 20 | 21 | - If you don't need to serve buttons and cards images, you can set the environment variable `WEBSOCKET_EVENTS=True` and avoid the need for a public URL 22 | - If you are implementing buttons & cards, you will need a public URL (e. g. by using a service like Ngrok, or hosting your bot in the cloud) - configure this via the `PUBLIC_URL` environment variable 23 | 24 | ## How to run (local machine) 25 | 26 | Assuming you plan to us [ngrok](https://ngrok.com) to give your bot a publically available URL (optional, see above), you can run this template in a jiffy: 27 | 28 | 1. Clone this repo: 29 | 30 | ```sh 31 | git clone https://github.com/CiscoDevNet/botkit-webex-samples.git 32 | 33 | cd botkit-webex-samples 34 | ``` 35 | 36 | 1. Install the Node.js dependencies: 37 | 38 | ```sh 39 | npm install 40 | ``` 41 | 42 | 1. Create a Webex Teams bot account at ['Webex for Developers'](https://developer.webex.com/my-apps/new/bot), and note/save your bot's access token 43 | 44 | 1. Launch Ngrok to expose port 3000 of your local machine to the internet: 45 | 46 | ```sh 47 | ngrok http 3000 48 | ``` 49 | 50 | Note/save the 'Forwarding' HTTPS address that ngrok generates 51 | 52 | 1. Rename the `env.example` file to `.env`, then edit to configure the settings and info for your bot. Individual features included in this project may need specific configurations in `.env` (see the comments at the top of each feature `.js` file for details.) 53 | 54 | >Note: you can also specify any of these settings via environment variables (which will take precedent over any settings configured in the `.env` file)...often preferred in production environments 55 | 56 | To successfully run all of the sample features, you'll need to specify at minimum a `WEBEX_ACCESS_TOKEN` (Webex Teams bot access token), and either a `PUBLIC_URL` or enable `WEBSOCKET_EVENTS`. 57 | 58 | >Note: If running on Glitch.me or Heroku (with [Dyno Metadata](https://devcenter.heroku.com/articles/dyno-metadata) enbaled), the `PUBLIC_URL` will be auto-configured 59 | 60 | Additional values in the `.env` file (like `OWNER` and `CODE`) are used to populate the healthcheck URL meta-data. 61 | 62 | Be sure to save the `.env` file! 63 | 64 | 1. You're ready to run your bot: 65 | 66 | ```sh 67 | node bot.js 68 | ``` 69 | 70 | ## Quick start on Glitch.me 71 | 72 | * Click [![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/import/github/CiscoDevNet/botkit-template) 73 | 74 | * Open the `.env` file, then uncomment the `WEBEX_ACCESS_TOKEN` variable and paste in your bot's access token 75 | 76 | **Optional**: enter appropirate info in the "Bot meta info..." section 77 | 78 | >Note that thanks to Glitch `PROJECT_DOMAIN` env variable, you do not need to add a `PUBLIC_URL` variable pointing to your app domain 79 | 80 | You bot is all set, responding in 1-1 and 'group' spaces, and sending a welcome message when added to a space! 81 | 82 | You can verify the bot is up and running by browsing to its healthcheck URL (i.e. the app domain.) 83 | 84 | ## Quick start on Heroku 85 | 86 | * Create a new project pointing to this repo. 87 | 88 | * Open your app settings, view your config variables, and add a `WEBEX_ACCESS_TOKEN` variable with your bot's access token as value. 89 | 90 | * Unless your app is using [Dyno Metadata](https://devcenter.heroku.com/articles/dyno-metadata), you also need to add a PUBLIC_URL variable pointing to your app domain. 91 | 92 | ![](assets/images/heroku_config-variables.png) 93 | 94 | You bot is all set, responding in 1-1 and 'group' spaces, and sending a welcome message when added to a space! 95 | 96 | You can verify the bot is up and running by browsing to its healthcheck URL (i.e. the app domain.) -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Cisco and/or its affiliates. 2 | // Permission is hereby granted, free of charge, to any person obtaining a copy 3 | // of this software and associated documentation files (the "Software"), to deal 4 | // in the Software without restriction, including without limitation the rights 5 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | // copies of the Software, and to permit persons to whom the Software is 7 | // furnished to do so, subject to the following conditions: 8 | // The above copyright notice and this permission notice shall be included in all 9 | // copies or substantial portions of the Software. 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | // SOFTWARE. 17 | // __ __ ___ ___ 18 | // |__) / \ | |__/ | | 19 | // |__) \__/ | | \ | | 20 | 21 | // This is the main file for the template bot. 22 | 23 | // Load process.env values from .env file 24 | require('dotenv').config(); 25 | 26 | if (!process.env.WEBEX_ACCESS_TOKEN) { 27 | console.log( '\n-->Token missing: please provide a valid Webex Teams user or bot access token in .env or via WEBEX_ACCESS_TOKEN environment variable'); 28 | process.exit(1); 29 | } 30 | 31 | // Read public URL from env, 32 | // if not specified, try to infer it from public cloud platforms environments 33 | var public_url = process.env.PUBLIC_URL; 34 | 35 | if (!public_url) { 36 | // Heroku hosting: available if dyno metadata are enabled, https://devcenter.heroku.com/articles/dyno-metadata 37 | if (process.env.HEROKU_APP_NAME) { 38 | public_url = 'https://' + process.env.HEROKU_APP_NAME + '.herokuapp.com'; 39 | } 40 | 41 | // Glitch hosting 42 | if (process.env.PROJECT_DOMAIN) { 43 | public_url = 'https://' + process.env.PROJECT_DOMAIN + '.glitch.me'; 44 | } 45 | } 46 | 47 | var storage; 48 | 49 | if (process.env.REDIS_URL) { 50 | 51 | const redis = require('redis'); 52 | const { RedisDbStorage } = require('botbuilder-storage-redis'); 53 | 54 | // Initialize redis client 55 | const redisClient = redis.createClient(process.env.REDIS_URL, { prefix: 'bot-storage:' }); 56 | storage = new RedisDbStorage(redisClient); 57 | } 58 | 59 | if (process.env.MONGO_URI) { 60 | 61 | const { MongoDbStorage } = require('botbuilder-storage-mongodb'); 62 | 63 | storage = new MongoDbStorage({ url: process.env.MONGO_URI }) 64 | } 65 | 66 | // Create Webex Adapter 67 | const { v4: uuidv4 } = require( 'uuid' ); 68 | const { WebexAdapter } = require('botbuilder-adapter-webex'); 69 | 70 | // If PUBLIC_URL not configured, supply a dummy pulic_address 71 | // If using websockets, don't supply a secret 72 | const adapter = new WebexAdapter({ 73 | 74 | access_token: process.env.WEBEX_ACCESS_TOKEN, 75 | public_address: public_url ? public_url : 'http://127.0.0.1', 76 | secret: ( process.env.WEBSOCKET_EVENTS == 'True' ) ? null : uuidv4() 77 | }); 78 | 79 | const { Botkit } = require('botkit'); 80 | 81 | const controller = new Botkit({ 82 | 83 | webhook_uri: '/api/messages', 84 | adapter: adapter, 85 | storage 86 | }); 87 | 88 | // Create Botkit controller 89 | 90 | if (process.env.CMS_URI) { 91 | const { BotkitCMSHelper } = require('botkit-plugin-cms'); 92 | controller.usePlugin(new BotkitCMSHelper({ 93 | uri: process.env.CMS_URI, 94 | token: process.env.CMS_TOKEN 95 | })); 96 | }; 97 | 98 | // Express response stub to supply to processWebsocketActivity 99 | // Luckily, the Webex adapter doesn't do anything meaningful with it 100 | class responseStub { 101 | status(){} 102 | end(){} 103 | } 104 | 105 | function processWebsocketActivity( event ) { 106 | // Express request stub to fool the Activity processor 107 | let requestStub = {}; 108 | // Event details are expected in a 'body' property 109 | requestStub.body = event; 110 | 111 | // Hand the event off to the Botkit activity processory 112 | controller.adapter.processActivity( requestStub, new responseStub, controller.handleTurn.bind( controller ) ) 113 | } 114 | 115 | // Once the bot has booted up its internal services, you can use them to do stuff 116 | controller.ready( async () => { 117 | 118 | const path = require('path'); 119 | 120 | // load developer-created custom feature modules 121 | controller.loadModules( path.join( __dirname, 'features' ) ); 122 | 123 | if ( ( !public_url ) && ( process.env.WEBSOCKET_EVENTS !== 'True' ) ) { 124 | console.log( '\n-->No inbound event channel available. Please configure at least one of PUBLIC_URL and/or WEBSOCKET_EVENTS' ); 125 | process.exit( 1 ); 126 | } 127 | 128 | if ( public_url ) { 129 | // Make the app public_url available to feature modules, for use in adaptive card content links 130 | controller.public_url = public_url; 131 | } 132 | 133 | if ( process.env.WEBSOCKET_EVENTS == 'True' ) { 134 | 135 | await controller.adapter._api.memberships.listen(); 136 | controller.adapter._api.memberships.on( 'created', ( event ) => processWebsocketActivity( event ) ); 137 | controller.adapter._api.memberships.on( 'updated', ( event ) => processWebsocketActivity( event ) ); 138 | controller.adapter._api.memberships.on( 'deleted', ( event ) => processWebsocketActivity( event ) ); 139 | 140 | await controller.adapter._api.messages.listen(); 141 | controller.adapter._api.messages.on('created', ( event ) => processWebsocketActivity( event ) ); 142 | controller.adapter._api.messages.on('deleted', ( event ) => processWebsocketActivity( event ) ); 143 | 144 | await controller.adapter._api.attachmentActions.listen(); 145 | controller.adapter._api.attachmentActions.on('created', ( event ) => processWebsocketActivity( event ) ); 146 | 147 | // Remove unnecessary auto-created webhook subscription 148 | await controller.adapter.resetWebhookSubscriptions(); 149 | 150 | console.log( 'Using websockets for incoming messages/events'); 151 | } 152 | else { 153 | // Register attachmentActions webhook 154 | controller.adapter.registerAdaptiveCardWebhookSubscription( controller.getConfig( 'webhook_uri' ) ); 155 | } 156 | } ); 157 | 158 | controller.publicFolder( '/www', __dirname + '/www' ); 159 | 160 | controller.webserver.get( '/', ( req, res ) => { 161 | 162 | res.send( JSON.stringify( controller.botCommons, null, 4 ) ); 163 | } ); 164 | 165 | let healthCheckUrl = public_url ? public_url : `http://localhost:${ process.env.PORT }`; 166 | 167 | console.log( `Health check available at: ${ healthCheckUrl }` ); 168 | 169 | controller.commandHelp = []; 170 | 171 | controller.checkAddMention = function ( roomType, command ) { 172 | 173 | var botName = adapter.identity.displayName; 174 | 175 | if (roomType === 'group') { 176 | 177 | return `\`@${botName} ${command}\`` 178 | } 179 | 180 | return `\`${command}\`` 181 | } 182 | -------------------------------------------------------------------------------- /features/roomkit.js: -------------------------------------------------------------------------------- 1 | // Please configure options for this feature in the 'Configurations for the roomkit.js feature' 2 | // section of the .env file 3 | 4 | const jsxapi = require( 'jsxapi' ); 5 | 6 | const { BotkitConversation } = require( 'botkit' ); 7 | 8 | module.exports = function ( controller ) { 9 | 10 | // Object to store the jsxapi API cbject/connection and device details 11 | // Provides a connect() function to manage connecting to the target device 12 | var roomkit = { 13 | state: 'disconnected', 14 | connect: function ( address, user, password ) { 15 | 16 | return new Promise( ( resolve, reject ) => { 17 | 18 | // Empty passwords are supported 19 | if ( !( address && user ) ) reject( 'Roomkit: Unable to connect: Missing address or user' ); 20 | 21 | // Use jsxapi to connect to the device; store the xApi object on roomkit 22 | this.xapi = jsxapi.connect( `ssh://${ address }`, { username: user, password: password } ) 23 | .on( 'error', err => { 24 | 25 | this.state = 'disconnected'; 26 | reject( `Error attempting connection to ${ address }: ${ err }` ); 27 | 28 | } ) 29 | .on( 'ready', () => { 30 | 31 | this.state = 'connected'; 32 | this.address = address; 33 | this.user = user; 34 | resolve( ); 35 | } ) 36 | } ) 37 | } 38 | } 39 | 40 | controller.hears( 'roomkit', 'message,direct_message', async ( bot, message ) => { 41 | 42 | // Retrieve the Webex email of the requesting user 43 | let requestor = await bot.api.people.get( message.user ).then( person => { return person.emails[ 0 ] } ); 44 | 45 | // If a restricted User is configured, check to see if it matches the requesting User 46 | if ( process.env.ROOM_DEVICE_AUTHORIZED_USER && 47 | ( requestor != process.env.ROOM_DEVICE_AUTHORIZED_USER ) ) { 48 | 49 | await bot.reply( message, { markdown: `(Roomkit) \`${ requestor }\` is not authorized for this feature` } ); 50 | return; 51 | } 52 | 53 | // If a restrcited Space is configured, check to see if the request is coming from that Space 54 | if ( process.env.ROOM_DEVICE_AUTHORIZED_SPACE && 55 | ( message.roomId != process.env.ROOM_DEVICE_AUTHORIZED_SPACE ) ) { 56 | 57 | await bot.reply( message, { markdown: `(Roomkit) Users in this Space are not authorized for this feature` } ); 58 | return; 59 | } 60 | 61 | // If xApi is not already connected, and if a device address is configured... 62 | if ( roomkit.state != 'connected' ) { 63 | 64 | if ( process.env.ROOM_DEVICE_ADDRESS ) { 65 | 66 | await bot.reply( message, `(Roomkit) Connecting to device: ${ process.env.ROOM_DEVICE_ADDRESS }...` ); 67 | 68 | // Attempt to connect to the device 69 | await roomkit.connect( 70 | process.env.ROOM_DEVICE_ADDRESS, 71 | process.env.ROOM_DEVICE_USER, 72 | process.env.ROOM_DEVICE_PASSWORD 73 | ).then( ) 74 | // Handle any errors connecting 75 | .catch( async ( err ) => { 76 | // This appears to be necessary to ensure the Botkit worker is in the 77 | // correct conversation context 78 | await bot.changeContext( message.reference ); 79 | 80 | await bot.reply( message, `(Roomkit) ${ err }` ); 81 | }) 82 | } 83 | } 84 | 85 | // This appears to be necessary to ensure the Botkit worker is in the 86 | // correct conversation context 87 | await bot.changeContext( message.reference ); 88 | 89 | // If the connection was successful, or perhaps already existed from a previous interaction... 90 | if ( roomkit.state == 'connected' ) { 91 | 92 | await bot.reply( message, `(Roomkit) Device connection ready: ${ roomkit.user }@${ roomkit.address }`); 93 | 94 | let question = '**Commands available:** \n'; 95 | question += '* **peoplecount**: query the device\'s face-counting feature \n'; 96 | question += '* **xstatus {path}**: execute a T-Shell xStatus command for the given path \n'; 97 | 98 | await bot.reply( message, { channelData: { markdown: question } } ); 99 | } 100 | else { 101 | 102 | await bot.reply( message, '(Roomkit) unable to connect to a device (check .env config). Exiting...' ); 103 | return; 104 | } 105 | 106 | // The xApi device connect is in good shape, start the conversation 107 | await bot.beginDialog( 'roomkit_chat' ); 108 | } ); 109 | 110 | const convo = new BotkitConversation( 'roomkit_chat', controller ); 111 | 112 | convo.ask( { channelData: { markdown: '(Roomkit) Enter a command, or `exit`' } }, [ 113 | { 114 | pattern: '^peoplecount', 115 | handler: async ( response, convo, bot ) => { 116 | await convo.gotoThread( 'peoplecount_thread' ); 117 | } 118 | }, 119 | { 120 | pattern: '^xstatus', 121 | handler: async ( response, convo, bot ) => { 122 | 123 | // Execute xAPI xStatus request using the provided path 124 | await roomkit.xapi.status.get( response.split( 'status ' )[ 1 ] ) 125 | .then( async ( resp ) => { 126 | 127 | // If it succeeded, reply with the pretty printed JSON in a markdown code block 128 | await bot.say( { channelData: { markdown: '```json\n' + JSON.stringify( resp, undefined, 4 ) + '\n```' } } ); 129 | } ) 130 | .catch( async ( err ) => { 131 | 132 | let msg = `(Roomkit) Error executing xStatus request: ${ err.message } \n`; 133 | msg += 'Try: `xstatus SystemUnit Uptime`'; 134 | 135 | await bot.say( { channelData: { markdown: msg } } ); 136 | }) 137 | 138 | await convo.repeat(); 139 | } 140 | }, 141 | { 142 | pattern: '^exit', 143 | handler: async ( response,convo, bot ) => { 144 | await convo.gotoThread( 'exit_thread' ); 145 | } 146 | }, 147 | { 148 | default: true, 149 | handler: async ( response, convo, bot ) => { 150 | await bot.say( '(Roomkit) Unrecognized response... \nTry again!' ); 151 | await convo.repeat(); 152 | } 153 | } 154 | ]); 155 | 156 | convo.before( 'peoplecount_thread', async ( convo, bot ) => { 157 | 158 | // Fetch current people count 159 | await roomkit.xapi.status.get( 'RoomAnalytics PeopleCount Current' ) 160 | .then( async count => { 161 | 162 | switch( count ) { 163 | 164 | // -1 indictes the device is in standby/halfwake mode 165 | case '-1': 166 | await convo.setVar( 'analysis', 'Sorry, the device is not counting right now: wake it up!' ); 167 | break; 168 | 169 | case '0': 170 | await convo.setVar( 'analysis', 'No faces detected!' ); 171 | break; 172 | 173 | default: 174 | await convo.setVar( 'analysis', `**Currently, I can see ${ count } face(s)**` ); 175 | } 176 | } ) 177 | .catch( async err => { 178 | 179 | console.log( `Roomkit: Error while counting: ${ err.message }` ); 180 | await convo.setVar( 'analysis', `(Roomkit) Error while querying the device: ${ err.message }` ); 181 | return; 182 | }) 183 | }); 184 | 185 | // Menu option peoplecount 186 | convo.addMessage( 187 | { text: '(Roomkit) {{vars.analysis}}', action: 'default' }, 188 | 'peoplecount_thread' ); 189 | 190 | // Menu option exit 191 | convo.addMessage( 192 | { text: '(Roomkit) Exiting...', action: 'complete' }, 193 | 'exit_thread' ); 194 | 195 | controller.addDialog( convo ); 196 | 197 | controller.commandHelp.push( { command: 'Roomkit', text: 'Query a room device using the jsxapi API over SSH' } ); 198 | 199 | } 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /www/events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "PNWPHP2016", 4 | "name": "Pacific Northwest PHP", 5 | "url": "https://communities.cisco.com/events/1902", 6 | "description": "The Pacific Northwest PHP (PNWPHP) Conference is a 3-day event (September 15th-17th, 2016) for PHP developers. This 2nd annual PNWPHP 2016 event will be held at the Cornish Playhouse. This year, the event is a full-day of workshops, followed by two full days of a single-track conference, with talented speakers from the PHP community and software industry. Come meet us in the Cisco DevNet booth!", 7 | "beginDate": "2016-09-15T15:00:00.000Z", 8 | "beginDay": "Sept 15", 9 | "beginDayInWeek": "friday", 10 | "beginTime": "8:00AM", 11 | "endDate": "2016-09-18T04:00:00.000Z", 12 | "endDay": "Sept 17", 13 | "endDayInWeek": "sunday", 14 | "endTime": "9:00PM", 15 | "category": "conference", 16 | "country": "USA", 17 | "city": "Seattle", 18 | "location_url": "https://developer.cisco.com/site/DevNetZone/" 19 | }, 20 | { 21 | "id": "DevNetCreate17", 22 | "name": "DevNet Create", 23 | "url": "https://communities.cisco.com/events/2181", 24 | "description": "The developer conference where applications meet infrastructure: The application development landscape is changing. Innovation is thriving at the intersection of application development and infrastructure. The DevNet Create inaugural event brings together application developers, infrastructure engineers, designers, technologists, innovators, DevOps engineers and IT Pros who want to define and build this new landscape. Here are some topics that we think will be interesting to bring to this event. We welcome experienced and first time speakers!", 25 | "beginDate": "2017-05-23T16:00:00.000Z", 26 | "beginDay": "May 23", 27 | "beginDayInWeek": "tuesday", 28 | "beginTime": "09:00AM", 29 | "endDate": "2017-05-25T00:00:00.000Z", 30 | "endDay": "May 24", 31 | "endDayInWeek": "wednesday", 32 | "endTime": "5:00PM", 33 | "category": "conference", 34 | "country": "United States", 35 | "city": "San Francisco Bay Area", 36 | "location_url": "https://devnetcreate.io/2017" 37 | }, 38 | { 39 | "id": "ATTPublicSector2016", 40 | "name": "AT&T Public Sector", 41 | "url": "https://communities.cisco.com/events/1944", 42 | "description": "Have an app idea to improve government services? Interested in creating innovative applications that will harness the “Internet of Things” enabling the Federal Government to better serve their employees and the citizen? Come to this special Internet of Things (IoT) and Government focused hackathon, produced by the AT&T Developer Program. We are excited to sponsor this hackathon with IoT and Collaboration technologies. Cisco DevNet will be there to talk about IOx, Jasper, and Spark technologies as they would be a great fit to use to build an app to improve government services. ", 43 | "beginDate": "2016-09-16T22:00:00.000Z", 44 | "beginDay": "Sept 16", 45 | "beginDayInWeek": "friday", 46 | "beginTime": "6:00PM", 47 | "endDate": "2016-09-18T02:00:00.000Z", 48 | "endDay": "Sept 17", 49 | "endDayInWeek": "sunday", 50 | "endTime": "10:00PM", 51 | "category": "hackathon", 52 | "country": "USA", 53 | "city": "Washington, DC", 54 | "location_url": "https://developer.cisco.com/site/DevNetZone/" 55 | }, 56 | { 57 | "id": "HackZurich2016", 58 | "name": "HackZurich", 59 | "url": "https://communities.cisco.com/events/1902", 60 | "description": "The biggest European hackathon is HackZurich! It's 40 hours and 500 hackers! You will team up to create a web, mobile, or desktop application - it's a 40 hour marathon of non-stop hacking. We will be there to present our use case and how CMX technology and infrastructure can help solve machine to human communication. We will be there to present Cisco Spark and Tropo as well.", 61 | "beginDate": "2016-09-16T15:00:00.000Z", 62 | "beginDay": "Sept 16", 63 | "beginDayInWeek": "friday", 64 | "beginTime": "5:00PM", 65 | "endDate": "2016-09-18T14:00:00.000Z", 66 | "endDay": "Sept 18", 67 | "endDayInWeek": "sunday", 68 | "endTime": "4:00PM", 69 | "category": "hackathon", 70 | "country": "Switzerland", 71 | "city": "Zurich", 72 | "location_url": "https://developer.cisco.com/site/DevNetZone/" 73 | }, 74 | { 75 | "id": "PostMobileTag2016", 76 | "name": "Poste Mobile @TAG", 77 | "url": "https://communities.cisco.com/events/1884", 78 | "description": "The second Poste Mobile Hackathon will open officially in the Talent Campus Garden (TAG) Italian Post on Friday, September 23, 2016 at 18.00 in the Sala Enrico Gasperini. We will be there discussing and helping you dig into Cisco Spark and Tropo APIs.", 79 | "beginDate": "2016-09-23T16:00:00.000Z", 80 | "beginDay": "Sept 23", 81 | "beginDayInWeek": "friday", 82 | "beginTime": "6:00PM", 83 | "endDate": "2016-09-25T17:00:00.000Z", 84 | "endDay": "Sept 25", 85 | "endDayInWeek": "sunday", 86 | "endTime": "7:00PM", 87 | "category": "hackathon", 88 | "country": "Italy", 89 | "city": "Roma", 90 | "location_url": "https://developer.cisco.com/site/DevNetZone/" 91 | }, 92 | { 93 | "id": "InnoTrans2016", 94 | "name": "InnoTrans 2016", 95 | "url": "https://communities.cisco.com/events/1982", 96 | "description": "InnoTrans is the leading international trade fair for transport technology and takes places every two years in Berlin - and this is the year to come talk to us! Cisco DevNet will be there to talk with you about the Cisco Innovation Centers and the delivery model and to share the DevNet areas that we drive innovation in the transportation area.", 97 | "beginDate": "2016-09-20T07:00:00.000Z", 98 | "beginDay": "Sept 20", 99 | "beginDayInWeek": "tuesday", 100 | "beginTime": "9:00AM", 101 | "endDate": "2016-09-23T16:00:00.000Z", 102 | "endDay": "Sept 23", 103 | "endDayInWeek": "friday", 104 | "endTime": "6:00PM", 105 | "category": "conference", 106 | "country": "Germany", 107 | "city": "Berlin", 108 | "location_url": "https://developer.cisco.com/site/DevNetZone/" 109 | }, 110 | { 111 | "id": "ThinkRise2016-UK", 112 | "name": "ThinkRise Barclays Hackathon", 113 | "url": "https://communities.cisco.com/events/1983", 114 | "description": "It's a hackathon in the India and the UK! Rise is a physical and digital community that provides start-ups and businesses with the connections and resources to create ground-breaking businesses. It’s the place where the world’s best thinkers and doers create the future of financial technology. Cisco DevNet will be there as we dive into Cisco Spark and Tropo.", 115 | "beginDate": "2016-09-24T07:00:00.000Z", 116 | "beginDay": "Sept 24", 117 | "beginDayInWeek": "saturday", 118 | "beginTime": "8:00AM", 119 | "endDate": "2016-09-25T17:00:00.000Z", 120 | "endDay": "Sept 25", 121 | "endDayInWeek": "sunday", 122 | "endTime": "6:00PM", 123 | "category": "hackathon", 124 | "country": "UK", 125 | "city": "Manchester", 126 | "location_url": "https://developer.cisco.com/site/DevNetZone/" 127 | }, 128 | { 129 | "id": "ThinkRise2016-India", 130 | "name": "ThinkRise Barclays Hackathon", 131 | "url": "https://communities.cisco.com/events/1983", 132 | "description": "It's a hackathon in the India and the UK! Rise is a physical and digital community that provides start-ups and businesses with the connections and resources to create ground-breaking businesses. It’s the place where the world’s best thinkers and doers create the future of financial technology. Cisco DevNet will be there as we dive into Cisco Spark and Tropo.", 133 | "beginDate": "2016-09-24T07:00:00.000Z", 134 | "beginDay": "Sept 24", 135 | "beginDayInWeek": "saturday", 136 | "beginTime": "8:00AM", 137 | "endDate": "2016-09-25T17:00:00.000Z", 138 | "endDay": "Sept 25", 139 | "endDayInWeek": "sunday", 140 | "endTime": "6:00PM", 141 | "category": "hackathon", 142 | "country": "India", 143 | "city": "Mumbai", 144 | "location_url": "https://developer.cisco.com/site/DevNetZone/" 145 | }, 146 | { 147 | "id": "DNAProgrammabilityMemphis17", 148 | "name": "DevNet Express DNA Programmability", 149 | "url": "https://communities.cisco.com/events/2195", 150 | "description": "Who is the event for? Network Engineers or Architects with an interest in DevOps, DevOps Engineers with an interest in networking, Software System Integrators with an interest in networking, It's for anyone interested in understanding how and why to develop using Cisco Enterprise Network Programmability tools and APIs and prefer a ‘listen, learn and then put in into practice’ learning style. What's it all about? It’s about what we call the network’s Digital Network Architecture (DNA): the building blocks of a well-managed network: Cloud Service Management, Automation and Analytics, Physical and Virtual Network Elements, At the DevNet Express we’ll have short, hands-off sessions followed by hands-on missions.", 151 | "beginDate": "2017-03-28T15:00:00.000Z", 152 | "beginDay": "Mar 28", 153 | "beginDayInWeek": "tuesday", 154 | "beginTime": "09:00AM", 155 | "endDate": "2017-03-29T23:00:00.000Z", 156 | "endDay": "Mar 29", 157 | "endDayInWeek": "wednesday", 158 | "endTime": "5:00PM", 159 | "category": "training", 160 | "country": "United States", 161 | "city": "Memphis", 162 | "location_url": "http://devnetevents.cisco.com/event/DevNet-Express-Memphis-DNA-2017" 163 | }, 164 | { 165 | "id": "PulverHWC2016", 166 | "name": "Pulver HWC", 167 | "url": "https://communities.cisco.com/events/1981", 168 | "description": "It's a three day conference (MoNage) focused on the 'Age of Messaging on the Net' focused on the Business of Messaging. Cisco DevNet will be there as we dive into Cisco Spark and Tropo. Learn how you can build an agile team to transform the way you communicate. Connect applications and processes to enhance customer engagement. Use Cisco Spark APIs to integrate and customize collaboration experiences. Add real-time communications to apps by using Tropo APIs. Cisco’s open APIs are easy-to-use with access to 24/7 developer support, so you can build off of the platforms anytime from anywhere.", 169 | "beginDate": "2016-09-20T13:00:00.000Z", 170 | "beginDay": "Sept 20", 171 | "beginDayInWeek": "tuesday", 172 | "beginTime": "9:00AM", 173 | "endDate": "2016-09-22T22:00:00.000Z", 174 | "endDay": "Sept 22", 175 | "endDayInWeek": "thursday", 176 | "endTime": "6:00PM", 177 | "category": "conference", 178 | "country": "USA", 179 | "city": "Boston", 180 | "location_url": "https://developer.cisco.com/site/DevNetZone/" 181 | } 182 | ] --------------------------------------------------------------------------------