├── dashboard ├── views │ ├── error.pug │ ├── notify.pug │ └── layout.pug ├── public │ └── stylesheets │ │ └── style.css ├── .env ├── package.json ├── app.js ├── routes │ └── index.js └── bin │ └── www ├── bot ├── .env ├── package.json ├── app.js ├── bot │ └── index.js └── models │ ├── docdbUtils.js │ └── userDao.js ├── .gitignore └── README.md /dashboard/views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /dashboard/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /*Whatever styles you wanna use*/ 2 | /*Not used atm*/ 3 | 4 | body { 5 | padding: 50px; 6 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 7 | } 8 | 9 | a { 10 | color: #00B7FF; 11 | } 12 | -------------------------------------------------------------------------------- /bot/.env: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | 3 | MICROSOFT_APP_ID= 4 | MICROSOFT_APP_PASSWORD= 5 | 6 | # Document db config 7 | 8 | HOST=https://.documents.azure.com:443/ 9 | AUTHKEY= 10 | DATABASEID= 11 | COLLECTIONID= -------------------------------------------------------------------------------- /dashboard/.env: -------------------------------------------------------------------------------- 1 | # Bot Framework Credentials 2 | 3 | MICROSOFT_APP_ID= 4 | MICROSOFT_APP_PASSWORD= 5 | 6 | # Document db config 7 | 8 | HOST=https://.documents.azure.com:443/ 9 | AUTHKEY= 10 | DATABASEID= 11 | COLLECTIONID= -------------------------------------------------------------------------------- /dashboard/views/notify.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | .container 5 | h2 Send a notification to all users via the chatbot 6 | // form to send message to all 7 | form(method='POST' action='/notify') 8 | div.form-group 9 | label(for='message') Message: 10 | input#name.form-control(type='text', placeholder='Enter message to send' name='message') 11 | button.btn.btn-primary(type='submit') Send notification -------------------------------------------------------------------------------- /bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "summit-bot", 3 | "version": "1.0.0", 4 | "description": "Concierge for the MoM Summit", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "botbuilder": "^3.9.0", 14 | "documentdb": "^1.12.2", 15 | "dotenv-extended": "^2.0.1", 16 | "restify": "^5.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "summit-dash", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "devstart": "nodemon ./bin/www" 8 | }, 9 | "dependencies": { 10 | "body-parser": "~1.17.1", 11 | "botbuilder": "^3.9.0", 12 | "cookie-parser": "~1.4.3", 13 | "debug": "~2.6.3", 14 | "documentdb": "^1.12.2", 15 | "dotenv-extended": "^2.0.1", 16 | "express": "^4.15.4", 17 | "morgan": "~1.8.1", 18 | "pug": "^2.0.0-rc.3", 19 | "serve-favicon": "~2.4.2" 20 | }, 21 | "devDependencies": { 22 | "nodemon": "^1.11.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dashboard/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype=html 2 | html(language=en) 3 | head 4 | // Use bootstrap 5 | link(rel="stylesheet", href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7", crossorigin="anonymous") 6 | link(rel="stylesheet", href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css", integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r", crossorigin="anonymous") 7 | script(src="https://code.jquery.com/jquery-2.2.4.js", integrity="sha256-iT6Q9iMJYuQiMWNd9lDyBUStIq/8PuOW33aOqmvFpqI=", crossorigin="anonymous") 8 | script(src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js", integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS", crossorigin="anonymous") 9 | body 10 | block content -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | -------------------------------------------------------------------------------- /bot/app.js: -------------------------------------------------------------------------------- 1 | // This loads the environment variables from the .env file 2 | require('dotenv-extended').load(); 3 | 4 | // Register Bot 5 | var bot = require('./bot'); 6 | var restify = require('restify'); 7 | var server = restify.createServer(); 8 | 9 | //========================================================= 10 | // DocumentDb Setup 11 | //========================================================= 12 | var DocumentDBClient = require('documentdb').DocumentClient; 13 | var UserDao = require('./models/userDao'); 14 | 15 | var docDbClient = new DocumentDBClient(process.env.HOST, { 16 | masterKey: process.env.AUTHKEY 17 | }); 18 | var userDao = new UserDao(docDbClient, process.env.DATABASEID, process.env.COLLECTIONID); 19 | 20 | userDao.init(function (err) { 21 | if (err) { 22 | console.log("Could not connect to db: " + err) 23 | } else { 24 | console.log("User db initialised") 25 | } 26 | 27 | // Pass data access object to bot module 28 | bot.initdb(userDao); 29 | 30 | server.post('/api/messages', bot.listen()); 31 | 32 | // Start listening 33 | var port = process.env.port || process.env.PORT || 3979; 34 | server.listen(port, function () { 35 | console.log('Server listening on port %s', port); 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /dashboard/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv-extended').load(); 2 | 3 | var express = require('express'); 4 | var path = require('path'); 5 | var favicon = require('serve-favicon'); 6 | var logger = require('morgan'); 7 | var cookieParser = require('cookie-parser'); 8 | var bodyParser = require('body-parser'); 9 | 10 | var index = require('./routes/index'); 11 | 12 | var app = express(); 13 | 14 | // view engine setup 15 | app.set('views', path.join(__dirname, 'views')); 16 | app.set('view engine', 'pug'); 17 | 18 | // uncomment after placing your favicon in /public 19 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 20 | app.use(logger('dev')); 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(cookieParser()); 24 | app.use(express.static(path.join(__dirname, 'public'))); 25 | 26 | app.use('/', index); 27 | 28 | // catch 404 and forward to error handler 29 | app.use(function(req, res, next) { 30 | var err = new Error('Not Found'); 31 | err.status = 404; 32 | next(err); 33 | }); 34 | 35 | // error handler 36 | app.use(function(err, req, res, next) { 37 | // set locals, only providing error in development 38 | res.locals.message = err.message; 39 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 40 | 41 | // render the error page 42 | res.status(err.status || 500); 43 | res.render('error'); 44 | }); 45 | 46 | module.exports = app; 47 | -------------------------------------------------------------------------------- /dashboard/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var builder = require('botbuilder'); 5 | 6 | var connector = new builder.ChatConnector({ 7 | appId: process.env.MICROSOFT_APP_ID, 8 | appPassword: process.env.MICROSOFT_APP_PASSWORD 9 | }); 10 | 11 | var bot = new builder.UniversalBot(connector); 12 | 13 | var documentClient = require("documentdb").DocumentClient; 14 | var client = new documentClient(process.env.HOST, { "masterKey": process.env.AUTHKEY }); 15 | var databaseUrl = `dbs/${process.env.DATABASEID}`; 16 | var collectionUrl = `${databaseUrl}/colls/${process.env.COLLECTIONID}`; 17 | 18 | /* GET home page. */ 19 | router.get('/', function(req, res, next) { 20 | res.render('notify', { title: 'Send Notification' }); 21 | }); 22 | 23 | router.post('/notify', function(req, res, next) { 24 | // Get all addresses from documentdb 25 | var addresses = queryCollection(function (docs){ 26 | // Iterate through each user and send them the message 27 | docs.forEach(function (addr) { 28 | sendProactiveMessage(req.body.message, addr); 29 | }); 30 | res.render('notify', { title: 'Send Notification' }); 31 | }); 32 | }); 33 | 34 | // Get all addresses 35 | // https://docs.microsoft.com/en-us/azure/cosmos-db/documentdb-nodejs-samples 36 | function queryCollection(callback) { 37 | console.log(`Querying collection through index:\n${process.env.COLLECTIONID}`); 38 | 39 | var queryIterator = client.readDocuments(collectionUrl).toArray(function (err, docs) { 40 | if (err) { 41 | console.log(err); 42 | } else { 43 | console.log(docs.length + ' user addresses found'); 44 | callback(docs); 45 | } 46 | }); 47 | }; 48 | 49 | function sendProactiveMessage(text, addr) { 50 | var msg = new builder.Message().address(addr); 51 | msg.text(text); 52 | msg.textLocale('en-US'); 53 | bot.send(msg); 54 | } 55 | 56 | module.exports = router; 57 | -------------------------------------------------------------------------------- /bot/bot/index.js: -------------------------------------------------------------------------------- 1 | var builder = require('botbuilder'); 2 | 3 | //========================================================= 4 | // Bot Setup 5 | //========================================================= 6 | 7 | var connector = new builder.ChatConnector({ 8 | appId: process.env.MICROSOFT_APP_ID, 9 | appPassword: process.env.MICROSOFT_APP_PASSWORD 10 | }); 11 | 12 | // User db access 13 | var userDao; 14 | 15 | var bot = new builder.UniversalBot(connector, function (session) { 16 | // Modify with your own dialogs 17 | session.send("Hello, how can I help?") 18 | }); 19 | 20 | // Enable Conversation Data persistence 21 | bot.set('persistConversationData', true); 22 | 23 | // Send welcome and save user address when conversation with bot is updated 24 | bot.on('conversationUpdate', function (message) { 25 | if (message.membersAdded) { 26 | message.membersAdded.forEach(function (identity) { 27 | if (identity.id === message.address.bot.id) { 28 | // Save the address to cosmos db here 29 | userDao.addItem(message.address, function (err) { 30 | if (err) { 31 | console.log(err); 32 | } else { 33 | console.log("Successfully saved user address"); 34 | } 35 | }); 36 | bot.beginDialog(message.address, '/'); 37 | } 38 | }); 39 | } 40 | }); 41 | 42 | function listen() { 43 | return connector.listen(); 44 | } 45 | 46 | // Other wrapper functions 47 | function beginDialog(address, dialogId, dialogArgs) { 48 | bot.beginDialog(address, dialogId, dialogArgs); 49 | } 50 | 51 | function sendMessage(message) { 52 | bot.send(message); 53 | } 54 | 55 | function initdb(userdao) { 56 | userDao = userdao; 57 | } 58 | 59 | module.exports = { 60 | initdb: initdb, 61 | listen: listen, 62 | beginDialog: beginDialog, 63 | sendMessage: sendMessage 64 | }; -------------------------------------------------------------------------------- /dashboard/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('summit-dash:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Bot Framework Notifications via Web App # 2 | 3 | Push custom notifications to all your bot users with Microsoft Bot Framework from a webpage. Enter a message and push the message to all users. This scenario is ideal for event bots, where the organiser or comms team may want to send announcements to all the participants. I have not included any authentication - but in production you will want to add some form of auth so that the web app is protected from the public. 4 | 5 | **Note: The bot and the web app are hosted separately, which is ideal in production.** 6 | 7 | ## Demo ## 8 | 9 | ![demo-gif](http://i.imgur.com/SxqWSav.gif) 10 | 11 | ## Install and run locally ## 12 | 13 | Run `npm install` in both bot and dashboard folders to build dependencies. 14 | 15 | Populate the .env files for the bot and dashboard: 16 | - If you're running locally, you can leave the MICROSOFT_APP_ID and MICROSOFT_APP_PASSWORD fields blank for now. 17 | - Get an Azure Subscription. You can sign up for a free trial [here](https://azure.microsoft.com/en-us/free/). 18 | - Spin up an Azure Cosmos db instance [here](https://azure.microsoft.com/en-us/services/cosmos-db/), for Document db. You will need to populate the .env file with the host url, primary key, database id and collection id. 19 | 20 | Run `npm start` in the bot and dashboard. Start typing custom messages in your web app, and see these get sent to your bot through the [emulator](https://emulator.botframework.com). 21 | 22 | ## Install and run on the cloud ## 23 | 24 | - Generate a bot at http://dev.botframework.com to get the MICROSOFT_APP_ID and MICROSOFT_APP_PASSWORD. Populate the .env variables with these values. 25 | - Generate 2 separate Azure App Services to host the bot and the web app separately. 26 | - Use continuous deployment with a source control of your choice (Github/VSTS) to push your code to the 2 App Services. 27 | 28 | ## Additional Resources ## 29 | 30 | - [Sending proactive messages via Bot Framework](https://github.com/Microsoft/BotBuilder-Samples/tree/master/Node/core-proactiveMessages) 31 | - [Working with the Document db Nodejs API](https://docs.microsoft.com/en-us/azure/cosmos-db/documentdb-nodejs-samples) -------------------------------------------------------------------------------- /bot/models/docdbUtils.js: -------------------------------------------------------------------------------- 1 | var DocumentDBClient = require('documentdb').DocumentClient; 2 | 3 | var DocDBUtils = { 4 | getOrCreateDatabase: function (client, databaseId, callback) { 5 | var querySpec = { 6 | query: 'SELECT * FROM root r WHERE r.id= @id', 7 | parameters: [{ 8 | name: '@id', 9 | value: databaseId 10 | }] 11 | }; 12 | 13 | client.queryDatabases(querySpec).toArray(function (err, results) { 14 | if (err) { 15 | console.log(err); 16 | callback(err); 17 | 18 | } else { 19 | if (results.length === 0) { 20 | var databaseSpec = { 21 | id: databaseId 22 | }; 23 | 24 | client.createDatabase(databaseSpec, function (err, created) { 25 | callback(null, created); 26 | }); 27 | 28 | } else { 29 | callback(null, results[0]); 30 | } 31 | } 32 | }); 33 | }, 34 | 35 | getOrCreateCollection: function (client, databaseLink, collectionId, callback) { 36 | var querySpec = { 37 | query: 'SELECT * FROM root r WHERE r.id=@id', 38 | parameters: [{ 39 | name: '@id', 40 | value: collectionId 41 | }] 42 | }; 43 | 44 | client.queryCollections(databaseLink, querySpec).toArray(function (err, results) { 45 | if (err) { 46 | callback(err); 47 | 48 | } else { 49 | if (results.length === 0) { 50 | var collectionSpec = { 51 | id: collectionId 52 | }; 53 | 54 | var requestOptions = { 55 | offerType: 'S1' 56 | }; 57 | 58 | client.createCollection(databaseLink, collectionSpec, requestOptions, function (err, created) { 59 | callback(null, created); 60 | }); 61 | 62 | } else { 63 | callback(null, results[0]); 64 | } 65 | } 66 | }); 67 | } 68 | }; 69 | 70 | module.exports = DocDBUtils; -------------------------------------------------------------------------------- /bot/models/userDao.js: -------------------------------------------------------------------------------- 1 | var DocumentDBClient = require('documentdb').DocumentClient; 2 | var docdbUtils = require('./docdbUtils'); 3 | 4 | function UserDao(documentDBClient, databaseId, collectionId) { 5 | this.client = documentDBClient; 6 | this.databaseId = databaseId; 7 | this.collectionId = collectionId; 8 | 9 | this.database = null; 10 | this.collection = null; 11 | } 12 | 13 | module.exports = UserDao; 14 | 15 | UserDao.prototype = { 16 | init: function (callback) { 17 | var self = this; 18 | 19 | docdbUtils.getOrCreateDatabase(self.client, self.databaseId, function (err, db) { 20 | if (err) { 21 | console.log(err); 22 | callback(err); 23 | 24 | } else { 25 | self.database = db; 26 | docdbUtils.getOrCreateCollection(self.client, self.database._self, self.collectionId, function (err, coll) { 27 | if (err) { 28 | console.log(err); 29 | callback(err); 30 | 31 | } else { 32 | self.collection = coll; 33 | callback(); 34 | } 35 | }); 36 | } 37 | }); 38 | }, 39 | 40 | find: function (querySpec, callback) { 41 | var self = this; 42 | 43 | self.client.queryDocuments(self.collection._self, querySpec).toArray(function (err, results) { 44 | if (err) { 45 | callback(err); 46 | 47 | } else { 48 | callback(null, results); 49 | } 50 | }); 51 | }, 52 | 53 | addItem: function (item, callback) { 54 | var self = this; 55 | 56 | self.client.createDocument(self.collection._self, item, function (err, doc) { 57 | if (err) { 58 | console.log(err); 59 | callback(err); 60 | 61 | } else { 62 | callback(null, doc); 63 | } 64 | }); 65 | }, 66 | 67 | updateItem: function (itemId, callback) { 68 | var self = this; 69 | 70 | self.getItem(itemId, function (err, doc) { 71 | if (err) { 72 | callback(err); 73 | 74 | } else { 75 | doc.completed = true; 76 | 77 | self.client.replaceDocument(doc._self, doc, function (err, replaced) { 78 | if (err) { 79 | callback(err); 80 | 81 | } else { 82 | callback(null, replaced); 83 | } 84 | }); 85 | } 86 | }); 87 | }, 88 | 89 | getItem: function (itemId, callback) { 90 | var self = this; 91 | 92 | var querySpec = { 93 | query: 'SELECT * FROM root r WHERE r.id = @id', 94 | parameters: [{ 95 | name: '@id', 96 | value: itemId 97 | }] 98 | }; 99 | 100 | self.client.queryDocuments(self.collection._self, querySpec).toArray(function (err, results) { 101 | if (err) { 102 | callback(err); 103 | 104 | } else { 105 | callback(null, results[0]); 106 | } 107 | }); 108 | } 109 | }; --------------------------------------------------------------------------------