diff --git a/Node/cards-AdaptiveCards/.env b/Node/cards-AdaptiveCards/.env new file mode 100644 index 0000000000..d5f29e6150 --- /dev/null +++ b/Node/cards-AdaptiveCards/.env @@ -0,0 +1,4 @@ +# Bot Framework Credentials + +MICROSOFT_APP_ID= +MICROSOFT_APP_PASSWORD= diff --git a/Node/cards-AdaptiveCards/README.md b/Node/cards-AdaptiveCards/README.md new file mode 100644 index 0000000000..be5af93a12 --- /dev/null +++ b/Node/cards-AdaptiveCards/README.md @@ -0,0 +1,18 @@ +# Multi-Dialog Bot Sample + +A sample bot showing different kind of dialogs. + +[![Deploy to Azure][Deploy Button]][Deploy Node/AdaptiveCards] + +[Deploy Button]: https://azuredeploy.net/deploybutton.png +[Deploy Node/AdaptiveCards]: https://azuredeploy.net + +### Prerequisites + +The minimum prerequisites to run this sample are: +* Latest Node.js with NPM. Download it from [here](https://nodejs.org/en/download/). +* The Bot Framework Emulator. To install the Bot Framework Emulator, download it from [here](https://emulator.botframework.com/). Please refer to [this documentation article](https://github.com/microsoft/botframework-emulator/wiki/Getting-Started) to know more about the Bot Framework Emulator. +* **[Recommended]** Visual Studio Code for IntelliSense and debugging, download it from [here](https://code.visualstudio.com/) for free. + +### Code Highlights + diff --git a/Node/cards-AdaptiveCards/app.js b/Node/cards-AdaptiveCards/app.js new file mode 100644 index 0000000000..ac63f11bb9 --- /dev/null +++ b/Node/cards-AdaptiveCards/app.js @@ -0,0 +1,266 @@ +// This loads the environment variables from the .env file +require('dotenv-extended').load(); + +var util = require('util'); +var builder = require('botbuilder'); +var restify = require('restify'); + +// Setup Restify Server +var server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, function () { + console.log('%s listening to %s', server.name, server.url); +}); + +// Create chat bot and listen to messages +var connector = new builder.ChatConnector({ + appId: process.env.MICROSOFT_APP_ID, + appPassword: process.env.MICROSOFT_APP_PASSWORD +}); +server.post('/api/messages', connector.listen()); + +var bot = new builder.UniversalBot(connector, function (session) { + + if (session.message && session.message.value) { + // receiving a card submit action + var value = session.message.value; + + // Search, vlaidate parameters + if (value.type === 'hotelSearch') { + if (validateHotelSearch(session.message.value)) { + // proceed to search + return session.beginDialog('hotels-search', session.message.value); + } + } + + // Hotel selection + if (value.type === 'hotelSelection') { + return sendHotelSelection(session, session.message.value); + } + + // A form data was recieved, invalid or incomplete since the previous validation did not pass + return session.send('Please complete all the search parameters'); + } + + // Display Welcome card with Hotels and Flights search options + var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + '$schema': 'http://adaptivecards.io/schemas/adaptive-card.json', + 'type': 'AdaptiveCard', + 'version': '1.0', + 'body': [ + { + 'type': 'Container', + 'speak': 'Hello!Are you looking for a flight or a hotel?', + 'items': [ + { + 'type': 'ColumnSet', + 'columns': [ + { + 'type': 'Column', + 'size': 'auto', + 'items': [ + { + 'type': 'Image', + 'url': 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Adaptive+Cards&w=300&h=300', + 'size': 'medium', + 'style': 'person' + } + ] + }, + { + 'type': 'Column', + 'size': 'stretch', + 'items': [ + { + 'type': 'TextBlock', + 'text': 'Hello!', + 'weight': 'bolder', + 'isSubtle': true + }, + { + 'type': 'TextBlock', + 'text': 'Are you looking for a flight or a hotel?', + 'wrap': true + } + ] + } + ] + } + ] + } + ], + 'actions': [ + // Hotels Search form + { + 'type': 'Action.ShowCard', + 'title': 'Hotels', + 'speak': 'Hotels', + 'card': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': 'Welcome to the Hotels finder!', + 'speak': 'Welcome to the Hotels finder!', + 'weight': 'bolder', + 'size': 'large' + }, + { + 'type': 'TextBlock', + 'text': 'Please enter your destination:' + }, + { + 'type': 'Input.Text', + 'id': 'destination', + 'speak': 'Please enter your destination', + 'placeholder': 'Miami, Florida', + 'style': 'text' + }, + { + 'type': 'TextBlock', + 'text': 'When do you want to check in?' + }, + { + 'type': 'Input.Date', + 'id': 'checkin', + 'speak': 'When do you want to check in?' + }, + { + 'type': 'TextBlock', + 'text': 'How many nights do you want to stay?' + }, + { + 'type': 'Input.Number', + 'id': 'nights', + 'min': 1, + 'max': 60, + 'speak': 'How many nights do you want to stay?' + } + ], + 'actions': [ + { + 'type': 'Action.Submit', + 'title': 'Search', + 'speak': 'Search', + 'data': { + 'type': 'hotelSearch' + } + } + ] + } + }, + { + 'type': 'Action.ShowCard', + 'title': 'Flights', + 'speak': 'Flights', + 'card': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': 'Flights is not implemented =(', + 'speak': 'Flights is not implemented', + 'weight': 'bolder' + } + ] + } + } + ] + } + }; + + var msg = new builder.Message(session) + .addAttachment(card); + session.send(msg); +}); + +// Search Hotels +bot.dialog('hotels-search', require('./hotels-search')); + +// Help +bot.dialog('support', require('./support')) + .triggerAction({ + matches: [/help/i, /support/i, /problem/i] + }); + +// log any bot errors into the console +bot.on('error', function (e) { + console.log('And error ocurred', e); +}); + +function validateHotelSearch(hotelSearch) { + if (!hotelSearch) { + return false; + } + + // Destination + var hasDestination = typeof hotelSearch.destination === 'string' && hotelSearch.destination.length > 3; + + // Checkin + var checkin = Date.parse(hotelSearch.checkin); + var hasCheckin = !isNaN(checkin); + if (hasCheckin) { + hotelSearch.checkin = new Date(checkin); + } + + // Nights + var nights = parseInt(hotelSearch.nights, 10); + var hasNights = !isNaN(nights); + if (hasNights) { + hotelSearch.nights = nights; + } + + return hasDestination && hasCheckin && hasNights; +} + +function sendHotelSelection(session, hotel) { + var description = util.format('%d stars with %d reviews. From $%d per night.', hotel.rating, hotel.numberOfReviews, hotel.priceStarting); + var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'Container', + 'items': [ + { + 'type': 'TextBlock', + 'text': hotel.name + ' in ' + hotel.location, + 'weight': 'bolder', + 'speak': '' + hotel.name + '' + }, + { + 'type': 'TextBlock', + 'text': description, + 'speak': '' + description + '' + }, + { + 'type': 'Image', + 'size': 'auto', + 'url': hotel.image + }, + { + 'type': 'ImageSet', + 'imageSize': 'medium', + 'separation': 'strong', + 'images': hotel.moreImages.map((img) => ({ + 'type': 'Image', + 'url': img + })) + } + ], + 'selectAction': { + 'type': 'Action.OpenUrl', + 'url': 'https://dev.botframework.com/' + } + } + ] + } + }; + + var msg = new builder.Message(session) + .addAttachment(card); + + session.send(msg); +} \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/azuredeploy.json b/Node/cards-AdaptiveCards/azuredeploy.json new file mode 100644 index 0000000000..b42df36f07 --- /dev/null +++ b/Node/cards-AdaptiveCards/azuredeploy.json @@ -0,0 +1,129 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "defaultValue": "BotBuilder-Samples", + "type": "string" + }, + "hostingPlanName": { + "type": "string" + }, + "siteLocation": { + "type": "string" + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + }, + "repoUrl": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "Project": { + "type": "string", + "defaultValue": "Node/cards-AdaptiveCards" + }, + "WEBSITE_NODE_DEFAULT_VERSION": { + "type": "string", + "defaultValue": "6.9.5" + }, + "MICROSOFT_APP_ID": { + "type": "string" + }, + "MICROSOFT_APP_PASSWORD": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "name": "[parameters('hostingPlanName')]", + "type": "Microsoft.Web/serverFarms", + "location": "[parameters('siteLocation')]", + "properties": { + "name": "[parameters('hostingPlanName')]", + "sku": "[parameters('sku')]", + "workerSize": "[parameters('workerSize')]", + "numberOfWorkers": 1 + } + }, + { + "apiVersion": "2014-06-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/Sites", + "location": "[parameters('siteLocation')]", + "dependsOn": [ + "[concat('Microsoft.Web/serverFarms/', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty" + }, + "properties": { + "name": "[parameters('siteName')]", + "serverFarm": "[parameters('hostingPlanName')]" + }, + "resources": [ + { + "apiVersion": "2014-04-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[concat('Microsoft.Web/Sites/', parameters('siteName'))]" + ], + "properties": { + "appSettings": [ + { + "name": "Project", + "value": "[parameters('Project')]" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "[parameters('WEBSITE_NODE_DEFAULT_VERSION')]" + }, + { + "name": "MICROSOFT_APP_ID", + "value": "[parameters('MICROSOFT_APP_ID')]" + }, + { + "name": "MICROSOFT_APP_PASSWORD", + "value": "[parameters('MICROSOFT_APP_PASSWORD')]" + } + ] + } + }, + { + "apiVersion": "2014-04-01", + "name": "web", + "type": "sourcecontrols", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]", + "[concat('Microsoft.Web/Sites/', parameters('siteName'), '/config/web')]" + ], + "properties": { + "RepoUrl": "[parameters('repoUrl')]", + "branch": "[parameters('branch')]", + "IsManualIntegration": true + } + } + ] + } + ] +} \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/hotels-search.js b/Node/cards-AdaptiveCards/hotels-search.js new file mode 100644 index 0000000000..df8f511434 --- /dev/null +++ b/Node/cards-AdaptiveCards/hotels-search.js @@ -0,0 +1,84 @@ +var util = require('util'); +var _ = require('lodash'); +var builder = require('botbuilder'); +var Store = require('./store'); + +module.exports = function search(session, hotelSearch) { + var destination = hotelSearch.destination; + var checkIn = hotelSearch.checkin; + var checkOut = checkIn.addDays(hotelSearch.nights); + + session.send( + 'Ok. Searching for Hotels in %s from %d/%d to %d/%d...', + destination, + checkIn.getMonth() + 1, checkIn.getDate(), + checkOut.getMonth() + 1, checkOut.getDate()); + + // Async search + Store + .searchHotels(destination, checkIn, checkOut) + .then(function (hotels) { + // Results + var title = util.format('I found in total %d hotels for your dates:', hotels.length); + + var rows = _.chunk(hotels, 3).map(group => + ({ + 'type': 'ColumnSet', + 'columns': group.map(asHotelItem) + })); + + var card = { + 'contentType': 'application/vnd.microsoft.card.adaptive', + 'content': { + 'type': 'AdaptiveCard', + 'body': [ + { + 'type': 'TextBlock', + 'text': title, + 'size': 'extraLarge', + 'speak': '' + title + '' + } + ].concat(rows) + } + }; + + var msg = new builder.Message(session) + .addAttachment(card); + session.send(msg); + }); + + session.endDialog(); +}; + +// Helpers +function asHotelItem(hotel) { + return { + 'type': 'Column', + 'size': '20', + 'items': [ + { + 'type': 'TextBlock', + 'horizontalAlignment': 'center', + 'wrap': false, + 'weight': 'bolder', + 'text': hotel.name, + 'speak': '' + hotel.name + '' + }, + { + 'type': 'Image', + 'size': 'auto', + 'url': hotel.image + } + ], + 'selectAction': { + 'type': 'Action.Submit', + 'data': _.extend({ type: 'hotelSelection' }, hotel) + } + } +} + +Date.prototype.addDays = function (days) { + var date = new Date(this.valueOf()); + date.setDate(date.getDate() + days); + return date; +}; \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/package.json b/Node/cards-AdaptiveCards/package.json new file mode 100644 index 0000000000..389bd68001 --- /dev/null +++ b/Node/cards-AdaptiveCards/package.json @@ -0,0 +1,30 @@ +{ + "name": "botbuilder-sample-adaptivecards", + "version": "1.0.0", + "description": "Bot Builder Sample - Adaptive Cards", + "scripts": { + "start": "node app.js" + }, + "author": "Microsoft Corp.", + "license": "MIT", + "keywords": [ + "botbuilder", + "bots", + "chatbots", + "botbuilder-samples" + ], + "bugs": { + "url": "https://github.com/Microsoft/BotBuilder-Samples/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/BotBuilder-Samples.git" + }, + "dependencies": { + "bluebird": "^3.5.0", + "botbuilder": "^3.8.3", + "dotenv-extended": "^2.0.0", + "lodash": "^4.17.4", + "restify": "^4.3.0" + } +} diff --git a/Node/cards-AdaptiveCards/store.js b/Node/cards-AdaptiveCards/store.js new file mode 100644 index 0000000000..d2c6df7547 --- /dev/null +++ b/Node/cards-AdaptiveCards/store.js @@ -0,0 +1,32 @@ +var Promise = require('bluebird'); + +module.exports = { + searchHotels: function (destination, checkInDate, checkOutDate) { + return new Promise(function (resolve) { + + // Filling the hotels results manually just for demo purposes + var hotels = []; + for (var i = 1; i <= 6; i++) { + hotels.push({ + name: 'Hotel ' + i, + location: destination, + rating: Math.ceil(Math.random() * 5), + numberOfReviews: Math.floor(Math.random() * 5000) + 1, + priceStarting: Math.floor(Math.random() * 450) + 80, + image: 'https://placeholdit.imgix.net/~text?txtsize=35&txt=Hotel+' + i + '&w=500&h=260', + moreImages: [ + 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+1&w=450&h=300', + 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+2&w=450&h=300', + 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+3&w=450&h=300', + 'https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+4&w=450&h=300' + ] + }); + } + + hotels.sort(function (a, b) { return a.priceStarting - b.priceStarting; }); + + // complete promise with a timer to simulate async response + setTimeout(function () { resolve(hotels); }, 1000); + }); + } +}; \ No newline at end of file diff --git a/Node/cards-AdaptiveCards/support.js b/Node/cards-AdaptiveCards/support.js new file mode 100644 index 0000000000..72b9d0adcf --- /dev/null +++ b/Node/cards-AdaptiveCards/support.js @@ -0,0 +1,13 @@ +module.exports = function (session) { + // Generate ticket + var tickerNumber = Math.ceil(Math.random() * 20000); + + // Reply and return to parent dialog + session.send('Your message \'%s\' was registered. Once we resolve it; we will get back to you.', session.message.text); + + session.send('Thanks for contacting our support team. Your ticket number is %s.', tickerNumber); + + session.endDialogWithResult({ + response: tickerNumber + }); +}; \ No newline at end of file