diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3ab1ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +.vscode/ +node_modules/ +build/ +tmp/ +temp/ \ No newline at end of file diff --git a/.vs/Coffee-Bot/v15/.suo b/.vs/Coffee-Bot/v15/.suo new file mode 100644 index 0000000..6c35b1a Binary files /dev/null and b/.vs/Coffee-Bot/v15/.suo differ diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 0000000..f8b4888 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..f5d0a94 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,7 @@ +{ + "ExpandedNodes": [ + "" + ], + "SelectedNode": "\\app.js", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000..5a604ae Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/PostDeployScripts/githubProject.json.template b/PostDeployScripts/githubProject.json.template new file mode 100644 index 0000000..953220a --- /dev/null +++ b/PostDeployScripts/githubProject.json.template @@ -0,0 +1,9 @@ +{ + "name": "{WEB_SITE_NAME}", + "description": "{WEB_SITE_NAME} Azure Bot Service Code", + "homepage": "https://github.com", + "private": false, + "has_issues": true, + "has_projects": true, + "has_wiki": true +} \ No newline at end of file diff --git a/PostDeployScripts/prepareSrc.cmd b/PostDeployScripts/prepareSrc.cmd new file mode 100644 index 0000000..2290e64 --- /dev/null +++ b/PostDeployScripts/prepareSrc.cmd @@ -0,0 +1,58 @@ +rem @echo off +setlocal +SET password=%1 +SET repoName=srcRepo +SET repoUrl=file:///%HOMEDRIVE:~0,1%/%HOMEPATH:~1%/site/%repoName% +SET download=bot-src + +echo %repoUrl% + +rem cd to project root +pushd ..\wwwroot + +rem init git +call git init +call git config user.name "botframework" +call git config user.email "util@botframework.com" +call git add . +call git commit -m "prepare to download source" +call git remote add srcRepo %repoUrl% +popd + +rem init upstream +pushd %HOME%\site +mkdir srcRepo +cd srcRepo +call git init --bare +popd + +rem push to upstream +pushd ..\wwwroot +call git push --set-upstream srcRepo master +popd + +rem clone srcRepo +pushd %HOME%\site +call git clone %repoUrl% %download% +rem delete .git +cd %download% +call rm -r -f .git +popd + +rem prepare for publish +type PostDeployScripts\publish.js.template | sed -e s/\{WEB_SITE_NAME\}/%WEBSITE_SITE_NAME%/g | sed -e s/\{PASSWORD\}/%password%/g > %HOME%\site\%download%\publish.js + +rem preare the zip file +%HOMEDRIVE%\7zip\7za a %HOME%\site\%download%.zip %HOME%\site\%download%\* + +rem cleanup git stuff +pushd ..\wwwroot +call rm -r -f .git +popd + +pushd %HOME%\site +call rm -r -f %download% +call rm -r -f %repoName% +popd + +endlocal diff --git a/PostDeployScripts/publish.js.template b/PostDeployScripts/publish.js.template new file mode 100644 index 0000000..0f635f3 --- /dev/null +++ b/PostDeployScripts/publish.js.template @@ -0,0 +1,52 @@ +var zipFolder = require('zip-folder'); +var path = require('path'); +var fs = require('fs'); +var request = require('request'); + +var rootFolder = path.resolve('.'); +var zipPath = path.resolve(rootFolder, '../{WEB_SITE_NAME}.zip'); +var kuduApi = 'https://{WEB_SITE_NAME}.scm.azurewebsites.net/api/zip/site/wwwroot'; +var userName = '${WEB_SITE_NAME}'; +var password = '{PASSWORD}'; + +function uploadZip(callback) { + fs.createReadStream(zipPath).pipe(request.put(kuduApi, { + auth: { + username: userName, + password: password, + sendImmediately: true + }, + headers: { + "Content-Type": "applicaton/zip" + } + })) + .on('response', function(resp){ + if (resp.statusCode >= 200 && resp.statusCode < 300) { + fs.unlink(zipPath); + callback(null); + } else if (resp.statusCode >= 400) { + callback(resp); + } + }) + .on('error', function(err) { + callback(err) + }); +} + +function publish(callback) { + zipFolder(rootFolder, zipPath, function(err) { + if (!err) { + uploadZip(callback); + } else { + callback(err); + } + }) +} + +publish(function(err) { + if (!err) { + console.log('{WEB_SITE_NAME} publish'); + } else { + console.error('failed to publish {WEB_SITE_NAME}', err); + } +}); \ No newline at end of file diff --git a/PostDeployScripts/runGulp.cmd b/PostDeployScripts/runGulp.cmd new file mode 100644 index 0000000..5b9fd79 --- /dev/null +++ b/PostDeployScripts/runGulp.cmd @@ -0,0 +1,28 @@ +@echo off +setlocal + +if exist ..\wwwroot\package.json ( + pushd ..\wwwroot + echo npm install --production + call npm install --production + popd +) + +for /d %%d in (..\wwwroot\*) do ( + echo check %%d + pushd %%d + if exist package.json ( + echo npm install --production + call npm install --production + ) else ( + echo no package.json found + ) + popd +) + +echo record deployment timestamp +date /t >> ..\deployment.log +time /t >> ..\deployment.log +echo ---------------------- >> ..\deployment.log +echo Deployment done + diff --git a/PostDeployScripts/setupGithubRemoteRepo.cmd b/PostDeployScripts/setupGithubRemoteRepo.cmd new file mode 100644 index 0000000..f6a1f50 --- /dev/null +++ b/PostDeployScripts/setupGithubRemoteRepo.cmd @@ -0,0 +1,44 @@ +@echo off +setlocal +rem ------------------------------------------------------------------------------------------ +rem setupVsoRemoteRepo [remoteUser] [personalAccessToken] [projName{optional}] +rem create and populate VSO git repo for the ABS code instance +rem +rem remoteUser: user account name of the personal access token +rem personalAccessToken: the personal access token used to access github REST API (requires repos scope) +rem projName the name of the project to create (default to WEBSITE_SITE_NAME) +rem ------------------------------------------------------------------------------------------ +set remoteUrl=https://api.github.com +set remoteUser=%1 +set remotePwd=%2 +set projName=%3 +if '%projName%'=='' set projName=%WEBSITE_SITE_NAME% +set repoUrl=https://%remoteUser%:%remotePwd%@github.com/%remoteUser%/%projName%.git +rem use curl to create project +pushd ..\wwwroot +type PostDeployScripts\githubProject.json.template | sed -e s/\{WEB_SITE_NAME\}/%projName%/g > %TEMP%\githubProject.json +call curl -H "Content-Type: application/json" -u %remoteUser%:%remotePwd% -d "@%TEMP%\githubProject.json" -X POST %remoteUrl%/user/repos +rem rm %TEMP%\githubProject.json +popd + +popd +rem cd to project root +pushd ..\wwwroot + +rem init git +call git init +call git config user.name "%remoteUser%" +call git config user.password "%remotePwd%" +call git config user.email "util@botframework.com" +call git add . +call git commit -m "prepare to setup source control" +call git push %repoUrl% master +popd + + +rem cleanup git stuff +pushd ..\wwwroot +call rm -r -f .git +popd + +endlocal \ No newline at end of file diff --git a/PostDeployScripts/setupVsoRemoteRepo.cmd b/PostDeployScripts/setupVsoRemoteRepo.cmd new file mode 100644 index 0000000..4fa1ac1 --- /dev/null +++ b/PostDeployScripts/setupVsoRemoteRepo.cmd @@ -0,0 +1,50 @@ +@echo off +setlocal +rem ------------------------------------------------------------------------------------------ +rem setupVsoRemoteRepo [vsoRemote] [vsoUserName] [vsoPersonalAccessToken] [projName{optional}] +rem create and populate VSO git repo for the ABS code instance +rem +rem vsoRmote: url of the VSO site (e.g. https://awesomebot.visualstudio.com ) +rem vosUserName: user account name of the personal access token +rem vsoPersonalAccessToken: the personal access token used to access VSO REST api +rem projName the name of the project to create (default to WEBSITE_SITE_NAME) +rem ------------------------------------------------------------------------------------------ +set remoteUrl=%1 +set remoteUser=%2 +set remotePwd=%3 +set projName=%4 +if '%projName%'=='' set projName=%WEBSITE_SITE_NAME% +set vstsRoot=%remoteUrl% +set repoUrl=https://%remoteUser%:%remotePwd%@%remoteUrl:~8%/_git/%projName% +set vstsCreateProject=https://%remoteUser%:%remotePwd%@%remoteUrl:~8%/defaultcollection/_apis/projects?api-version=3.0 + +rem use curl to create project +pushd ..\wwwroot +type PostDeployScripts\vsoProject.json.template | sed -e s/\{WEB_SITE_NAME\}/%projName%/g > %TEMP%\vsoProject.json +call curl -H "Content-Type: application/json" -d "@%TEMP%\vsoProject.json" -X POST %vstsCreateProject% +rm %TEMP%\vsoProject.json +rem sleep for 15 seconds for the creation to complete, this is a wild guess +call sleep 15 +popd + +popd +rem cd to project root +pushd ..\wwwroot + +rem init git +call git init +call git config user.name "%remoteUser%" +call git config user.password "%remotePwd%" +call git config user.email "util@botframework.com" +call git add . +call git commit -m "prepare to setup source control" +call git push %repoUrl% master +popd + + +rem cleanup git stuff +pushd ..\wwwroot +call rm -r -f .git +popd + +endlocal \ No newline at end of file diff --git a/PostDeployScripts/vsoProject.json.template b/PostDeployScripts/vsoProject.json.template new file mode 100644 index 0000000..e7bcdc9 --- /dev/null +++ b/PostDeployScripts/vsoProject.json.template @@ -0,0 +1,12 @@ +{ + "name": "{WEB_SITE_NAME}", + "description": "{WEB_SITE_NAME} Azure Bot Service Code", + "capabilities": { + "versioncontrol": { + "sourceControlType": "Git" + }, + "processTemplate": { + "templateTypeId": "6b724908-ef14-45cf-84f8-768b5384da45" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce9f6f9 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +## Use Azure app service editor + +1. make code change in the online editor + +Your code changes go live as the code changes are saved. + +## Use Visual Studio Code + +### Build and debug +1. download source code zip and extract source in local folder +2. open the source folder in Visual Studio Code +3. make code changes +4. download and run [botframework-emulator](https://emulator.botframework.com/) +5. connect the emulator to http://localhost:3987 + +### Publish back + +``` +npm run azure-publish +``` + +## Use continuous integration + +If you have setup continuous integration, then your bot will automatically deployed when new changes are pushed to the source repository. + + + diff --git a/app.js b/app.js new file mode 100644 index 0000000..3f12061 --- /dev/null +++ b/app.js @@ -0,0 +1,284 @@ +/*----------------------------------------------------------------------------- +A simple echo bot for the Microsoft Bot Framework. +-----------------------------------------------------------------------------*/ +var restify = require('restify'); +var builder = require('botbuilder'); +var azure = require("botbuilder-azure"); +var builder_cognitiveservices = require('botbuilder-cognitiveservices'); + + +// 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 connector for communicating with the Bot Framework Service +var connector = new builder.ChatConnector({ + appId: process.env.MicrosoftAppId, + appPassword: process.env.MicrosoftAppPassword, + openIdMetadata: process.env.BotOpenIdMetadata +}); + +// Listen for messages from users +server.post('/api/messages', connector.listen()); + +/* Legacy table storage connection credentials to be removed at later point */ +// var tableName = 'botdata'; +// var azureTableClient = new azure.AzureTableClient(tableName, process.env['AzureWebJobsStorage']); +// var tableStorage = new azure.AzureBotStorage({ gzipData: false }, azureTableClient); + +// Cosmos Db connection credentials +var documentDbOptions = { + host: 'https://***********.documents.azure.com:443/', + masterKey: '*********************', + database: '**********', + collection: '*********' +}; +var docDbClient = new azure.DocumentDbClient(documentDbOptions, { + masterKey: documentDbOptions.masterKey +}); +var cosmosStorage = new azure.AzureBotStorage({ gzipData: false }, docDbClient); + +// Creates bot using Cosmos for saving state data +var bot = new builder.UniversalBot(connector).set('storage', cosmosStorage); + +// Calls bot upon start up +bot.on('conversationUpdate', function(message) { + if (message.membersAdded) { + message.membersAdded.forEach(function(identity) { + if (identity.id === message.address.bot.id) { + bot.beginDialog(message.address, '/'); + } + }); + } +}); + +const logUserConversation = (event) => { + console.log('message: ' + event.text + ', user: ' + event.address.user.name); + console.log("Event", JSON.stringify(event, null, 4)); +}; + +// Middleware for logging +bot.use({ + receive: function(event, next) { + console.log("Received from user:"); + logUserConversation(event); + next(); + }, + send: function(event, next) { + console.log("Sent by bot:"); + logUserConversation(event); + next(); + } +}); + +// Loads the bots opening message and graphic +bot.dialog('/', [ + function(session) { + var welcomeCard = new builder.HeroCard(session) + .title('How can I help you?') + .images([ + new builder.CardImage(session) + .url('https://2.bp.blogspot.com/-I0rdxZj_dwk/UFZQs22fSBI/AAAAAAAAAKw/byN1OWiehWI/s1600/ToffeeMocha.JPG') + .alt('Mocha') + ]) + .buttons([ + builder.CardAction.imBack(session, "order coffee", "Order a Coffee") + ]); + session.send(new builder.Message(session).addAttachment(welcomeCard)); + } +]) + +// Dialog for ordering coffee(s) and special actions +bot.dialog('orderCoffee', [ + function(session, args) { + if (!args.continueOrder) { + session.userData.cart = []; + } + session.send("At anytime you can say 'cancel order', 'view cart', or 'checkout'."); + builder.Prompts.choice(session, "What coffee would you like to order?", "Drip|Espresso|Mocha", { listStyle: builder.ListStyle.button }); + }, + function(session, results) { + session.dialogData.coffeeType = results.response.entity; + session.beginDialog('order' + session.dialogData.coffeeType.toString()); + }, + function(session, results) { + if (results.response) { + session.userData.cart.push(results.response); + } + session.replaceDialog('orderCoffee', { continueOrder: true }); + } + ]).triggerAction({ + matches: /order.*coffee/i, + confirmPrompt: "This will cancel the current order. Are you sure?" + }) + .cancelAction('cancelOrderAction', "Order canceled.", { + matches: /(cancel.*order|^cancel)/i, + confirmPrompt: "Are you sure?" + }) + .beginDialogAction('viewCartAction', 'viewCartDialog', { + matches: /view.*cart/i + }) + .beginDialogAction('checkoutAction', 'checkoutDialog', { + matches: /checkout/i, + matches: /check.*out/i + }); + +// Dialog for ordering drop coffee +bot.dialog('orderDrip', [ + function(session) { + session.dialogData.coffeeType = 'drip'; + session.send("Drip coffees only come in two sizes.") + builder.Prompts.choice(session, "Which size would you like?", "12 oz.|16 oz.", { listStyle: builder.ListStyle.button }); + }, + function(session, results) { + session.dialogData.coffeeSize = results.response.entity; + builder.Prompts.text(session, "What is your name? I'd like to add it to your order.") + }, + function(session, results) { + session.dialogData.customerName = results.response.charAt(0).toUpperCase() + results.response.slice(1).toLowerCase(); + var uuid = (S4() + S4() + "-" + S4() + "-4" + S4().substr(0, 3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase(); + var item = { + order: session.dialogData.coffeeSize + ' ' + session.dialogData.coffeeType + ' coffee', + coffee: session.dialogData.coffeeType, + size: session.dialogData.coffeeSize, + flavor: null, + shots: null, + name: session.dialogData.customerName, + guid: uuid + }; + session.send('\n* %s added for %s', item.order, item.name); + session.endDialogWithResult({ response: item }); + } +]).cancelAction('cancelItemAction', "Item canceled.", { + matches: /(cancel.*item|^cancel)/i +}); + +// Dialog for ordering espress coffee +bot.dialog('orderEspresso', [ + function(session) { + session.dialogData.coffeeType = 'espresso'; + builder.Prompts.choice(session, "What size?", "12 oz.|16 oz.|24 oz.", { listStyle: builder.ListStyle.button }); + }, + function(session, results) { + session.dialogData.coffeeSize = results.response.entity; + builder.Prompts.choice(session, "How many shots would you like?", "One|Two|Three|Four", { listStyle: builder.ListStyle.button }); + }, + function(session, results) { + session.dialogData.coffeeShots = results.response.entity.toLowerCase(); + builder.Prompts.text(session, "What is your name? I'd like to add it to your order.") + }, + function(session, results) { + session.dialogData.customerName = results.response.charAt(0).toUpperCase() + results.response.slice(1).toLowerCase(); + var uuid = (S4() + S4() + "-" + S4() + "-4" + S4().substr(0, 3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase(); + var item = { + order: session.dialogData.coffeeSize + ' ' + session.dialogData.coffeeType + ' with ' + + session.dialogData.coffeeShots + ' shot(s)', + coffee: session.dialogData.coffeeType, + size: session.dialogData.coffeeSize, + flavor: null, + shots: session.dialogData.coffeeShots, + name: session.dialogData.customerName, + guid: uuid + }; + session.send('\n* %s added for %s', item.order, item.name); + session.endDialogWithResult({ response: item }); + } +]).cancelAction('cancelItemAction', "Item canceled.", { + matches: /(cancel.*item|^cancel)/i +}); + +// Dialog for ordering a mocha coffee +bot.dialog('orderMocha', [ + function(session) { + session.dialogData.coffeeType = 'mocha'; + builder.Prompts.choice(session, "What size?", "12 oz.|16 oz.|24 oz.", { listStyle: builder.ListStyle.button }); + }, + function(session, results) { + session.dialogData.coffeeSize = results.response.entity; + builder.Prompts.choice(session, "Would you like to add a flavor?", "Vanilla|Hazelnut|Raspberry|None", { listStyle: builder.ListStyle.button }); + }, + function(session, results) { + session.dialogData.coffeeFlavor = results.response.entity.toLowerCase(); + builder.Prompts.choice(session, "How many shots would you like?", "One|Two|Three|Four", { listStyle: builder.ListStyle.button }); + }, + function(session, results) { + session.dialogData.coffeeShots = results.response.entity.toLowerCase(); + builder.Prompts.text(session, "What is your name? I'd like to add it to your order.") + }, + function(session, results) { + session.dialogData.customerName = results.response.charAt(0).toUpperCase() + results.response.slice(1).toLowerCase(); + var uuid = (S4() + S4() + "-" + S4() + "-4" + S4().substr(0, 3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase(); + if (session.dialogData.coffeeFlavor == 'none') { + var item = { + order: session.dialogData.coffeeSize + ' ' + session.dialogData.coffeeType + ' with ' + + 'no flavor added and ' + + session.dialogData.coffeeShots + ' shot(s)', + coffee: session.dialogData.coffeeType, + size: session.dialogData.coffeeSize, + flavor: 'none', + shots: session.dialogData.coffeeShots, + name: session.dialogData.customerName, + guid: uuid + }; + } else { + var item = { + order: session.dialogData.coffeeSize + ' ' + session.dialogData.coffeeType + ' with ' + + session.dialogData.coffeeFlavor + ' flavor and ' + + session.dialogData.coffeeShots + ' shot(s)', + coffee: session.dialogData.coffeeType, + size: session.dialogData.coffeeSize, + flavor: session.dialogData.coffeeFlavor, + shots: session.dialogData.coffeeShots, + name: session.dialogData.customerName, + guid: uuid + }; + } + session.send('\n* %s added for %s', item.order, item.name); + session.endDialogWithResult({ response: item }); + } +]).cancelAction('cancelItemAction', "Item canceled.", { + matches: /(cancel.*item|^cancel)/i +}); + +// Dialog for showing the users cart +bot.dialog('viewCartDialog', [ + function(session) { + var msg; + var cart = session.userData.cart; + if (cart.length > 0) { + msg = "Items in your cart:"; + for (var i = 0; i < cart.length; i++) { + msg += "\n* " + cart[i].order + " for " + cart[i].name; + } + } else { + msg = "Your cart is empty."; + } + session.endDialog(msg); + } +]); + +// Dialog for checking out +bot.dialog('checkoutDialog', [ + function(session) { + var cart = session.userData.cart; + if (cart.length > 0) { + session.send('Order confirmed.') + for (i = 0; i < cart.length; i++) { + session.send(` + Coffee: ${cart[i].order} + Customer name: ${cart[i].name}`) + } + } else { + msg = "Your cart is empty."; + } + delete session.userData.cart; + session.endConversation('Thank you for your order!'); + } +]); + +// Function for creating a guid +function S4() { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); +}; \ No newline at end of file diff --git a/iisnode.yml b/iisnode.yml new file mode 100644 index 0000000..cec5e5c --- /dev/null +++ b/iisnode.yml @@ -0,0 +1 @@ +nodeProcessCommandLine: "D:\Program Files (x86)\nodejs\6.9.1\node.exe" \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b83ae9d --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "coffeebot", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Steven Kanberg", + "license": "ISC", + "dependencies": { + "adaptivecards": "^1.0.0-beta11", + "azure-storage": "^2.8.0", + "botbuilder": "^3.13.1", + "botbuilder-azure": "^3.0.4", + "botbuilder-cognitiveservices": "^1.1.0", + "mssql": "^4.1.0", + "reflect-metadata": "^0.1.12", + "restify": "^5.0.0" + }, + "devDependencies": { + "request": "^2.81.0", + "typescript": "^2.7.2", + "zip-folder": "^1.0.0" + } +} \ No newline at end of file diff --git a/publish.js b/publish.js new file mode 100644 index 0000000..40f7b99 --- /dev/null +++ b/publish.js @@ -0,0 +1,52 @@ +var zipFolder = require('zip-folder'); +var path = require('path'); +var fs = require('fs'); +var request = require('request'); + +var rootFolder = path.resolve('.'); +var zipPath = path.resolve(rootFolder, '../ordercoffeebot.zip'); +var kuduApi = 'https://ordercoffeebot.scm.azurewebsites.net/api/zip/site/wwwroot'; +var userName = '$ordercoffeebot'; +var password = 'Kn7GndSjvbR2P5tv0HnWDnegit9yf66inRiL3eZv7zl0DnAmS5ucsb5G9pfy'; + +function uploadZip(callback) { + fs.createReadStream(zipPath).pipe(request.put(kuduApi, { + auth: { + username: userName, + password: password, + sendImmediately: true + }, + headers: { + "Content-Type": "applicaton/zip" + } + })) + .on('response', function(resp){ + if (resp.statusCode >= 200 && resp.statusCode < 300) { + fs.unlink(zipPath); + callback(null); + } else if (resp.statusCode >= 400) { + callback(resp); + } + }) + .on('error', function(err) { + callback(err) + }); +} + +function publish(callback) { + zipFolder(rootFolder, zipPath, function(err) { + if (!err) { + uploadZip(callback); + } else { + callback(err); + } + }) +} + +publish(function(err) { + if (!err) { + console.log('ordercoffeebot publish'); + } else { + console.error('failed to publish ordercoffeebot', err); + } +}); \ No newline at end of file diff --git a/web.config b/web.config new file mode 100644 index 0000000..10f2056 --- /dev/null +++ b/web.config @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +