├── 5-voting-service └── src │ ├── host.json │ ├── VoteNode │ ├── sample.dat │ ├── function.json │ └── index.js │ ├── CloseVotingNode │ ├── sample.dat │ ├── function.json │ └── index.js │ ├── CreateVotingNode │ ├── sample.dat │ ├── function.json │ └── index.js │ ├── DeleteVotingNode │ ├── sample.dat │ ├── function.json │ └── index.js │ ├── VotingStatusNode │ ├── sample.dat │ ├── function.json │ └── index.js │ ├── Sample Docs │ ├── vote.json │ └── voting.json │ ├── Content │ └── Images │ │ ├── Thumbs.db │ │ ├── CosmosDB-1.PNG │ │ ├── CosmosDB-2.PNG │ │ ├── CosmosDB-3.PNG │ │ ├── Architecture.PNG │ │ ├── Vote-Postman.PNG │ │ ├── CreateVotingTask.PNG │ │ ├── CloseVoting-Postman.PNG │ │ ├── SquireBotInAction.PNG │ │ ├── CreateVoting-Postman.PNG │ │ ├── DeleteVoting-Postman.PNG │ │ └── VotingStatus-Postman.PNG │ ├── local.settings.json │ ├── createfunction.sh │ └── createcosmosdb.sh ├── 5-voting-service.v1 ├── src │ ├── VoteNode │ │ ├── sample.dat │ │ ├── function.json │ │ └── index.js │ ├── host.json │ ├── CloseVotingNode │ │ ├── sample.dat │ │ ├── function.json │ │ └── index.js │ ├── CreateVotingNode │ │ ├── sample.dat │ │ ├── function.json │ │ └── index.js │ ├── DeleteVotingNode │ │ ├── sample.dat │ │ ├── function.json │ │ └── index.js │ ├── VotingStatusNode │ │ ├── sample.dat │ │ ├── function.json │ │ └── index.js │ ├── Sample Docs │ │ ├── vote.json │ │ └── voting.json │ ├── Content │ │ └── Images │ │ │ ├── Thumbs.db │ │ │ ├── CosmosDB-1.PNG │ │ │ ├── CosmosDB-2.PNG │ │ │ ├── CosmosDB-3.PNG │ │ │ ├── Architecture.PNG │ │ │ ├── Vote-Postman.PNG │ │ │ ├── CreateVotingTask.PNG │ │ │ ├── SquireBotInAction.PNG │ │ │ ├── CloseVoting-Postman.PNG │ │ │ ├── CreateVoting-Postman.PNG │ │ │ ├── DeleteVoting-Postman.PNG │ │ │ └── VotingStatus-Postman.PNG │ └── appsettings.json └── README.md ├── 6-scheduler-bot ├── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ └── 8.png ├── src │ ├── step-2 │ │ ├── index.snippet.js │ │ ├── index.js │ │ └── sample.json │ └── step-1 │ │ └── index.js └── README.md ├── 3-squirebot ├── images │ ├── cors-create.png │ ├── configure-bot.png │ ├── create-a-bot.png │ ├── container-create.png │ └── connection_string.png └── README.md ├── 4-github-bot ├── Content │ └── Images │ │ ├── 16-URL.png │ │ ├── 11-GitHub.png │ │ ├── 14-Slack.png │ │ ├── 17-Postman1.png │ │ ├── 18-Postman2.png │ │ ├── 19-SlackPost.png │ │ ├── 3-AzureMenu.png │ │ ├── 1-Architecutre.png │ │ ├── 10-JsonSchema.png │ │ ├── 12-GitHubFields.png │ │ ├── 2-AzureLogicApps.png │ │ ├── 20-LogicAppsRuns.png │ │ ├── 23-SquireBotSetup.PNG │ │ ├── 7-LogicAppRequest.png │ │ ├── 21-RequestResponse.PNG │ │ ├── 22-BotConversation.PNG │ │ ├── 15-SlackDyniamicValues.png │ │ ├── 24-GitHubIssueCreated.PNG │ │ ├── 4-AzureCreateLogicApp.png │ │ ├── 5-LogicAppParameters.png │ │ ├── 8-LogicAppRequestBody.png │ │ ├── 9-LogicAppJsonObject.png │ │ ├── 13-GitHubDyniamicValues.png │ │ └── 6-LogicAppBlankTemplate.png └── README.md ├── github-module ├── Content │ └── Images │ │ ├── 16-URL.png │ │ ├── 14-Slack.png │ │ ├── 11-GitHub.png │ │ ├── 17-Postman1.png │ │ ├── 18-Postman2.png │ │ ├── 3-AzureMenu.png │ │ ├── 10-JsonSchema.png │ │ ├── 19-SlackPost.png │ │ ├── 1-Architecutre.png │ │ ├── 12-GitHubFields.png │ │ ├── 2-AzureLogicApps.png │ │ ├── 20-LogicAppsRuns.png │ │ ├── 7-LogicAppRequest.png │ │ ├── 4-AzureCreateLogicApp.png │ │ ├── 5-LogicAppParameters.png │ │ ├── 8-LogicAppRequestBody.png │ │ ├── 9-LogicAppJsonObject.png │ │ ├── 13-GitHubDyniamicValues.png │ │ ├── 15-SlackDyniamicValues.png │ │ └── 6-LogicAppBlankTemplate.png └── README.md ├── 8-coder-cards ├── images │ └── function-bindings.png └── README.md ├── 7-photo-mosaic-bot ├── images │ ├── orlando-eye-both.jpg │ └── custom-vision-keys.png └── README.md ├── CHANGELOG.md ├── README.md ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── LICENSE.md ├── CONTRIBUTING.md ├── 1-intro-and-prereqs └── README.md ├── .gitignore └── 2-hello-functions └── README.md /5-voting-service/src/host.json: -------------------------------------------------------------------------------- 1 | { } -------------------------------------------------------------------------------- /5-voting-service/src/VoteNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/VoteNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/host.json: -------------------------------------------------------------------------------- 1 | {"id":"d877f79dd5e843d6af85a7c8ef0c1a42"} -------------------------------------------------------------------------------- /5-voting-service/src/CloseVotingNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/CloseVotingNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/CreateVotingNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/DeleteVotingNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/VotingStatusNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service/src/CreateVotingNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service/src/DeleteVotingNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service/src/VotingStatusNode/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /5-voting-service/src/Sample Docs/vote.json: -------------------------------------------------------------------------------- 1 | { 2 | "votingname": "pizzavote", 3 | "user": "don", 4 | "option": "Pepperoni" 5 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/Sample Docs/vote.json: -------------------------------------------------------------------------------- 1 | { 2 | "votingname": "pizzavote", 3 | "user": "don", 4 | "option": "Pepperoni" 5 | } -------------------------------------------------------------------------------- /6-scheduler-bot/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/6-scheduler-bot/images/1.png -------------------------------------------------------------------------------- /6-scheduler-bot/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/6-scheduler-bot/images/2.png -------------------------------------------------------------------------------- /6-scheduler-bot/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/6-scheduler-bot/images/3.png -------------------------------------------------------------------------------- /6-scheduler-bot/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/6-scheduler-bot/images/4.png -------------------------------------------------------------------------------- /6-scheduler-bot/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/6-scheduler-bot/images/5.png -------------------------------------------------------------------------------- /6-scheduler-bot/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/6-scheduler-bot/images/6.png -------------------------------------------------------------------------------- /6-scheduler-bot/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/6-scheduler-bot/images/7.png -------------------------------------------------------------------------------- /6-scheduler-bot/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/6-scheduler-bot/images/8.png -------------------------------------------------------------------------------- /3-squirebot/images/cors-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/3-squirebot/images/cors-create.png -------------------------------------------------------------------------------- /3-squirebot/images/configure-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/3-squirebot/images/configure-bot.png -------------------------------------------------------------------------------- /3-squirebot/images/create-a-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/3-squirebot/images/create-a-bot.png -------------------------------------------------------------------------------- /3-squirebot/images/container-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/3-squirebot/images/container-create.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/16-URL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/16-URL.png -------------------------------------------------------------------------------- /github-module/Content/Images/16-URL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/16-URL.png -------------------------------------------------------------------------------- /3-squirebot/images/connection_string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/3-squirebot/images/connection_string.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/11-GitHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/11-GitHub.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/14-Slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/14-Slack.png -------------------------------------------------------------------------------- /github-module/Content/Images/14-Slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/14-Slack.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/17-Postman1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/17-Postman1.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/18-Postman2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/18-Postman2.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/19-SlackPost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/19-SlackPost.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/3-AzureMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/3-AzureMenu.png -------------------------------------------------------------------------------- /8-coder-cards/images/function-bindings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/8-coder-cards/images/function-bindings.png -------------------------------------------------------------------------------- /github-module/Content/Images/11-GitHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/11-GitHub.png -------------------------------------------------------------------------------- /github-module/Content/Images/17-Postman1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/17-Postman1.png -------------------------------------------------------------------------------- /github-module/Content/Images/18-Postman2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/18-Postman2.png -------------------------------------------------------------------------------- /github-module/Content/Images/3-AzureMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/3-AzureMenu.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/1-Architecutre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/1-Architecutre.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/10-JsonSchema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/10-JsonSchema.png -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/Thumbs.db -------------------------------------------------------------------------------- /7-photo-mosaic-bot/images/orlando-eye-both.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/7-photo-mosaic-bot/images/orlando-eye-both.jpg -------------------------------------------------------------------------------- /github-module/Content/Images/10-JsonSchema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/10-JsonSchema.png -------------------------------------------------------------------------------- /github-module/Content/Images/19-SlackPost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/19-SlackPost.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/12-GitHubFields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/12-GitHubFields.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/2-AzureLogicApps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/2-AzureLogicApps.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/20-LogicAppsRuns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/20-LogicAppsRuns.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/23-SquireBotSetup.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/23-SquireBotSetup.PNG -------------------------------------------------------------------------------- /4-github-bot/Content/Images/7-LogicAppRequest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/7-LogicAppRequest.png -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/Thumbs.db -------------------------------------------------------------------------------- /7-photo-mosaic-bot/images/custom-vision-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/7-photo-mosaic-bot/images/custom-vision-keys.png -------------------------------------------------------------------------------- /github-module/Content/Images/1-Architecutre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/1-Architecutre.png -------------------------------------------------------------------------------- /github-module/Content/Images/12-GitHubFields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/12-GitHubFields.png -------------------------------------------------------------------------------- /github-module/Content/Images/2-AzureLogicApps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/2-AzureLogicApps.png -------------------------------------------------------------------------------- /github-module/Content/Images/20-LogicAppsRuns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/20-LogicAppsRuns.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/21-RequestResponse.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/21-RequestResponse.PNG -------------------------------------------------------------------------------- /4-github-bot/Content/Images/22-BotConversation.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/22-BotConversation.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/CosmosDB-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/CosmosDB-1.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/CosmosDB-2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/CosmosDB-2.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/CosmosDB-3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/CosmosDB-3.PNG -------------------------------------------------------------------------------- /github-module/Content/Images/7-LogicAppRequest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/7-LogicAppRequest.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/15-SlackDyniamicValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/15-SlackDyniamicValues.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/24-GitHubIssueCreated.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/24-GitHubIssueCreated.PNG -------------------------------------------------------------------------------- /4-github-bot/Content/Images/4-AzureCreateLogicApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/4-AzureCreateLogicApp.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/5-LogicAppParameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/5-LogicAppParameters.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/8-LogicAppRequestBody.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/8-LogicAppRequestBody.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/9-LogicAppJsonObject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/9-LogicAppJsonObject.png -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/CosmosDB-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/CosmosDB-1.PNG -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/CosmosDB-2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/CosmosDB-2.PNG -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/CosmosDB-3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/CosmosDB-3.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/Architecture.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/Architecture.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/Vote-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/Vote-Postman.PNG -------------------------------------------------------------------------------- /github-module/Content/Images/4-AzureCreateLogicApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/4-AzureCreateLogicApp.png -------------------------------------------------------------------------------- /github-module/Content/Images/5-LogicAppParameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/5-LogicAppParameters.png -------------------------------------------------------------------------------- /github-module/Content/Images/8-LogicAppRequestBody.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/8-LogicAppRequestBody.png -------------------------------------------------------------------------------- /github-module/Content/Images/9-LogicAppJsonObject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/9-LogicAppJsonObject.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/13-GitHubDyniamicValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/13-GitHubDyniamicValues.png -------------------------------------------------------------------------------- /4-github-bot/Content/Images/6-LogicAppBlankTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/4-github-bot/Content/Images/6-LogicAppBlankTemplate.png -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/Architecture.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/Architecture.PNG -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/Vote-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/Vote-Postman.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/CreateVotingTask.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/CreateVotingTask.PNG -------------------------------------------------------------------------------- /github-module/Content/Images/13-GitHubDyniamicValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/13-GitHubDyniamicValues.png -------------------------------------------------------------------------------- /github-module/Content/Images/15-SlackDyniamicValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/15-SlackDyniamicValues.png -------------------------------------------------------------------------------- /github-module/Content/Images/6-LogicAppBlankTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/github-module/Content/Images/6-LogicAppBlankTemplate.png -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/CreateVotingTask.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/CreateVotingTask.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/CloseVoting-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/CloseVoting-Postman.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/SquireBotInAction.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/SquireBotInAction.PNG -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/SquireBotInAction.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/SquireBotInAction.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/CreateVoting-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/CreateVoting-Postman.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/DeleteVoting-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/DeleteVoting-Postman.PNG -------------------------------------------------------------------------------- /5-voting-service/src/Content/Images/VotingStatus-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service/src/Content/Images/VotingStatus-Postman.PNG -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/CloseVoting-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/CloseVoting-Postman.PNG -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/CreateVoting-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/CreateVoting-Postman.PNG -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/DeleteVoting-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/DeleteVoting-Postman.PNG -------------------------------------------------------------------------------- /5-voting-service.v1/src/Content/Images/VotingStatus-Postman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tikyau/azure-serverless-workshop-team-assistant/master/5-voting-service.v1/src/Content/Images/VotingStatus-Postman.PNG -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /5-voting-service.v1/src/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "votingbot_DOCUMENTDB": "AccountEndpoint=https://<<<>>>.documents.azure.com:443/;AccountKey=<<<>>>;" } 6 | } -------------------------------------------------------------------------------- /5-voting-service/src/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "votingbot_DOCUMENTDB": "AccountEndpoint=https://<<<>>>.documents.azure.com:443/;AccountKey=<<<>>>;" 6 | } 7 | } -------------------------------------------------------------------------------- /5-voting-service/src/Sample Docs/voting.json: -------------------------------------------------------------------------------- 1 | { 2 | "votingname": "pizzavote", 3 | "name": "Pizza Voting", 4 | "isOpen": true, 5 | "question": "What pizza do you want?", 6 | "options": [ 7 | { 8 | "text": "Pepperoni" 9 | }, 10 | { 11 | "text": "Mushrooms" 12 | }, 13 | { 14 | "text": "Margherita" 15 | }, 16 | { 17 | "text": "Quattro Stagioni" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/Sample Docs/voting.json: -------------------------------------------------------------------------------- 1 | { 2 | "votingname": "pizzavote", 3 | "name": "Pizza Voting", 4 | "isOpen": true, 5 | "question": "What pizza do you want?", 6 | "options": [ 7 | { 8 | "text": "Pepperoni" 9 | }, 10 | { 11 | "text": "Mushrooms" 12 | }, 13 | { 14 | "text": "Margherita" 15 | }, 16 | { 17 | "text": "Quattro Stagioni" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /6-scheduler-bot/src/step-2/index.snippet.js: -------------------------------------------------------------------------------- 1 | var scheduledEvents = []; 2 | 3 | //Add all scheduled events into an array 4 | req.body.forEach(function (calendar) { 5 | calendar.items.forEach(function (items) { 6 | scheduledEvents.push({ 7 | start: new Date(items['start']).toLocaleTimeString('en-US', { hour12: false }), 8 | end: new Date(items['end']).toLocaleTimeString('en-US', { hour12: false }) 9 | }); 10 | context.log(items['start']); 11 | context.log(new Date(items['start']).getUTCHours()); 12 | }) 13 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Serverless Workshop 2 | 3 | This is a serverless workshop that will take you through 8 modules involving building a serverless personal assistant in Azure. 4 | 5 | ## Table of contents 6 | 7 | 1. [Intro and Prerequisites](1-intro-and-prereqs/README.md) 8 | 2. [Hello Functions](2-hello-functions/README.md) 9 | 3. [Serverless Bot](3-squirebot/README.md) 10 | 4. [GitHub Assistant](4-github-bot/README.md) 11 | 5. [Voting Service](5-voting-service/README.md) 12 | 6. [Schedule Assistant](6-scheduler-bot/README.md) 13 | 7. [Photo Mosaic Bot](7-photo-mosaic-bot/README.md) 14 | 8. [Extra Credit: Coder Cards](8-coder-cards/README.md) 15 | -------------------------------------------------------------------------------- /5-voting-service.v1/src/DeleteVotingNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | }, 15 | { 16 | "type": "documentDB", 17 | "name": "inputDocument", 18 | "databaseName": "votingbot", 19 | "collectionName": "votingbot", 20 | "sqlQuery": "SELECT * from c where c.id = {id}", 21 | "connection": "votingbot_DOCUMENTDB", 22 | "direction": "in" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /5-voting-service/src/DeleteVotingNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | }, 15 | { 16 | "type": "cosmosDB", 17 | "name": "inputDocument", 18 | "databaseName": "votingbot", 19 | "collectionName": "votingbot", 20 | "sqlQuery": "SELECT * from c where c.id = {id}", 21 | "connectionStringSetting": "votingbot_DOCUMENTDB", 22 | "direction": "in" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/VotingStatusNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ "post" ], 9 | "name": "req" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | }, 16 | { 17 | "type": "documentDB", 18 | "name": "inputDocument", 19 | "databaseName": "votingbot", 20 | "collectionName": "votingbot", 21 | "sqlQuery": "SELECT * from c where c.id = {id}", 22 | "connection": "votingbot_DOCUMENTDB", 23 | "direction": "in" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /5-voting-service/src/VotingStatusNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ "post" ], 9 | "name": "req" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | }, 16 | { 17 | "type": "cosmosDB", 18 | "name": "inputDocument", 19 | "databaseName": "votingbot", 20 | "collectionName": "votingbot", 21 | "sqlQuery": "SELECT * from c where c.id = {id}", 22 | "connectionStringSetting": "votingbot_DOCUMENTDB", 23 | "direction": "in" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/CreateVotingNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "methods": [ "post" ], 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | }, 15 | { 16 | "type": "documentDB", 17 | "name": "outputDocument", 18 | "databaseName": "votingbot", 19 | "collectionName": "votingbot", 20 | "createIfNotExists": true, 21 | "connection": "votingbot_DOCUMENTDB", 22 | "direction": "out", 23 | "partitionKey": "/votingname" 24 | } 25 | ], 26 | "disabled": false 27 | } -------------------------------------------------------------------------------- /5-voting-service/src/CreateVotingNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "methods": [ "post" ], 8 | "name": "req" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | }, 15 | { 16 | "type": "cosmosDB", 17 | "name": "outputDocument", 18 | "databaseName": "votingbot", 19 | "collectionName": "votingbot", 20 | "createIfNotExists": true, 21 | "connectionStringSetting": "votingbot_DOCUMENTDB", 22 | "direction": "out", 23 | "partitionKey": "/votingname" 24 | } 25 | ], 26 | "disabled": false 27 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/VoteNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ "post" ], 9 | "name": "req" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | }, 16 | { 17 | "type": "documentDB", 18 | "name": "inputDocument", 19 | "databaseName": "votingbot", 20 | "collectionName": "votingbot", 21 | "sqlQuery": "SELECT * from c where c.id = {id}", 22 | "connection": "votingbot_DOCUMENTDB", 23 | "direction": "in" 24 | }, 25 | { 26 | "type": "documentDB", 27 | "name": "outputDocument", 28 | "databaseName": "votingbot", 29 | "collectionName": "votingbot", 30 | "connection": "votingbot_DOCUMENTDB", 31 | "direction": "out" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /5-voting-service.v1/src/CloseVotingNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ "post" ], 9 | "name": "req" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | }, 16 | { 17 | "type": "documentDB", 18 | "name": "inputDocument", 19 | "databaseName": "votingbot", 20 | "collectionName": "votingbot", 21 | "sqlQuery": "SELECT * from c where c.id = {id}", 22 | "connection": "votingbot_DOCUMENTDB", 23 | "direction": "in" 24 | }, 25 | { 26 | "type": "documentDB", 27 | "name": "outputDocument", 28 | "databaseName": "votingbot", 29 | "collectionName": "votingbot", 30 | "connection": "votingbot_DOCUMENTDB", 31 | "direction": "out" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /5-voting-service/src/VoteNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ "post" ], 9 | "name": "req" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | }, 16 | { 17 | "type": "cosmosDB", 18 | "name": "inputDocument", 19 | "databaseName": "votingbot", 20 | "collectionName": "votingbot", 21 | "sqlQuery": "SELECT * from c where c.id = {id}", 22 | "connectionStringSetting": "votingbot_DOCUMENTDB", 23 | "direction": "in" 24 | }, 25 | { 26 | "type": "cosmosDB", 27 | "name": "outputDocument", 28 | "databaseName": "votingbot", 29 | "collectionName": "votingbot", 30 | "connectionStringSetting": "votingbot_DOCUMENTDB", 31 | "direction": "out" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /5-voting-service/src/CloseVotingNode/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ "post" ], 9 | "name": "req" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | }, 16 | { 17 | "type": "cosmosDB", 18 | "name": "inputDocument", 19 | "databaseName": "votingbot", 20 | "collectionName": "votingbot", 21 | "sqlQuery": "SELECT * from c where c.id = {id}", 22 | "connectionStringSetting": "votingbot_DOCUMENTDB", 23 | "direction": "in" 24 | }, 25 | { 26 | "type": "cosmosDB", 27 | "name": "outputDocument", 28 | "databaseName": "votingbot", 29 | "collectionName": "votingbot", 30 | "connectionStringSetting": "votingbot_DOCUMENTDB", 31 | "direction": "out" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /5-voting-service/src/createfunction.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set variables 4 | resourceGroupName='votingbot' 5 | location='eastus' 6 | storageAccountName='<<<>>>' 7 | functionAppName='<<<>>>' 8 | votingBotCosmosDBConnStr='<<<>>>' 9 | databaseName='votingbot' 10 | collectionName='votingbot' 11 | partitionkeypath='/votingname' 12 | 13 | # Create an azure storage account 14 | az storage account create \ 15 | --name $storageAccountName \ 16 | --location $location \ 17 | --resource-group $resourceGroupName \ 18 | --sku Standard_LRS 19 | 20 | # Create Function App 21 | az functionapp create \ 22 | --name $functionAppName \ 23 | --storage-account $storageAccountName \ 24 | --consumption-plan-location $location \ 25 | --resource-group $resourceGroupName 26 | 27 | # Configure the function for v2 and add the cosmosDB connection string 28 | az functionapp config appsettings set --name $functionAppName \ 29 | --resource-group $resourceGroupName \ 30 | --settings FUNCTIONS_EXTENSION_VERSION=beta votingbot_COSMOSDB=$votingBotCosmosDBConnStr -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /5-voting-service/src/createcosmosdb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set variables for the new account, database, and collection 4 | resourceGroupName='votingbot' 5 | location='eastus' 6 | databaseAccountname='<<<>>>' 7 | databaseName='votingbot' 8 | collectionName='votingbot' 9 | partitionkeypath='/votingname' 10 | 11 | # Create a resource group 12 | az group create \ 13 | --name $resourceGroupName \ 14 | --location $location 15 | 16 | # Create a DocumentDB API Cosmos DB account 17 | az cosmosdb create \ 18 | --name $databaseAccountname \ 19 | --resource-group $resourceGroupName 20 | 21 | # Create a database 22 | az cosmosdb database create \ 23 | --name $databaseAccountname \ 24 | --db-name $databaseName \ 25 | --resource-group $resourceGroupName 26 | 27 | # Create a collection 28 | az cosmosdb collection create \ 29 | --collection-name $collectionName \ 30 | --partition-key-path $partitionkeypath \ 31 | --name $databaseAccountname \ 32 | --db-name $databaseName \ 33 | --throughput 400 \ 34 | --resource-group $resourceGroupName 35 | 36 | # Get the database account connection strings 37 | az cosmosdb list-keys \ 38 | --name $databaseAccountname \ 39 | --resource-group $resourceGroupName -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 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 -------------------------------------------------------------------------------- /5-voting-service/src/CreateVotingNode/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | 4 | if (req.body && req.body.votingname && req.body.question && req.body.options) { 5 | var body = req.body; 6 | var votingname = body.votingname.replace(/\s/g,'').toLowerCase(); 7 | body.votingname = votingname; 8 | body.id = votingname; 9 | var optionsValues = req.body.options.replace(/\s/g,'').split(","); 10 | var options = []; 11 | for(var i=0; i< optionsValues.length; i++){ 12 | var option = {}; 13 | option.text = optionsValues[i]; 14 | option.votes = 0; 15 | option.voters = []; 16 | options.push(option); 17 | } 18 | 19 | body.options = options; 20 | 21 | context.bindings.outputDocument = body; 22 | 23 | var responseBody = {}; 24 | responseBody.voting = body; 25 | responseBody.message = "Wow! Voting with id '" + votingname + "' was created!"; 26 | 27 | context.res = { 28 | status: 201, 29 | body:responseBody 30 | }; 31 | } 32 | else { 33 | context.res = { 34 | status: 400, 35 | body: { "message" : "Please pass a voting object in the request body"} 36 | }; 37 | } 38 | context.done(); 39 | }; -------------------------------------------------------------------------------- /5-voting-service.v1/src/CreateVotingNode/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | 4 | if (req.body && req.body.votingname && req.body.question && req.body.options) { 5 | var body = req.body; 6 | var votingname = body.votingname.replace(/\s/g,'').toLowerCase(); 7 | body.votingname = votingname; 8 | body.id = votingname; 9 | var optionsValues = req.body.options.replace(/\s/g,'').split(","); 10 | var options = []; 11 | for(var i=0; i< optionsValues.length; i++){ 12 | var option = {}; 13 | option.text = optionsValues[i]; 14 | option.votes = 0; 15 | option.voters = []; 16 | options.push(option); 17 | } 18 | 19 | body.options = options; 20 | 21 | context.bindings.outputDocument = body; 22 | 23 | var responseBody = {}; 24 | responseBody.voting = body; 25 | responseBody.message = "Wow! Voting with id '" + votingname + "' was created!"; 26 | 27 | context.res = { 28 | status: 201, 29 | body:responseBody 30 | }; 31 | } 32 | else { 33 | context.res = { 34 | status: 400, 35 | body: { "message" : "Please pass a voting object in the request body"} 36 | }; 37 | } 38 | context.done(); 39 | }; -------------------------------------------------------------------------------- /5-voting-service/src/VotingStatusNode/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | 4 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 5 | { 6 | var voting = context.bindings.inputDocument[0]; 7 | 8 | var message = "Here we go, these are the current results - "; 9 | 10 | for(var i=0; i < voting.options.length; i++){ 11 | var votes = voting.options[i].votes; 12 | message += voting.options[i].text + " has " + votes + (votes === 1 ? " vote" : " votes"); 13 | if(i < voting.options.length-1 ) { message += ", "} 14 | } 15 | 16 | var responseBody = { 17 | "voting" : { 18 | "votingname" : voting.votingname, 19 | "isOpen" : voting.isOpen, 20 | "question" : voting.question, 21 | "options" : voting.options, 22 | "id" : voting.id 23 | }, 24 | "message" : message 25 | }; 26 | 27 | context.res = { 28 | status : 200, 29 | body : responseBody 30 | }; 31 | context.done(null, context.res); 32 | } 33 | else { 34 | context.res = { 35 | status : 400, 36 | body: { "message" : "Record with this id can not be found. Please pass an id of an existing document in the request body" } 37 | }; 38 | context.done(null, context.res); 39 | } 40 | }; -------------------------------------------------------------------------------- /5-voting-service.v1/src/VotingStatusNode/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | 4 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 5 | { 6 | var voting = context.bindings.inputDocument[0]; 7 | 8 | var message = "Here we go, these are the current results - "; 9 | 10 | for(var i=0; i < voting.options.length; i++){ 11 | var votes = voting.options[i].votes; 12 | message += voting.options[i].text + " has " + votes + (votes === 1 ? " vote" : " votes"); 13 | if(i < voting.options.length-1 ) { message += ", "} 14 | } 15 | 16 | var responseBody = { 17 | "voting" : { 18 | "votingname" : voting.votingname, 19 | "isOpen" : voting.isOpen, 20 | "question" : voting.question, 21 | "options" : voting.options, 22 | "id" : voting.id 23 | }, 24 | "message" : message 25 | }; 26 | 27 | context.res = { 28 | status : 200, 29 | body : responseBody 30 | }; 31 | context.done(null, context.res); 32 | } 33 | else { 34 | context.res = { 35 | status : 400, 36 | body: { "message" : "Record with this id can not be found. Please pass an id of an existing document in the request body" } 37 | }; 38 | context.done(null, context.res); 39 | } 40 | }; -------------------------------------------------------------------------------- /5-voting-service.v1/src/CloseVotingNode/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | 4 | if (req.body && req.body.id && req.body.isOpen != null) { 5 | 6 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 7 | { 8 | var voting = context.bindings.inputDocument[0]; 9 | context.bindings.outputDocument = voting; 10 | context.bindings.outputDocument.isOpen = req.body.isOpen; 11 | 12 | var responseBody = { 13 | "voting" : { 14 | "votingname" : voting.votingname, 15 | "isOpen" : voting.isOpen, 16 | "question" : voting.question, 17 | "options" : voting.options, 18 | "id" : voting.id 19 | }, 20 | "message" : "Nice! Voting with id '" + req.body.id + "' was updated!" 21 | }; 22 | 23 | context.res = { 24 | status: 200, 25 | body: responseBody 26 | }; 27 | context.done(null, context.res); 28 | } 29 | else { 30 | context.res = { 31 | status: 400, 32 | body: { "message" : "Record with this votingname can not be found. Please pass a votingname of an existing document in the request body"} 33 | }; 34 | context.done(null, context.res); 35 | }; 36 | } 37 | else { 38 | res = { 39 | status: 400, 40 | body: "Please pass a votingname and isOpen value in the request body" 41 | }; 42 | 43 | context.done(null, res); 44 | } 45 | }; -------------------------------------------------------------------------------- /5-voting-service/src/CloseVotingNode/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | 4 | if (req.body && req.body.id && req.body.isOpen != null) { 5 | 6 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 7 | { 8 | var voting = context.bindings.inputDocument[0]; 9 | context.bindings.outputDocument = voting; 10 | context.bindings.outputDocument.isOpen = req.body.isOpen; 11 | 12 | var responseBody = { 13 | "voting" : { 14 | "votingname" : voting.votingname, 15 | "isOpen" : voting.isOpen, 16 | "question" : voting.question, 17 | "options" : voting.options, 18 | "id" : voting.id 19 | }, 20 | "message" : "Nice! Voting with id '" + req.body.id + "' was updated!" 21 | }; 22 | 23 | context.res = { 24 | status: 200, 25 | body: responseBody 26 | }; 27 | context.done(null, context.res); 28 | } 29 | else { 30 | context.res = { 31 | status: 400, 32 | body: { "message" : "Record with this votingname can not be found. Please pass a votingname of an existing document in the request body"} 33 | }; 34 | context.done(null, context.res); 35 | }; 36 | } 37 | else { 38 | res = { 39 | status: 400, 40 | body: "Please pass a votingname and isOpen value in the request body" 41 | }; 42 | 43 | context.done(null, res); 44 | } 45 | }; -------------------------------------------------------------------------------- /5-voting-service/src/DeleteVotingNode/index.js: -------------------------------------------------------------------------------- 1 | var documentClient = require("documentdb").DocumentClient; 2 | var connectionString = process.env["votingbot_DOCUMENTDB"]; 3 | var arr = connectionString.split(';'); 4 | var endpoint = arr[0].split('=')[1]; 5 | var primaryKey = arr[1].split('=')[1] + "=="; 6 | var collectionUrl = 'dbs/votingbot/colls/votingbot'; 7 | var client = new documentClient(endpoint, { "masterKey": primaryKey }); 8 | 9 | module.exports = function (context, req) { 10 | context.log('JavaScript HTTP trigger function processed a request.'); 11 | 12 | if (req.body && req.body.id) { 13 | if(context.bindings.inputDocument && context.bindings.inputDocument.length == 1) { 14 | deleteDocument(req.body.id, context.bindings.inputDocument[0].id).then((result) => { 15 | console.log(`Deleted document: ${req.body.id}`); 16 | context.res = { 17 | status : 201, 18 | body: { "message" : `Deleted document: ${req.body.id}` } 19 | }; 20 | context.done(null, context.res); 21 | }, 22 | (err) => { 23 | context.log('error: ', err); 24 | context.res = { 25 | body: {"message" : "Error: " + JSON.stringify(err) } 26 | }; 27 | context.done(null, context.res); 28 | }); 29 | } 30 | else { 31 | context.res = { 32 | status : 400, 33 | body: { "message" : "Record with this id can not be found. Please pass a id of an existing document in the request body" } 34 | }; 35 | context.done(null, context.res); 36 | } 37 | } 38 | else { 39 | res = { 40 | status: 400, 41 | body: {"message" : "Please pass a name on the query string or in the request body" } 42 | }; 43 | context.done(null, res); 44 | } 45 | }; 46 | 47 | function deleteDocument(partitionKey, id) { 48 | let documentUrl = `${collectionUrl}/docs/${id}`; 49 | console.log(`Deleting document:\n${id}\n`); 50 | 51 | return new Promise((resolve, reject) => { 52 | client.deleteDocument(documentUrl, { 53 | partitionKey: [partitionKey] }, (err, result) => { 54 | if (err) reject(err); 55 | else { 56 | resolve(result); 57 | } 58 | }); 59 | }); 60 | }; -------------------------------------------------------------------------------- /5-voting-service.v1/src/DeleteVotingNode/index.js: -------------------------------------------------------------------------------- 1 | var documentClient = require("documentdb").DocumentClient; 2 | var connectionString = process.env["votingbot_DOCUMENTDB"]; 3 | var arr = connectionString.split(';'); 4 | var endpoint = arr[0].split('=')[1]; 5 | var primaryKey = arr[1].split('=')[1] + "=="; 6 | var collectionUrl = 'dbs/votingbot/colls/votingbot'; 7 | var client = new documentClient(endpoint, { "masterKey": primaryKey }); 8 | 9 | module.exports = function (context, req) { 10 | context.log('JavaScript HTTP trigger function processed a request.'); 11 | 12 | if (req.body && req.body.id) { 13 | if(context.bindings.inputDocument && context.bindings.inputDocument.length == 1) { 14 | deleteDocument(req.body.id, context.bindings.inputDocument[0].id).then((result) => { 15 | console.log(`Deleted document: ${req.body.id}`); 16 | context.res = { 17 | status : 201, 18 | body: { "message" : `Deleted document with id ${req.body.id}` } 19 | }; 20 | context.done(null, context.res); 21 | }, 22 | (err) => { 23 | context.log('error: ', err); 24 | context.res = { 25 | body: {"message" : "Error: " + JSON.stringify(err) } 26 | }; 27 | context.done(null, context.res); 28 | }); 29 | } 30 | else { 31 | context.res = { 32 | status : 400, 33 | body: { "message" : "Record with this id can not be found. Please pass a id of an existing document in the request body" } 34 | }; 35 | context.done(null, context.res); 36 | } 37 | } 38 | else { 39 | res = { 40 | status: 400, 41 | body: {"message" : "Please pass a name on the query string or in the request body" } 42 | }; 43 | context.done(null, res); 44 | } 45 | }; 46 | 47 | function deleteDocument(partitionKey, id) { 48 | let documentUrl = `${collectionUrl}/docs/${id}`; 49 | console.log(`Deleting document:\n${id}\n`); 50 | 51 | return new Promise((resolve, reject) => { 52 | client.deleteDocument(documentUrl, { 53 | partitionKey: [partitionKey] }, (err, result) => { 54 | if (err) reject(err); 55 | else { 56 | resolve(result); 57 | } 58 | }); 59 | }); 60 | }; -------------------------------------------------------------------------------- /6-scheduler-bot/src/step-1/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | var scheduledEvents = [ 4 | { 5 | "start": "8:00:00", 6 | "end": "14:00:00" 7 | } 8 | ]; 9 | 10 | //TODO: Change scheduled events to events from Google Calendar 11 | 12 | //Sort all events chronologically 13 | scheduledEvents.sort(compare); 14 | 15 | var schedule = findSlotInDay(scheduledEvents); 16 | 17 | //Find first slot 18 | context.log(JSON.stringify(schedule)); 19 | 20 | context.res = { 21 | body: { 22 | availableSlots: schedule.length, 23 | availableTime: schedule 24 | } 25 | }; 26 | 27 | context.done(); 28 | }; 29 | 30 | //Data comparison function 31 | function compare(a, b) { 32 | if (a['start'] < b['start']) { 33 | return -1; 34 | } 35 | if (a['start'] > b['start']) { 36 | return 1; 37 | } 38 | return 0; 39 | } 40 | 41 | //Courtesy of StackOverflow 42 | //https://stackoverflow.com/questions/19277348/how-to-calculate-free-available-time-based-on-array-values 43 | function findSlotInDay(scheduledEvents) { 44 | var schedule = []; 45 | var start = '08:00:00'; 46 | var end = '17:00:00'; 47 | var finalEnd = '17:00:00' 48 | for(var i=0, l=scheduledEvents.length; i 0){ 74 | m -= 1; 75 | }else{ 76 | if(h > 0){ 77 | h -= 1; 78 | }else{ 79 | return false; 80 | } 81 | m = 59; 82 | } 83 | 84 | if(h < 10) 85 | h = '0'+h; 86 | 87 | if(m < 10) 88 | m = '0'+m; 89 | 90 | return h+':'+m+':00'; 91 | } 92 | 93 | function addMinute(time){ 94 | var h = +time.substr(0, 2); 95 | var m = +time.substr(3, 2); 96 | 97 | if(m < 59){ 98 | m += 1; 99 | }else{ 100 | if(h < 22){ 101 | h += 1; 102 | }else{ 103 | return false; 104 | } 105 | m = 0; 106 | } 107 | 108 | if(h < 10) 109 | h = '0'+h; 110 | 111 | if(m < 10) 112 | m = '0'+m; 113 | 114 | return h+':'+m+':00'; 115 | } -------------------------------------------------------------------------------- /5-voting-service/src/VoteNode/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | 4 | if (req.body && req.body.id && req.body.user && req.body.option) { 5 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 6 | { 7 | var body = context.bindings.inputDocument[0]; 8 | var found = false; 9 | var alreadyset = false; 10 | for (var index = 0; index < body.options.length; ++index) { 11 | if (body.options[index].text.toLowerCase() == req.body.option.toLowerCase()) { 12 | found = true; 13 | for (var index2 = 0; index2 < body.options[index].voters.length; index2++) { 14 | if (body.options[index].voters[index2].toLowerCase() == req.body.user.toLowerCase()) { 15 | context.res = { 16 | status: 201, 17 | body: { "message" : "Vote was already there, nothing updated" } 18 | }; 19 | alreadyset = true; 20 | break; 21 | } 22 | } 23 | if (found & !alreadyset){ 24 | body.options[index].votes++; 25 | body.options[index].voters.push(req.body.user); 26 | } 27 | break; 28 | } 29 | } 30 | if (found & !alreadyset){ 31 | context.bindings.outputDocument = body; 32 | 33 | var responseBody = { 34 | "voting" : { 35 | "votingname" : body.votingname, 36 | "isOpen" : body.isOpen, 37 | "question" : body.question, 38 | "options" : body.options, 39 | "id" : body.id 40 | }, 41 | "message" : "Nice! Your vote was counted!" 42 | }; 43 | 44 | context.res = { 45 | status: 201, 46 | body: responseBody 47 | }; 48 | } 49 | else { 50 | if (!alreadyset){ 51 | context.res = { 52 | status: 400, 53 | body: { "message" : "No vote option found with value " + req.body.option + " in voting session " + req.body.id } 54 | } 55 | } 56 | } 57 | } 58 | else { 59 | context.res = { 60 | status: 400, 61 | body: { "message" : "Record with this id can not be found. Please pass a id of an existing document in the request body" } 62 | }; 63 | context.done(null, context.res); 64 | }; 65 | } 66 | else { 67 | context.res = { 68 | status: 400, 69 | body: { "message" : "Please pass a vote object in the request body" } 70 | }; 71 | } 72 | context.done(); 73 | }; -------------------------------------------------------------------------------- /5-voting-service.v1/src/VoteNode/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | 4 | if (req.body && req.body.id && req.body.user && req.body.option) { 5 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 6 | { 7 | var body = context.bindings.inputDocument[0]; 8 | var found = false; 9 | var alreadyset = false; 10 | for (var index = 0; index < body.options.length; ++index) { 11 | if (body.options[index].text.toLowerCase() == req.body.option.toLowerCase()) { 12 | found = true; 13 | for (var index2 = 0; index2 < body.options[index].voters.length; index2++) { 14 | if (body.options[index].voters[index2].toLowerCase() == req.body.user.toLowerCase()) { 15 | context.res = { 16 | status: 201, 17 | body: { "message" : "Vote was already there, nothing updated" } 18 | }; 19 | alreadyset = true; 20 | break; 21 | } 22 | } 23 | if (found & !alreadyset){ 24 | body.options[index].votes++; 25 | body.options[index].voters.push(req.body.user); 26 | } 27 | break; 28 | } 29 | } 30 | if (found & !alreadyset){ 31 | context.bindings.outputDocument = body; 32 | 33 | var responseBody = { 34 | "voting" : { 35 | "votingname" : body.votingname, 36 | "isOpen" : body.isOpen, 37 | "question" : body.question, 38 | "options" : body.options, 39 | "id" : body.id 40 | }, 41 | "message" : "Nice! Your vote was counted!" 42 | }; 43 | 44 | context.res = { 45 | status: 201, 46 | body: responseBody 47 | }; 48 | } 49 | else { 50 | if (!alreadyset){ 51 | context.res = { 52 | status: 400, 53 | body: { "message" : "No vote option found with value " + req.body.option + " in voting session " + req.body.id } 54 | } 55 | } 56 | } 57 | } 58 | else { 59 | context.res = { 60 | status: 400, 61 | body: { "message" : "Record with this id can not be found. Please pass a id of an existing document in the request body" } 62 | }; 63 | context.done(null, context.res); 64 | }; 65 | } 66 | else { 67 | context.res = { 68 | status: 400, 69 | body: { "message" : "Please pass a vote object in the request body" } 70 | }; 71 | } 72 | context.done(); 73 | }; -------------------------------------------------------------------------------- /6-scheduler-bot/src/step-2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req) { 2 | context.log('JavaScript HTTP trigger function processed a request.'); 3 | var scheduledEvents = []; 4 | 5 | //Add all scheduled events into an array 6 | req.body.forEach(function (calendar) { 7 | calendar.items.forEach(function (items) { 8 | scheduledEvents.push({ 9 | start: new Date(items['start']).toLocaleTimeString('en-US', { hour12: false }), 10 | end: new Date(items['end']).toLocaleTimeString('en-US', { hour12: false }) 11 | }); 12 | context.log(items['start']); 13 | context.log(new Date(items['start']).getUTCHours()); 14 | }) 15 | }); 16 | 17 | //Sort all events chronologically 18 | scheduledEvents.sort(compare); 19 | 20 | var schedule = findSlotInDay(scheduledEvents); 21 | 22 | //Find first slot 23 | context.log(JSON.stringify(schedule)); 24 | 25 | context.res = { 26 | body: { 27 | availableSlots: schedule.length, 28 | availableTime: schedule 29 | } 30 | }; 31 | 32 | context.done(); 33 | }; 34 | 35 | //Data comparison function 36 | function compare(a, b) { 37 | if (a['start'] < b['start']) { 38 | return -1; 39 | } 40 | if (a['start'] > b['start']) { 41 | return 1; 42 | } 43 | return 0; 44 | } 45 | 46 | //Courtesy of StackOverflow 47 | //https://stackoverflow.com/questions/19277348/how-to-calculate-free-available-time-based-on-array-values 48 | function findSlotInDay(scheduledEvents) { 49 | var schedule = []; 50 | var start = '08:00:00'; 51 | var end = '17:00:00'; 52 | var finalEnd = '17:00:00' 53 | for(var i=0, l=scheduledEvents.length; i 0){ 79 | m -= 1; 80 | }else{ 81 | if(h > 0){ 82 | h -= 1; 83 | }else{ 84 | return false; 85 | } 86 | m = 59; 87 | } 88 | 89 | if(h < 10) 90 | h = '0'+h; 91 | 92 | if(m < 10) 93 | m = '0'+m; 94 | 95 | return h+':'+m+':00'; 96 | } 97 | 98 | function addMinute(time){ 99 | var h = +time.substr(0, 2); 100 | var m = +time.substr(3, 2); 101 | 102 | if(m < 59){ 103 | m += 1; 104 | }else{ 105 | if(h < 22){ 106 | h += 1; 107 | }else{ 108 | return false; 109 | } 110 | m = 0; 111 | } 112 | 113 | if(h < 10) 114 | h = '0'+h; 115 | 116 | if(m < 10) 117 | m = '0'+m; 118 | 119 | return h+':'+m+':00'; 120 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 6 | 7 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /6-scheduler-bot/src/step-2/sample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "kind": "calendar#events", 4 | "etag": "\"p32k9vse4muitc0g\"", 5 | "summary": "azureserverlessdemo@gmail.com", 6 | "updated": "2017-09-14T19:42:05.529Z", 7 | "timeZone": "America/Los_Angeles", 8 | "accessRole": "owner", 9 | "defaultReminders": [ 10 | { 11 | "method": "popup", 12 | "minutes": 30 13 | } 14 | ], 15 | "items": [ 16 | { 17 | "kind": "calendar#event", 18 | "etag": "\"3010835784790000\"", 19 | "id": "783ka7ijdonf7jmeteorolhr1d", 20 | "status": "confirmed", 21 | "htmlLink": "https://www.google.com/calendar/event?eid=Nzgza2E3aWpkb25mN2ptZXRlb3JvbGhyMWQgYXp1cmVzZXJ2ZXJsZXNzZGVtb0Bt", 22 | "created": "2017-09-14T19:38:12Z", 23 | "updated": "2017-09-14T19:38:12.395Z", 24 | "summary": "Busy", 25 | "creator": "azureserverlessdemo@gmail.com", 26 | "organizer": "azureserverlessdemo@gmail.com", 27 | "start": "2017-09-15T16:30:00+00:00", 28 | "end": "2017-09-15T18:30:00+00:00", 29 | "iCalUID": "783ka7ijdonf7jmeteorolhr1d@google.com", 30 | "sequence": 0, 31 | "reminders": { 32 | "useDefault": true 33 | }, 34 | "description": "", 35 | "location": "", 36 | "attendees": "", 37 | "endTimeUnspecified": false 38 | }, 39 | { 40 | "kind": "calendar#event", 41 | "etag": "\"3010835790684000\"", 42 | "id": "1q62gsq6nj8c45fu9go2k1vd99", 43 | "status": "confirmed", 44 | "htmlLink": "https://www.google.com/calendar/event?eid=MXE2MmdzcTZuajhjNDVmdTlnbzJrMXZkOTkgYXp1cmVzZXJ2ZXJsZXNzZGVtb0Bt", 45 | "created": "2017-09-14T19:38:15Z", 46 | "updated": "2017-09-14T19:38:15.342Z", 47 | "summary": "Busy", 48 | "creator": "azureserverlessdemo@gmail.com", 49 | "organizer": "azureserverlessdemo@gmail.com", 50 | "start": "2017-09-15T18:30:00+00:00", 51 | "end": "2017-09-15T21:30:00+00:00", 52 | "iCalUID": "1q62gsq6nj8c45fu9go2k1vd99@google.com", 53 | "sequence": 0, 54 | "reminders": { 55 | "useDefault": true 56 | }, 57 | "description": "", 58 | "location": "", 59 | "attendees": "", 60 | "endTimeUnspecified": false 61 | } 62 | ] 63 | }, 64 | { 65 | "kind": "calendar#events", 66 | "etag": "\"p320fjftlmuitc0g\"", 67 | "summary": "Thiago Almeida", 68 | "description": "Thiago Almeida's calendar", 69 | "updated": "2017-09-14T19:41:33.264Z", 70 | "timeZone": "America/Los_Angeles", 71 | "accessRole": "owner", 72 | "defaultReminders": [], 73 | "items": [ 74 | { 75 | "kind": "calendar#event", 76 | "etag": "\"3010835814630000\"", 77 | "id": "1r5b5g7239ah5e1ekenlksmf7a", 78 | "status": "confirmed", 79 | "htmlLink": "https://www.google.com/calendar/event?eid=MXI1YjVnNzIzOWFoNWUxZWtlbmxrc21mN2EgdWptcXZyNW91azhwOW5taWEybzRoNm8zM29AZw", 80 | "created": "2017-09-14T19:38:20Z", 81 | "updated": "2017-09-14T19:38:28.653Z", 82 | "summary": "Busy", 83 | "creator": "azureserverlessdemo@gmail.com", 84 | "organizer": "ujmqvr5ouk8p9nmia2o4h6o33o@group.calendar.google.com", 85 | "start": "2017-09-15T21:30:00+00:00", 86 | "end": "2017-09-15T23:00:00+00:00", 87 | "iCalUID": "1r5b5g7239ah5e1ekenlksmf7a@google.com", 88 | "sequence": 0, 89 | "reminders": { 90 | "useDefault": false, 91 | "overrides": [ 92 | { 93 | "method": "popup", 94 | "minutes": 30 95 | } 96 | ] 97 | }, 98 | "description": "", 99 | "location": "", 100 | "attendees": "", 101 | "endTimeUnspecified": false 102 | } 103 | ] 104 | } 105 | ] -------------------------------------------------------------------------------- /1-intro-and-prereqs/README.md: -------------------------------------------------------------------------------- 1 | # Introduction and setup 2 | 3 | To begin the workshop, we'll have you set up all the various accounts and download any prereqs you don't already have. 4 | 5 | ## 1. Accounts 6 | 7 | This workshop relies upon a few services that are provided by various entities. They should all have a free option to get started. 8 | 9 | ### Azure Subscription 10 | 11 | In the the workshop, we'll be using Azure to run and host our serverless application in the cloud. If you don't have an Azure subscrition, you can get a 12 month free trial [here](https://azure.microsoft.com/en-us/free/?v=17.39a). If you have any issues, please ask an instructor for help. 12 | 13 | ### Microsoft Bot Framework account 14 | 15 | In the workshop, in order for your bot to talk with the world, we'll be using Microsoft bot framework, which acts as a middlelayer to talk to all the various chat services (Slack, Facebook, Microsoft teams, Kik, etc.) with a single unified API. The service is free to get started, but you will need a Microsoft account to login. You can login to the bot service [here](https://dev.botframework.com/bots), and if you don't have a Microsoft account, you can sign up for one [here](https://account.microsoft.com/account) 16 | 17 | ### Slack or similar chat service 18 | 19 | In the workshop, we're using Slack to prove we connected the bot, but it is 100% optional. If you want a Slack account, you can make one which will allow a small number of intergations [here](https://slack.com/create#email). You can use the Bot Framework's developer chat interface or any of the following interfaces: Bing, Cortana, Email, Facebook, GroupMe, Kik, Skype, Skype for Business, Slack, SMS, Microsoft Teams, Telegram, WeChat, WebChat 20 | 21 | You can learn more about what is supported at each level via the [Bot Framework channel inspector docs](https://docs.microsoft.com/en-us/bot-framework/portal-channel-inspector). 22 | 23 | ### GitHub Account 24 | 25 | In the workshop, we assume you have a GitHub Account. If that isn't the case, please create one [here](https://github.com/join?source=header-home). 26 | 27 | ## 2. Developer tools 28 | 29 | Today you'll need the following developer tools: 30 | - [git](https://git-scm.com/downloads) 31 | - [Node 8.5.0](https://nodejs.org/en/download/releases/) 32 | - [dotnet core](https://www.microsoft.com/net/download/core) 33 | - [Azure Functions Core Tools 2.0 aka @core](https://www.npmjs.com/package/azure-functions-core-tools) 34 | - npm i -g azure-functions-core-tools@core 35 | - [Bot Framework Emulator](https://github.com/Microsoft/BotFramework-Emulator/releases/tag/v3.5.31). 36 | * NOTE: If you have a problem with the latest Mac installers, you can install the older release [botframework\-emulator\-3\.5\.19\-mac\.zip](https://github.com/Microsoft/BotFramework-Emulator/releases/download/v3.5.19/botframework-emulator-3.5.19-mac.zip). The emulator will automatically download updates when it launches, and you simply have to restart it once that is complete. 37 | - [Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) 38 | - [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/). 39 | - A modern browser 40 | - A REST API tool (cURL or [Postman](https://www.getpostman.com/) will do) 41 | - Visual Studio, either: 42 | - [Visual Studio 2017 Update 3](https://www.visualstudio.com/downloads/) with the Azure workload installed (Windows) 43 | - [Visual Studio Code](https://code.visualstudio.com/download) with the [C# extension](https://code.visualstudio.com/docs/languages/csharp) (Mac/Linux) 44 | - If you'd prefer to use a different tool than VS Code, that's totally fine. You might need some help getting debugging working later on, but it's not required. 45 | 46 | ## 3. Introduction 47 | 48 | In today's workshop, we'll be building a new service, end to end, with serverless technologies. The problem we'll be trying to solve is that today it's hard to build a simple webhook interface with all the various chat services today in a universal manner. We have to have slightly different implementations depending on which service you're talking to, be it Slack, Facebook, Teams, etc. Our solution is to build a SaaS service around the Microsoft Bot Framework which allows for users to create 1 webhook based service and it will work with all the bot providers. It will also allow for you to ask certain prompts from users in order to determine what the inputs to the webhook call will be. 49 | 50 | The outcome for this workshop should be an improved knowledge on how to build RESTful APIs with Azure Functions, a deeper understanding of Logic Apps, and practice with building services on top of other SaaS services like Bot Framework and Cognitive Services. 51 | 52 | ### Architecture 53 | 54 | 55 | -------------------------------------------------------------------------------- /github-module/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Issue Bot 2 | 3 | In this part of the workshop you will extend our bot with the capability to create issue in GitHub. 4 | We will build logic app with 3 steps. We will use Request / Response Step Connector for invoking the Logic App. Then we will pass the HTTP payload to GitHub Connector and create an issue. 5 | The last and final step will be to post issue’s link to the Slack channel. Please refer to the diagram below showing the steps in logic app. 6 | 7 | ![Architecture](Content/Images/1-Architecutre.png) 8 | 9 | ## Features 10 | 11 | This project provides the following features: 12 | 13 | * Create GitHub Issue 14 | * Post link to the issue back to Slack 15 | 16 | ## Getting Started 17 | 18 | ### Prerequisites 19 | 20 | 1. You need to have GitHub account, if you don’t please create one here https://github.com 21 | 22 | 2. Access to Slack account, please verify it here – https://slack.com 23 | 24 | 3. Access to Azure Subscription, please check here https://portal.azure.com 25 | 26 | 27 | ### Walkthrough 28 | 29 | - Login to Azure Portal and Create new Logic App 30 | 31 | - If you don’t see it on your left menu you can search for it via More Services on the bottom 32 | 33 | ![Azure Menu](Content/Images/2-AzureLogicApps.png) 34 | 35 | - Then type Logic Apps in the Search Bar 36 | 37 | ![Logic App Search](Content/Images/3-AzureMenu.png) 38 | 39 | - Press Add button 40 | 41 | ![Add Logic App](Content/Images/4-AzureCreateLogicApp.png) 42 | 43 | - And fill the required data and press the create button at the bottom 44 | 45 | ![Logic App Parameters](Content/Images/5-LogicAppParameters.png) 46 | 47 | - Next, we will configure the Logic App. In the Logic Apps Designer please select “Blank Logic App” from the Templates section 48 | 49 | ![Logic App Parameters](Content/Images/6-LogicAppBlankTemplate.png) 50 | 51 | - In the Connector Search select Request / Response Connector 52 | 53 | ![Logic App Request](Content/Images/7-LogicAppRequest.png) 54 | 55 | - Press “Use sample payload to generate schema” button 56 | 57 | ![Logic App Request Body](Content/Images/8-LogicAppRequestBody.png) 58 | 59 | - And paste the following JSON 60 | 61 | ```javascript 62 | { 63 | "title": "My new issue", 64 | "text": "My new issue description" 65 | } 66 | ``` 67 | 68 | - The contron should look like the picture below. Next press Done 69 | 70 | ![JSON object](Content/Images/9-LogicAppJsonObject.png) 71 | 72 | - The result will look like 73 | 74 | ![JSON schema](Content/Images/10-JsonSchema.png) 75 | 76 | - Then press “New Step” button, select “Add an action” and select GitHub Connector with Action – “Create an issue” 77 | 78 | ![Github step](Content/Images/11-GitHub.png) 79 | 80 | - Next, please select Sign In and input your GitHub account credentials and fill the required data that will be used for creating the GitHub issue 81 | 82 | ![Github data](Content/Images/12-GitHubFields.png) 83 | 84 | - For title and body will pick dynamic content fields - title and text 85 | 86 | ![Github dynamic data](Content/Images/13-GitHubDyniamicValues.png) 87 | 88 | - Press “New step” and select Slack with Action – “Post message” 89 | 90 | ![Slack post message](Content/Images/14-Slack.png) 91 | 92 | - Press Sign In and provide your Slack credentials and confirm the requested permissions 93 | 94 | - Select the channel you want to use and format the message following similar pattern as shown below. We will use dynamic content “Id” field to retrieve issue id for creating the link. Use the link to your repo in the beggining of the expression. 95 | 96 | ![Slack dynamic data](Content/Images/15-SlackDyniamicValues.png) 97 | 98 | - Save your work and now you are ready to test. Go to the Request / Response connector step and copy the URL 99 | ![URL capturing](Content/Images/16-URL.png) 100 | 101 | - If you want to your logic app immediatelly, go to Postman or similar app and POST to the provided URL. Do not forget to set Content-Type header to “application/json” 102 | ![Postman testing 1](Content/Images/17-Postman1.png) 103 | 104 | - For the body use the JSON schema we defined several steps ago 105 | ![Postman testing 2](Content/Images/18-Postman2.png) 106 | 107 | - You should receive the message in Slack as follows 108 | ![Slack post](Content/Images/19-SlackPost.png) 109 | 110 | - You can go to your Logic App home screen and see the history of execution and troubleshoot 111 | ![Logic app executions](Content/Images/20-LogicAppsRuns.png) 112 | 113 | 114 | Congratulations! You competed the module! 115 | 116 | You have just added GitHub issue creation as a new capability of our always improving bot! 117 | 118 | Thank you! 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /4-github-bot/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Issue Bot 2 | 3 | In this part of the workshop you will extend our bot with the capability to create an issue in GitHub. 4 | We will build a logic app with 3 steps. We will use Request / Response Step Connector for invoking the Logic App and providing the final response where our second step will be creating a GitHub issue. 5 | Please refer to the diagram below showing the steps in logic app. 6 | 7 | ![Architecture](Content/Images/1-Architecutre.png) 8 | 9 | ## Features 10 | 11 | This project provides the following features: 12 | 13 | * Create GitHub Issue 14 | * Provide link to the created GitHub issue in the response 15 | 16 | ## Getting Started 17 | 18 | ### Prerequisites 19 | 20 | 1. You need to have GitHub account with a repo, if you don’t please create one here https://github.com - or you can use the provided account: 21 | Username: azureserverlessdemo@gmail.com 22 | Password: S3rverless1 23 | 24 | 2. Access to Azure Subscription, please check here https://portal.azure.com 25 | 26 | 3. Bot Framework Emulator - https://github.com/Microsoft/BotFramework-Emulator/releases/ 27 | 28 | ### Walkthrough 29 | 30 | - Login to Azure Portal and Create new Logic App 31 | 32 | - If you don’t see it on your left menu you can search for it via More Services on the bottom 33 | 34 | ![Azure Menu](Content/Images/2-AzureLogicApps.png) 35 | 36 | - Then type Logic Apps in the Search Bar 37 | 38 | ![Logic App Search](Content/Images/3-AzureMenu.png) 39 | 40 | - Press Add button 41 | 42 | ![Add Logic App](Content/Images/4-AzureCreateLogicApp.png) 43 | 44 | - And fill the required data and press Create button at the bottom 45 | 46 | ![Logic App Parameters](Content/Images/5-LogicAppParameters.png) 47 | 48 | - Next, we will configure the Logic App. In the Logic Apps Designer please select “Blank Logic App” from the Templates section 49 | 50 | ![Logic App Parameters](Content/Images/6-LogicAppBlankTemplate.png) 51 | 52 | - In the Connector Search select Request / Response Connector 53 | 54 | ![Logic App Request](Content/Images/7-LogicAppRequest.png) 55 | 56 | - Press “Use sample payload to generate schema” button 57 | 58 | ![Logic App Request Body](Content/Images/8-LogicAppRequestBody.png) 59 | 60 | - And paste the following JSON 61 | 62 | ```javascript 63 | { 64 | "title": "My new issue", 65 | "text": "My new issue description" 66 | } 67 | ``` 68 | 69 | - The contron should look like the picture below. Next press Done 70 | 71 | ![JSON object](Content/Images/9-LogicAppJsonObject.png) 72 | 73 | - The result will look like 74 | 75 | ![JSON schema](Content/Images/10-JsonSchema.png) 76 | 77 | - Then press “New Step” button, select “Add an action” and select GitHub Connector with Action – “Create an issue” 78 | 79 | ![Github step](Content/Images/11-GitHub.png) 80 | 81 | - Next, please select Sign In and input your GitHub account credentials and fill the required data that will be used for creating the GitHub issue 82 | 83 | ![Github data](Content/Images/12-GitHubFields.png) 84 | 85 | - For title and body will pick dynamic content fields - title and text. Also set the correct repository and its owner. If using the associated GitHub account (azureservlessdemo@gmail.com) there is a repo already set up. You can use this info **only if you authenticated with the demo account**. If not use a repo in your own GitHub. 86 | Repo Owner: azuerserverlessdemo 87 | Repo Name: squirebotdemo 88 | Title: You can pass in the title from the trigger here. 89 | Text: You can pass in the text from the trigger here. 90 | 91 | ![Github dynamic data](Content/Images/13-GitHubDyniamicValues.png) 92 | 93 | - Press “New step” and search for Response and select Request - Response step: 94 | 95 | ![Slack post message](Content/Images/21-RequestResponse.PNG) 96 | 97 | - Next we will configure the step by selecting 200 for response code, specifying the repository and its owner. It is also very important to define the body with the following json object. Squire bot will expect message propery in the body object to display the final message to the user: 98 | 99 | ```javascript 100 | { 101 | 102 | "message": "[GitHub Issue Link](https://github.com/{input-repo-owner}/{input-repo}/issues/@{body('Create_an_issue')?['number']})" 103 | 104 | } 105 | ``` 106 | 107 | - We use dynamic value for issue ID when constructing the link. We use Markdown format for the message content: 108 | 109 | ![Slack post message](Content/Images/14-Slack.png) 110 | 111 | - Save your work and now you are ready to test. Go to the Request / Response connector step and copy the URL 112 | ![URL capturing](Content/Images/16-URL.png) 113 | 114 | - If you want to your logic app immediatelly, go to Postman or similar app and POST to the provided URL. Do not forget to set Content-Type header to “application/json” 115 | 116 | ![Postman testing 1](Content/Images/17-Postman1.png) 117 | 118 | - For the body use the JSON schema we defined several steps ago 119 | ![Postman testing 2](Content/Images/18-Postman2.png) 120 | 121 | - You should receive the following reponse in Postman: 122 | ![Slack post](Content/Images/19-SlackPost.png) 123 | 124 | - You can go to your Logic App home screen and see the history of execution and troubleshoot 125 | ![Logic app executions](Content/Images/20-LogicAppsRuns.png) 126 | 127 | - Now it is time to integrate GitHub Bot with the Squire Bot. Please follow the instructions for running locally the Squire Bot. And then copy the POST URL of the logic app we created in order to setup new task for Squire Bot. It should look very similar to the image below: 128 | ![Squire Bot GitHub Setup](Content/Images/23-SquireBotSetup.PNG) 129 | 130 | - Next start Bot Framework Emulator and connect to http://localhost:7071/api/bot . And now are you are ready to test GitHub Issuer via the Squire Bot. Please type the name of the task you created to start the conversation and the interaction should be very similar to the screenshot below: 131 | 132 | ![Squire Bot GitHub Task Test](Content/Images/22-BotConversation.PNG) 133 | 134 | 135 | - And if you click on the link in the last message you will be able to open the GitHub Issue we just created via Squire Bot: 136 | 137 | ![GitHub Issue](Content/Images/24-GitHubIssueCreated.PNG) 138 | 139 | Congratulations! You competed the module! 140 | 141 | You have just added GitHub issue creation as a new capability of our always improving bot! 142 | 143 | Thank you! 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /6-scheduler-bot/README.md: -------------------------------------------------------------------------------- 1 | # Add a team schedule module to your bot 2 | 3 | ## Overview 4 | 5 | In this module we will use Azure Functions and Logic Apps to build in functionality that integrates with team calendars. The idea is you can ask your bot to find a suitable meeting time between teammates, and it will respond back with available times. 6 | 7 | The eventual flow will be: 8 | 1. Bot is notified of a command to find schedules `Schedule appointment` for `jeff,thiago` 9 | 1. Logic Apps goes and grabs the calendar details for each person provided 10 | 1. A Function is called to calculate availability in calendars 11 | 1. A response is returned to the user with available times 12 | 13 | To simplify the lab we already have a Google account setup with 2 users whos calendars we are going to look into. However, the lab could work with any number of google calendars - as long as you have an account that can access them. The Google API requires you reference the calendar by the calendar ID, like `azureserverlessdemo@gmail.com` would be the main calendar for that account. In a real-world solution we would likely store a "friendly name" for the calendar - but for the purpose of simplicity we will keep the full calendar ID for this module. 14 | 15 | First let's build a function that can calculate available timeslots after recieving a list of scheduled appointments. This function will find all timeslots during a day between 8am and 5pm. 16 | 17 | ## Building the function 18 | 19 | Let's build a function to find available time slots when the schedule is hard-coded in. 20 | 21 | 1. In your function app on your machine, create a new javascript function called `SchedulerBot` 22 | * `func new` -> `JavaScript` -> `HttpTrigger` 23 | 1. Open the new function in Visual Studio Code: `code .` 24 | 1. In the new `index.js` file, overwrite with the following: [code snippet](src/step-1/index.js) 25 | * This code will run and return any available slot within a set of scheduled events. 26 | 1. Test to make sure the function works 27 | * `func host start` -> do a POST on the exposed URL locally `http://localhost:7071/api/SchedulerBot` 28 | 29 | **Response from step-1 index.js** 30 | 31 | ```javascript 32 | { 33 | "availableSlots": 1, 34 | "availableTime": [ 35 | { 36 | "start": "14:01:00", 37 | "end": "17:00:00" 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | This is stating that there is one available timeslot between 2:01pm and 5:00pm. Now let's replace the function so the schedule will be passed in and dynamically generated. This will work when our Logic App is able to pass in the schedule information from Google Calendar. 44 | 45 | 1. Go back to the `index.js` file and replace the 46 | 47 | ```javascript 48 | var scheduledEvents = [ 49 | { 50 | "start": "8:00:00", 51 | "end": "14:00:00" 52 | } 53 | ]; 54 | 55 | //TODO: Change scheduled events to events from Google Calendar 56 | ``` 57 | 58 | with this snippet: [code snippet](src/step-2/index.snippet.js) (the full index.js should look [like this](src/step-2/index.js)) 59 | 60 | 1. Run the function again after saving changes, and this time when you POST, post with the following JSON body: [sample JSON](src/step-2/sample.json). This is what the Logic App will generate. 61 | 62 | **Response from step-2 index.js with sample.json body** 63 | ```javascript 64 | { 65 | "availableSlots": 2, 66 | "availableTime": [ 67 | { 68 | "start": "08:00:00", 69 | "end": "09:29:00" 70 | }, 71 | { 72 | "start": "16:01:00", 73 | "end": "17:00:00" 74 | } 75 | ] 76 | } 77 | ``` 78 | 1. Publish the function app to Azure 79 | * `func azure functionapp publish {yourAzureFunctionAppName}` 80 | 81 | Now the function can correctly return back available times - we just need to write a Logic App to pull in calendar data. 82 | 83 | ## Building the Logic App 84 | 85 | So we have a Google Account setup with the following schedule for calendars from Jeff (azureserverlessdemo - the green items) and Thiago (ujmqvr5.... - the yellow ones). We need to pull *both* of their agendas and push this data to our Function to calculate when there is an available meeting time between 8am and 5pm. Here's a sample of both of their calendars: 86 | 87 | ![agenda](images/8.png) 88 | 89 | Do this we are going to use Azure Logic Apps and their connectors with services like Google Calendar. We will also be doing a small "scatter-gather" pattern by spinning a worker for each calendar and then aggregating the results for all calendars. 90 | 91 | 1. Go to the [Azure Portal](https://portal.azure.com) 92 | 1. Create a new logic app called `scheduler-bot` in any region you prefer 93 | 1. Open the logic app and add a `Request` trigger 94 | 1. Click on the `Use sample payload to generate schema` button to specify the shape of the request. 95 | 1. Paste in the following example request from the bot: 96 | ```json 97 | { 98 | "people": "azureserverlessdemo@gmail.com,ujmqvr5ouk8p9nmia2o4h6o33o@group.calendar.google.com" 99 | } 100 | ``` 101 | This is specifying that the bot will send in a "people" parameter. It will have a single value that is **comma seperated**. We'll need to split it up in the app. 102 | 1. Now we need to initialize a variable that will store each event schedule. Add a step for **Initialize variable**. Name the variable **schedules**, make it an Array, and you can leave the value empty. 103 | ![](images/4.png) 104 | 1. Add a New step, and under **..More** select **Add a for each** as we need to grab calendar details FOR EACH of the `people` from the trigger. However, if you recall the "people" we are sending in with the sample above are all in a single property. They are seperated by a comma. So we need to "split" the people by commas. Doing a split will return an array, something like: `['person1', 'person2']` which will allow us to iterate over each person. While we could write an Azure Function to do this, there is a simple [workflow definition language](http://aka.ms/logicappsdocs) to do basic transformations like this. 105 | 1. In the `Select an output from previous steps`, select the **Expression** tab on the right and type in the following expression to split the people by a `,`: `split(triggerBody()['people'], ',')` -> then press **OK** 106 | **HINT**: If you don't see expressions, zoom your browser out. You may be in "responsive" mode. Note that zooming out the design surface will not work, you MUST zoom out the browser. 107 | ![](images/5.png) 108 | 1. Add an action - **Google Calendar - List the events on a calendar** 109 | ![google calendar action](images/1.png) 110 | 1. Sign in with the following account: 111 | * username: `azureserverlessdemo@gmail.com` 112 | * password: `s3rverless1` 113 | 114 | You may be asked to verify the account when you login. If so, please see a proctor. 115 | 116 | 1. For the **Calendar ID** select **Enter custom value**. Choose another expression to get the current item of the foreach loop. The expression is: `item()`. Open the expression editor tab again and type in `item()` 117 | 1. Add another step in the foreach to **Append to array variable** - append the **Event List** to the "schedules" array. 118 | ![](images/6.png) 119 | 1. After/outside the foreach, call the function to evaluate the responses. 120 | * Add a function, select your app, select the `ScheduleBot` function 121 | * Pass in the **schedules** variable to the function 122 | 1. Add a response after the function, paste in the following to return a message to the bot. This will just return a stringified version of the response body from the Function Step (called `SchedulerBot`): 123 | ```json 124 | { 125 | "message": "@{body('SchedulerBot')}" 126 | } 127 | ``` 128 | ![](images/7.png) 129 | 1. Save the logic app, and copy the trigger invoke URL from the trigger. If you want before registering with the bot you can debug by using a REST client (like Postman) and POSTing to the Logic App URL with sample payload from step 5 above. However you can just register it to your bot to see it work live if you want. 130 | 131 | ## Train the bot 132 | Go to the Squire UX and add a new skill. 133 | 134 | 135 | |Field|Value| 136 | |--|--| 137 | |Title|Schedule appointment| 138 | |Description|Get available time to meet with people| 139 | |Method|POST| 140 | |URL|*Copy the URL from your Logic app trigger*| 141 | |Parameter Name|people| 142 | |Parameter Prompt|Who do you want to schedule it with? (Comma seperated)| 143 | 144 | 145 | Go to your bot and ask it to `Schedule appointment`. 146 | 147 | When it asks with whom, paste in the following 2 google calendars: 148 | `azureserverlessdemo@gmail.com,ujmqvr5ouk8p9nmia2o4h6o33o@group.calendar.google.com` 149 | 150 | We could continue to improve the bot by adding in some aliases for the Logic App via CosmosDB or some other store. For instance `jeff` could substitute to `azureserverlessdemo@gmail.com`, making the bot easier to communicate with. For sake of simplicity we will leave as-is for now. -------------------------------------------------------------------------------- /8-coder-cards/README.md: -------------------------------------------------------------------------------- 1 | # Coder Cards: geek trading card generator 2 | 3 | CoderCards is a geek trading card generator. It uses Microsoft Cognitive Services to detect the predominant emotion in a face, which is used to choose a card back. 4 | 5 | ## Prerequisites 6 | 7 | 1. Visual Studio, either: 8 | - [Visual Studio 2017 Update 3](https://www.visualstudio.com/downloads/) with the Azure workload installed (Windows) 9 | - [Visual Studio Code](https://code.visualstudio.com/download) with the [C# extension](https://code.visualstudio.com/docs/languages/csharp) (Mac/Linux) 10 | 11 | 1. If running on a Mac/Linux, [.NET Core 2.0](https://www.microsoft.com/net/core#macos) 12 | 13 | 1. If running on a Mac/Linx, install [azure\-functions\-core\-tools](https://www.npmjs.com/package/azure-functions-core-tools)@core from npm. For more information, see https://aka.ms/func-xplat. 14 | 15 | 1. [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/). 16 | 17 | 1. [Bot Framework Emulator](https://github.com/Microsoft/BotFramework-Emulator/releases/). 18 | 19 | * NOTE: There's a problem with the latest Mac installers. So, install the older release [botframework\-emulator\-3\.5\.19\-mac\.zip](https://github.com/Microsoft/BotFramework-Emulator/releases/download/v3.5.19/botframework-emulator-3.5.19-mac.zip). The emulator will automatically download updates when it launches, and you simply have to restart it once that is complete. 20 | 21 | 1. Azure Storage Account 22 | 23 | 1. [Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) 24 | 25 | ## About CoderCards 26 | 27 | * There are two functions defined in this project: 28 | * **RequestImageProcessing**. HTTP trigger that writes a queue message. The request payload must be in the following form: 29 | 30 | ```json 31 | { 32 | "PersonName": "Scott Guthrie", 33 | "Title": "Red Polo Connoisseur", 34 | "BlobName": "Scott Guthrie-Red Polo Connoisseur.jpg" 35 | } 36 | ``` 37 | 38 | * **GenerateCard**. Queue trigger that binds to the blob specified in the BlobName property of the queue payload. Based on the predominant emotion of the input image, it generates a card using one of 4 card templates. 39 | 40 | * The card is written to the output blob container specified by the app setting `output-container`. 41 | 42 | Here's a visualization of the bindings, using the [Azure Functions Bindings Visualizer](https://functions-visualizer.azurewebsites.net): 43 | 44 | ![Functions bindings](images/function-bindings.png) 45 | 46 | ## 1. Create Emotion API key 47 | 48 | 1. Create an Cognitive Services Emotion API key: 49 | 50 | - In the Azure portal, click **+ New** and search for **Emotion API**. 51 | - Enter the required information in the Create blade. You may use the free tier **F0** for this module. 52 | 53 | ## 2. Configure the CoderCards project 54 | 55 | 1. Get the [CoderCards project on GitHub](https://github.com/Azure-Samples/functions-dotnet-codercards), either by `git clone` or downloading the zip. 56 | 57 | - Use the `master` branch if you're on Windows 58 | - Use the `core` branch if you're on a Mac. 59 | 60 | 1. In the portal, find the resource group and account name for the Azure Storage account you wish to use. 61 | 62 | 1. From a terminal, navigate to the **functions-dotnet-codercards** directory. Run the following, using the storage account name and resource group from above: 63 | 64 | ``` 65 | az login 66 | python setup.py true 67 | ``` 68 | 69 | If you get python errors, make sure you've installed Python 3 and run the command `python3` instead (see [Installing Python 3 on Mac OS X](http://docs.python-guide.org/en/latest/starting/install3/osx/)). 70 | 71 | This will modify the file **local.settings.json**. The last argument controls whether to create containers prefixed with "local", which is useful if you want to use the same storage account when running locally and in Azure. 72 | 73 | 1. If using Visual Studio, open **CoderCards.sln**. On a Mac, open the **functions-dotnet-codercards** folder in VS Code. 74 | 75 | 1. Open the file **CoderCards/local.settings.json** 76 | 77 | 1. In the Azure portal, select your Emotion API instance. Select the **Keys** menu item and copy the value of **KEY 1**. Paste the value for the key `EmotionAPIKey`in **local.settings.json**. 78 | 79 | 1. Add the following `Host` setting to **local.settings.json**, as a peer to the `Values` collection: 80 | 81 | ```json 82 | { 83 | "IsEncrypted": false, 84 | "Host": { 85 | "LocalHttpPort": 7072, 86 | "CORS": "*" 87 | }, 88 | "Values": { 89 | ... 90 | } 91 | } 92 | ``` 93 | 94 | ### Summary of App Settings 95 | 96 | | Key | Description | 97 | |----- | ------| 98 | | AzureWebJobsStorage | Storage account connection string | 99 | | EmotionAPIKey | Key for [Cognitive Services Emotion API](https://www.microsoft.com/cognitive-services/en-us/emotion-api) | 100 | | input-queue | Name of Storage queue for to trigger card generation. Use a value like "local-queue" locally and "input-queue" on Azure 101 | | input-container | Name of Storage container for input images. Use a value like "input-local" locally and "card-input" on Azure | 102 | | output-container | Name of Storage container for output images. Use a value like "output-local" locally and "card-output" on Azure | 103 | | SITEURL | Set to `http://localhost:7072` locally. Not required on Azure. | 104 | | STORAGE_URL | URL of storage account, in the form `https://accountname.blob.core.windows.net/` | 105 | | CONTAINER_SAS | SAS token for uploading to input-container. Include the "?" prefix. | 106 | 107 | If you want to set these values in Azure, you can set them in **local.settings.json** and use the Azure Functions Core Tools to publish to Azure. 108 | 109 | ``` 110 | func azure functionapp publish function-app-name --publish-app-settings 111 | ``` 112 | 113 | ## 3. Run the project 114 | 115 | 1. Compile and run: 116 | 117 | - If using Visual Studio, just press F5 to compile and run **CoderCards.sln**. 118 | 119 | - If using VS Code on a Mac, run `dotnet build; dotnet publish`. Then, navigate to the output folder and run the Functions core tools: 120 | 121 | ``` 122 | cd CoderCards/bin/Debug/netstandard2.0/osx/publish 123 | func host start 124 | ``` 125 | 126 | You should see output similar to the following: 127 | 128 | ``` 129 | Http Functions: 130 | 131 | Settings: http://localhost:7072/api/Settings 132 | 133 | RequestImageProcessing: http://localhost:7072/api/RequestImageProcessing 134 | 135 | [10/6/17 7:01:18 AM] Found the following functions: 136 | [10/6/17 7:01:18 AM] Host.Functions.Settings 137 | [10/6/17 7:01:18 AM] Host.Functions.GenerateCard 138 | [10/6/17 7:01:18 AM] Host.Functions.RequestImageProcessing 139 | [10/6/17 7:01:18 AM] 140 | [10/6/17 7:01:18 AM] Job host started 141 | [10/6/17 7:01:18 AM] Host lock lease acquired by instance ID '0000000000000000000000005CADA547'. 142 | ``` 143 | 144 | 2. To test that the host is up and running, navigate to [http://localhost:7072/api/Settings](http://localhost:7072/api/Settings). 145 | 146 | ## 4. Use the bot 147 | 148 | 1. Go to the Squire UX and add a new skill: 149 | 150 | |Field|Value| 151 | |--|--| 152 | |Title|generate CoderCard| 153 | |Description|Generate a CoderCard| 154 | |Method|POST| 155 | |URL| http://localhost:7072/api/RequestImageProcessing| 156 | |Parameter Name|BlobName| 157 | |Parameter Prompt|What is the source image URL?| 158 | |Parameter Name|PersonName| 159 | |Parameter Prompt|What is the person name?| 160 | |Parameter Name|Title| 161 | |Parameter Prompt|What is the person's title?| 162 | 163 | 1. In Azure Storage explorer, navigate to the storage account you're using. 164 | 165 | - Select the container `input-local`. 166 | - Right-click and select **Set public access level**. 167 | - Select **Public read access for blobs only**. 168 | 169 | 2. Upload a *square* image of a face to `input-local` and copy the filename. 170 | 171 | 2. Go to your bot and ask it to `generate CoderCard`. Provide the filename you uploaded earlier. 172 | 173 | 3. Check the functions output window to see when the function is complete. The file will be written to the `output-local` container. Use Azure Storage Explorer to see the results. 174 | 175 | ``` 176 | [10/4/2017 1:34:59 AM] Function completed (Success, Id=a1d2a381-4eb6-4d82-8dc9-324ad90932c4, Duration=4993ms) 177 | [10/4/2017 1:34:59 AM] Executed 'GenerateCard' (Succeeded, Id=a1d2a381-4eb6-4d82-8dc9-324ad90932c4) 178 | ``` 179 | 180 | ## (Optional) 5. Use the CoderCards SPA 181 | 182 | 1. Run the Functions host on port 7071. Either modify the port **local.settings.json** or pass an explicit port when you start the functions host: `func host start --port 7071`. 183 | 184 | 2. In a command prompt, go to the `CoderCardsClient` directory. 185 | 186 | - Run `npm install` 187 | - Run `npm start`. This will launch a webpage at `http://127.0.0.1:8080/`. 188 | 189 | 190 | ## (Optional) 6. Running manually 191 | 192 | 1. Choose images that are **square** and upload to the `input-local` container. (Images that aren't square will be stretched.) 193 | 194 | 1. Send an HTTP request using Postman or CURL, specifying the path of the blob you just uploaded: 195 | 196 | `POST http://localhost:7072/api/RequestImageProcessing` 197 | 198 | ```json 199 | { 200 | "PersonName": "My Name", 201 | "Title": "My Title", 202 | "BlobName": "BlobFilename.jpg" 203 | } 204 | ``` -------------------------------------------------------------------------------- /2-hello-functions/README.md: -------------------------------------------------------------------------------- 1 | # Hello Functions 2 | 3 | In this module, you'll create a simple Function which listens for HTTP Requests and responds with an ASCII art response. We'll do this all locally to show how fast it is to develop and test locally. It is possible to do this all in the Azure Functions portal, however. 4 | 5 | ## 1. Pre-reqs 6 | 7 | You'll need: 8 | - Node 8.5.0 9 | - Azure Functions Core Tools (@core) 10 | - npm i -g azure-functions-core-tools@core 11 | - This has a dependency on dotnet core being installed 12 | - VS Code or similar text editor 13 | - cURL, Postman, or a general REST API tool 14 | 15 | ## 2. Create a Function App project 16 | 17 | Azure Functions can run locally with a very simple project structure. Essentially, you can create a directory which contains a child directory for each Function. It can also contain shared code/dependencies/static content. Each Function directory needs a `function.json` in it in order to be discovered by the Functions runtime; this file specifies the behavior of your application. The Function directory should also contain your code or you need to add a setting to your `function.json` on where that code lives. For most of this workshop, we'll just drop an `index.js` file in the same directory and let the runtime discover it automatically. 18 | 19 | Here's a simple ASCII representation of a Functions project structure. 20 | 21 | ``` 22 | (root) 23 | - host.json 24 | - local.settings.json 25 | - package.json 26 | - node_modules 27 | -- (...) 28 | - foo // <--- Function directory 29 | -- function.json 30 | -- index.js 31 | - bar // <--- Function directory 32 | -- function.json 33 | -- index.js 34 | ``` 35 | 36 | Fortunately, you don't have to create this all by hand. We can us the Azure Functions core tools to template for us. To create a new Functions Project, let's create a new directory and initialize it. 37 | 38 | ```bash 39 | # Create a new directory 40 | mkdir hello-functions 41 | cd hello-functions 42 | # Initialize that directory 43 | func init 44 | ``` 45 | 46 | It should show an output like so: 47 | 48 | ``` 49 | Writing .gitignore 50 | Writing host.json 51 | Writing local.settings.json 52 | Created launch.json 53 | Initialized empty Git repository in /Users/chris/workspace/hello-functions/.git/ 54 | ``` 55 | 56 | The tool won't overwrite any existing files, so if you ever accidentally delete a file and want to recreate it (like if you don't check in your `.vscode` directory), just run `func init` again. 57 | 58 | ## 3. Create a your first Function 59 | 60 | To create our first Function of the workshop, all we need to do is run: 61 | 62 | ``` 63 | func new 64 | ``` 65 | 66 | which will prompt us for which type of Function we'd like to create. We can also specify it via command line arguments. In this case, we want to create a JavaScript HTTP Function, so we'll run instead: 67 | 68 | ``` 69 | func new -l JavaScript -t HttpTrigger -n hello 70 | ``` 71 | 72 | which should output something like 73 | 74 | ``` 75 | Select a language: JavaScript 76 | Select a template: HttpTrigger 77 | Function name: [HttpTriggerJS] Writing /Users/chris/workspace/hello-functions/hello/index.js 78 | Writing /Users/chris/workspace/hello-functions/hello/sample.dat 79 | Writing /Users/chris/workspace/hello-functions/hello/function.json 80 | ``` 81 | 82 | In addition to the directory, this created three files for us: 83 | 84 | 1. `index.js` which contains our code 85 | 2. `function.json` which contains our config for the Function 86 | 3. `sample.dat` which is some test data you can use with the template out of the box. We don't need this, so you can delete it if you'd like. 87 | 88 | Let's go ahead and test out our Function now. 89 | 90 | ## 4. Running Azure Functions 91 | 92 | To start your Functions, be sure you're in the root of your Function project and run: 93 | 94 | ``` 95 | func host start 96 | ``` 97 | 98 | This should print an output like this: 99 | 100 | ``` 101 | 102 | %%%%%% 103 | %%%%%% 104 | @ %%%%%% @ 105 | @@ %%%%%% @@ 106 | @@@ %%%%%%%%%%% @@@ 107 | @@ %%%%%%%%%% @@ 108 | @@ %%%% @@ 109 | @@ %%% @@ 110 | @@ %% @@ 111 | %% 112 | % 113 | 114 | [10/7/17 3:14:29 PM] Reading host configuration file '/Users/chris/workspace/hello-functions/host.json' 115 | [10/7/17 3:14:29 PM] Host configuration file read: 116 | [10/7/17 3:14:29 PM] { } 117 | info: Worker.Node.d8612901-590c-4313-9a02-02a7d424f334[0] 118 | Start Process: node --inspect=5858 "/Users/chris/.azurefunctions/bin/workers/Node/dist/src/nodejsWorker.js" --host 127.0.0.1 --port 60505 --workerId d8612901-590c-4313-9a02-02a7d424f334 --requestId 7e03b625-8175-41f7-a47b-f06dec532484 119 | info: Worker.Node.d8612901-590c-4313-9a02-02a7d424f334[0] 120 | Debugger listening on ws://127.0.0.1:5858/3ed53bc1-e73e-450e-b98b-d1d78b73c0ed 121 | info: Worker.Node.d8612901-590c-4313-9a02-02a7d424f334[0] 122 | For help see https://nodejs.org/en/docs/inspector 123 | [10/7/17 3:14:30 PM] Generating 1 job function(s) 124 | [10/7/17 3:14:30 PM] Starting Host (HostId=christophersmacbookpro-114832657, Version=2.0.11308.0, ProcessId=50327, Debug=False, Attempt=0) 125 | [10/7/17 3:14:30 PM] Found the following functions: 126 | [10/7/17 3:14:30 PM] Host.Functions.hello 127 | [10/7/17 3:14:30 PM] 128 | [10/7/17 3:14:30 PM] Job host started 129 | info: Worker.Node.d8612901-590c-4313-9a02-02a7d424f334[0] 130 | Worker d8612901-590c-4313-9a02-02a7d424f334 connecting on 127.0.0.1:60505 131 | Listening on http://localhost:7071/ 132 | Hit CTRL-C to exit... 133 | 134 | Http Functions: 135 | 136 | hello: http://localhost:7071/api/hello 137 | ``` 138 | 139 | If you see errors, you might be missing a dependency. Get the attention of one of the instructors if you don't know what's wrong from the errors. One of the commons errors that occurs is if you're running an older version of Node.js, it won't install the Node worker properlly and you'll need to update to a new version of Node and reinstall. 140 | 141 | Note at the bottom of that output, we have a URL where our Function is hosted: `http://localhost:7071/api/hello` 142 | 143 | Go ahead an make a GET request to that via cURL, Postman, or even just a web browser. You should get a message like `Please pass a name on the query string or in the request body`. Now try again with a query parameter of `?name=world`. (aka `http://localhost:7071/api/hello?name=world`) which should get you a response like `Hello world`. You can try different name values like `?name=trogdor` and see the response change. 144 | 145 | While we won't go into detail on all the settings you can do with function.json here, it is worth looking at it and noting we have an `httpTrigger` input and an `http` output for the response. This is how the runtime knows that this Function is an http triggered Function and not a queue triggered Function. 146 | 147 | ## 5. Changing from hello world to ascii art 148 | 149 | In our workshop, we're building a service called "squirebot". The idea behind the name is that it is a bot which learns how to do things for you, but you have to train it, much like a squire of old. It is only appropriate then, that our first task we'll want squirebot to do for us is fetch us a lance. 150 | 151 | Let's change our function a bit to instead return some ASCII art. You can read through the code, but essentially we have two different templates for long and short lances and we just do a simple find and replace depending on which letters we want it to be made of. 152 | 153 | ```javascript 154 | module.exports = function (context, req) { 155 | context.log('JavaScript HTTP trigger function processed a request.'); 156 | 157 | if (req.body && req.body.lance_length && req.body.lance_material) { 158 | const long_lance = 159 | ` TTT 160 | TTTTTTTTT 161 | TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT 162 | TTTTTTTTT 163 | TTT` 164 | 165 | const short_lance = 166 | ` TTT 167 | TTTTTTTTTTTT 168 | TTT` 169 | 170 | let material = req.body.lance_material === "wood" ? "w" : "m"; 171 | 172 | let lance = req.body.lance_length === "short" ? short_lance.replace(/T/gi, material) : long_lance.replace(/T/gi, material); 173 | 174 | context.res = { 175 | // status: 200, /* Defaults to 200 */ 176 | body: { 177 | //card:"hero", 178 | message: `Here's your lance! 179 | ${lance}` 180 | } 181 | }; 182 | } 183 | else { 184 | context.res = { 185 | status: 400, 186 | body: { 187 | message: "I couldn't figure out how to do that..." 188 | } 189 | }; 190 | } 191 | context.done(); 192 | }; 193 | ``` 194 | 195 | Now try to run this via cURL or Postman (not your browser since this needs to be POST). 196 | 197 | ``` 198 | curl -H "Content-Type: application/json" -X POST -d "{\"lance_length\":\"long\",\"lance_material\":\"metal\"}" http://localhost:7071/api/hello 199 | ``` 200 | 201 | This should return us a fancy ASCII lance. You can now stop your Functions host. 202 | 203 | ## 6. Preparing our task for our squirebot 204 | 205 | Because this is no longer a hello world Function, and instead a lance fetching Function, one last step is to rename our Function. 206 | 207 | The name of your Function is tied to the directory name, which in this case is `hello`. You can rename your directory to rename your Function. You can rename your directory from your file explorer, VS Code, or terminal. Rename your directory to "lanceFetcher". 208 | 209 | `mv ./hello ./lanceFetcher` 210 | 211 | Now, if you start the funcitons host again, you'll see your API has changed to `api/lanceFetcher`. You don't have to change your Function name to change your route - you can also do it by setting the `route` property in the `function.json`. For example, if I change the `route` property to `foobar`, can access my function on `api/foobar`. If you want to remove `api` from the base route, you can do this in the host.json. You can [learn about host.json settings on docs.microsoft.com](https://docs.microsoft.com/en-us/azure/azure-functions/functions-host-json). 212 | 213 | Congratulations, you've now completed module 2 and created your first Function. You now know the basics on how to create a Functions project, how to create a Function from a template, how to edit and rename a Function, and how to run the Function locally. 214 | 215 | ## 7. (Optional) Create a C# function in Visual Studio 216 | 217 | You can create a C# function app in Visual Studio with the same HTTP function as above. Follow the tutorial [Create your first function using Visual Studio](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-your-first-function-visual-studio). 218 | -------------------------------------------------------------------------------- /7-photo-mosaic-bot/README.md: -------------------------------------------------------------------------------- 1 | # Photo Mosaic Bot 2 | 3 | This bot will generate a photo mosaic of an input image, using [Cognitive Services Custom Vision Service](https://azure.microsoft.com/en-us/services/cognitive-services/custom-vision-service/) to generate a photo mosaic from an input image. 4 | 5 | For example, you can train your model with Orlando landmarks, such as the Orlando Eye. Custom Vision will recognize an image of the Orlando Eye, and the function will create a photo mosaic composed of Bing image search results for "Orlando Eye." See example below. 6 | 7 | ![Orlando Eye Mosaic](images/orlando-eye-both.jpg) 8 | 9 | ## Prerequisites 10 | 11 | 1. Visual Studio, either: 12 | - [Visual Studio 2017 Update 3](https://www.visualstudio.com/downloads/) with the Azure workload installed (Windows) 13 | - [Visual Studio Code](https://code.visualstudio.com/download) with the [C# extension](https://code.visualstudio.com/docs/languages/csharp) (Mac/Linux) 14 | 15 | 1. If running on a Mac/Linux, [.NET Core 2.0](https://www.microsoft.com/net/core#macos) 16 | 17 | 1. If running on a Mac/Linx, install [azure\-functions\-core\-tools](https://www.npmjs.com/package/azure-functions-core-tools)@core from npm. For more information, see https://aka.ms/func-xplat. 18 | 19 | 1. [Bot Framework Emulator](https://github.com/Microsoft/BotFramework-Emulator/releases/). 20 | 21 | * NOTE: There's a problem with the latest Mac installers. So, install the older release [botframework\-emulator\-3\.5\.19\-mac\.zip](https://github.com/Microsoft/BotFramework-Emulator/releases/download/v3.5.19/botframework-emulator-3.5.19-mac.zip). The emulator will automatically download updates when it launches, and you simply have to restart it once that is complete. 22 | 23 | 1. Azure Storage Account 24 | 25 | 1. [Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) 26 | 27 | ## 1. Create API keys 28 | 29 | 1. Create a Bing Search API key: 30 | 31 | - In the Azure portal, click **+ New** and search for **Bing Search APIs**. 32 | - Enter the required information in the Create blade. You may use the lowest service tier of **S1** for this module. 33 | 34 | 1. (Optional) Create a Computer Vision API key. The function will fall back to the regular Computer Vision API if there isn't a match with images that have been trained in the Custom Vision Service. If you plan to test only with images that will match custom vision, you can skip this step. 35 | 36 | To create a Computer Vision API key: 37 | 38 | - In the Azure portal, click **+ New** and search for **Computer Vision API**. 39 | - Enter the required information in the Create blade. You may use the free tier **F0** for this module. 40 | 41 | ## 2. Set up Custom Vision Service project 42 | 43 | 1. Go to https://www.customvision.ai/ 44 | 45 | 1. Sign in with a Microsoft account 46 | 47 | 1. Create a new project and select the image type, such as "Landmarks" 48 | 49 | 1. Add images and tag them. You can use a Chrome extension such as [Bulk Image Downloader](http://www.talkapps.org/bulk-image-downloader) to download Google or Bing images of landmarks. 50 | 51 | 1. Once you have added several landmarks, click the **Train** button on the upper right. Make sure you have at least 2 different landmarks (with the landmark name as it's tag) and 5 images for each landmark. 52 | 53 | 1. (Optional) Test image recognition using the **Test** tab. 54 | 55 | 1. Click on the **Performance** tab. If you have more than one iteration, choose the latest iteration and click **Make default**. 56 | 57 | 58 | ## 3. Configure the photo mosaic project 59 | 60 | 1. Get the [photo mosaic project on GitHub](https://github.com/Azure-Samples/functions-dotnet-photo-mosaic), either by `git clone` or downloading the zip. 61 | 62 | - Use the `master` branch if you're on Windows 63 | - Use the `core` branch if you're on a Mac. 64 | 65 | 1. In the portal, find the resource group and account name for the Azure Storage account you wish to use. 66 | 67 | 1. From a terminal, navigate to the **functions-dotnet-photo-mosaic** directory. Run the following, using the storage account name and resource group from above: 68 | 69 | ``` 70 | az login 71 | python setup.py 72 | ``` 73 | 74 | If you get python errors, make sure you've installed Python 3 and run the command `python3` instead (see [Installing Python 3 on Mac OS X](http://docs.python-guide.org/en/latest/starting/install3/osx/)). 75 | 76 | This will modify the file **local.settings.json**. 77 | 78 | Alternatively, you can run the script from the Azure Cloud Shell in the Azure Portal. Just run `python` and paste the script. The script prints out settings values that you can use to manually modify `local.settings.json`. 79 | 80 | Ensure that you see "Setup successful!" in the output. 81 | 82 | 1. If using Visual Studio, open **MosaicMaker.sln**. On a Mac, open the **functions-dotnet-photo-mosaic** folder in VS Code. 83 | 84 | 1. Open the file **MosaicMaker/local.settings.json** 85 | 86 | 1. In the [Custom Vision portal](https://www.customvision.ai/), get the URL for your prediction service. Select **Prediction URL** and copy the second URL in the dialog box, under the section "**If you have an image file**". It will have the form `https://southcentralus.api.cognitive.microsoft.com/customvision/v1.0/Prediction//image`. Paste this value for the key `PredictionApiUrl` in **local.settings.json**. 87 | 88 | 1. In the Custom Vision portal, select the settings gear in the upper right. Copy the value of **Prediction Key** for the key `PredictionApiKey` in **local.settings.json**. 89 | 90 | ![Prediction API key](images/custom-vision-keys.png) 91 | 92 | 1. In the Azure portal, select your Bing Search APIs instance. Select the **Keys** menu item and copy the value of **KEY 1**. Paste the value for the key `SearchAPIKey`in **local.settings.json**. 93 | 94 | 1. (Optional) Photo mosaic will fall back to the regular vision service if there is not a match with custom vision. Paste your key for your Cognitive Services Vision Service as the value for `MicrosoftVisionApiKey` in **local.settings.json**. 95 | 96 | ### Summary of App Settings 97 | 98 | | Key | Description | 99 | |----- | ------| 100 | | AzureWebJobsStorage | Storage account connection string. | 101 | | SearchAPIKey | Key for [Bing Search API](https://azure.microsoft.com/en-us/services/cognitive-services/bing-web-search-api/). | 102 | | MicrosoftVisionApiKey | Key for [Computer Vision Service](https://azure.microsoft.com/en-us/services/cognitive-services/computer-vision/). | 103 | | PredictionApiUrl | Endpoint for [Cognitive Services Custom Vision Service](https://azure.microsoft.com/en-us/services/cognitive-services/custom-vision-service/). It should end with "image". | 104 | | PredictionApiKey | Prediction key for [Cognitive Services Custom Vision Service](https://azure.microsoft.com/en-us/services/cognitive-services/custom-vision-service/). | 105 | | generate-mosaic | Name of Storage queue for to trigger mosaic generation. Default value is "generate-mosaic". | 106 | | input-container | Name of Storage container for input images. Default value is "input-images". | 107 | | output-container | Name of Storage container for output images. Default value is "mosaic-output". | 108 | | tile-image-container | Name of Storage container for tile images. Default value is "tile-images". | 109 | | SITEURL | Set to `http://localhost:7072` locally. Not required on Azure. | 110 | | STORAGE_URL | URL of storage account, in the form `https://accountname.blob.core.windows.net/` | 111 | | CONTAINER_SAS | SAS token for uploading to input-container. Include the "?" prefix. | 112 | | APPINSIGHTS_INSTRUMENTATIONKEY | (optional) Application Insights instrumentation key. | 113 | | MosaicTileWidth | Default width of each mosaic tile. | 114 | | MosaicTileHeight | Default height of each mosaic tile. | 115 | 116 | If you want to set these values in Azure, you can set them in **local.settings.json** and use the Azure Functions Core Tools to publish to Azure. 117 | 118 | ``` 119 | func azure functionapp publish function-app-name --publish-app-settings 120 | ``` 121 | 122 | ## 4. Load Tile Images 123 | 124 | When the function app creates a mosaic, it needs source images to compose the mosaic. The **tile-image-container** referred to in App Settings (defaulted to **tile-images**) is the container that the function will look for images to use. Running the **setup.py** script above generated the containers for you, but you'll need to load images that container. Using the portal, [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/), or any other method of uploading blobs, upload images into the **tile-image-container** inside of your storage account. You can reuse the landmark images you downloaded earlier if desired. 125 | 126 | ## 5. Run the project 127 | 128 | 1. Compile and run: 129 | 130 | - If using Visual Studio, just press F5 to compile and run **PhotoMosaic.sln**. 131 | 132 | - If using VS Code on a Mac, open the Command Pallet (⇧⌘P) and run the Build Task which will run `dotnet build`. Then, navigate to the output folder and run the Functions core tools: 133 | 134 | ``` 135 | cd MosaicMaker/bin/Debug/netstandard2.0/osx 136 | func host start 137 | ``` 138 | 139 | You should see output similar to the following: 140 | 141 | ``` 142 | Http Functions: 143 | 144 | RequestMosaic: http://localhost:7072/api/RequestMosaic 145 | 146 | Settings: http://localhost:7072/api/Settings 147 | 148 | [10/4/2017 10:24:20 PM] Host lock lease acquired by instance ID '000000000000000000000000C9A597BE'. 149 | [10/4/2017 10:24:20 PM] Found the following functions: 150 | [10/4/2017 10:24:20 PM] MosaicMaker.MosaicBuilder.RequestImageProcessing 151 | [10/4/2017 10:24:20 PM] MosaicMaker.MosaicBuilder.Settings 152 | [10/4/2017 10:24:20 PM] MosaicMaker.MosaicBuilder.CreateMosaicAsync 153 | [10/4/2017 10:24:20 PM] 154 | [10/4/2017 10:24:20 PM] Job host started 155 | Debugger listening on [::]:5858 156 | ``` 157 | 158 | 2. To test that the host is up and running, navigate to [http://localhost:7072/api/Settings](http://localhost:7072/api/Settings). 159 | 160 | ## 6. Use the bot 161 | 162 | 1. Go to the Squire UX and add a new skill: 163 | 164 | |Field|Value| 165 | |--|--| 166 | |Title|generate mosaic| 167 | |Description|Generate a photo mosaic| 168 | |Method|POST| 169 | |URL| http://localhost:7072/api/RequestMosaic| 170 | |Parameter Name|InputImageUrl| 171 | |Parameter Prompt|What is the source image URL?| 172 | 173 | 2. Go to your bot and ask it to `generate mosaic`. Use an Google or Bing image URL of a landmark you've already trained, but try to use an image that you haven't trained it on. 174 | 175 | 3. The bot will show the URL where the generated mosaic will be available. Check the functions output window to see when the function is complete: 176 | 177 | ``` 178 | [10/4/2017 1:34:55 AM] Executing 'CreateMosaic' (Reason='New queue message detected on 'generate-mosaic'.', Id=a1d2a381-4eb6-4d82-8dc9-324ad90932c4) 179 | [10/4/2017 1:34:57 AM] Tag: Space Needle, Probability 1 180 | [10/4/2017 1:34:57 AM] 181 | 182 | Image analysis: Space Needle 183 | 184 | [10/4/2017 1:34:57 AM] Query hash: 439222976 185 | [10/4/2017 1:34:59 AM] Generating mosaic... 186 | [10/4/2017 1:34:59 AM] Time to generate mosaic: 344.2114 187 | [10/4/2017 1:34:59 AM] Function completed (Success, Id=a1d2a381-4eb6-4d82-8dc9-324ad90932c4, Duration=4993ms) 188 | [10/4/2017 1:34:59 AM] Executed 'CreateMosaic' (Succeeded, Id=a1d2a381-4eb6-4d82-8dc9-324ad90932c4) 189 | ``` 190 | 191 | ## (Optional) 7. Run manually 192 | 193 | To run manually, send an HTTP request using Postman or CURL: 194 | 195 | `POST http://localhost:7072/api/RequestMosaic` 196 | 197 | Body: 198 | ```json 199 | { 200 | "InputImageUrl": "http://url.of.your.image", 201 | "ImageContentString": "optional keyword for mosaic tiles", 202 | "TilePixels": 20 203 | } 204 | ``` 205 | -------------------------------------------------------------------------------- /3-squirebot/README.md: -------------------------------------------------------------------------------- 1 | # Squirebot 2 | 3 | This this module, we'll get the squirebot up and running. We'll also examine how it works. 4 | 5 | # 1. Pre-reqs 6 | 7 | - Node 8.5.0 8 | - Azure Functions core tools @ core 9 | - npm i -g azure-functions-core-tools@core 10 | - Also requires dotnet core installed (for now) 11 | - Bot Framework Emulator 12 | - VS Code/text editor, git, terminal 13 | - Azure Account 14 | - Bot Framework Account 15 | 16 | ## 2. Set up 17 | 18 | ### 1. Clone the squirebot repo 19 | ``` 20 | git clone https://github.com/christopheranderson/squirebot 21 | ``` 22 | 23 | ### 2. Install dependencies and start up hosts 24 | 25 | #### Function App 26 | ```bash 27 | cd ./src/tasks-functions 28 | npm i 29 | npm start 30 | ``` 31 | 32 | npm start will run the func host start --cors * command 33 | 34 | This should spin up the Function host 35 | 36 | #### Web Client 37 | ```bash 38 | cd ./src/webapp 39 | npm i 40 | npm start 41 | ``` 42 | 43 | This should spin up a local site hosting the static Angular content 44 | 45 | When it has finished building, open up your favorite browser to [http://localhost:4200](http://localhost:4200) 46 | 47 | You should now be able to see 1 task already existing for the build in "lanceFetcher" function. You can click on it to see the configuration. 48 | 49 | ### 3. Bot Framework Emulator 50 | 51 | If you haven't already installed the emulator, you can download packages for Windows, Linux and macOS from the [GitHub releases page](https://github.com/Microsoft/BotFramework-Emulator/releases). 52 | 53 | Start the bot framework emulator and set the url to "http://localhost:7071/api/bot". You don't need to set the application id or secret. 54 | 55 | Then type "hello" into the chat, and the bot should greet you. 56 | 57 | You can then type "Fetch me my lance" and the bot should prompt you with what size do you want your lance and what material. It should then return an ASCII lance to us. 58 | 59 | ## 3. How it works 60 | 61 | In the previous step, you started a Function App and a web client. The web client is an Angular project. Running in its current form, the service connects to "http://localhost:7071". This is determined by the `baseUrl` setting in the config file. 62 | 63 | Here's a sample from `./src/webapp/src/app/tasks.service.ts`: 64 | ```typescript 65 | @Injectable() 66 | export class TasksService { 67 | baseUrl = environment.baseUrl || ""; 68 | 69 | constructor(private http: HttpClient) { } 70 | 71 | getTasks(): Promise { 72 | if (environment.mocked) { 73 | return Promise.resolve(tasks); 74 | } else { 75 | const p: Promise = new Promise((res, rej) => { 76 | this.http 77 | .get(`${this.baseUrl}/api/tasks`) 78 | .subscribe(data => { 79 | res(data as ITask[]); 80 | }, (err) => { 81 | if (err.error instanceof Error) { 82 | rej(err); 83 | } else { 84 | if (err.status === 0) { 85 | if (isDevMode()) { 86 | alert("Could not connect to host, might need to enable CORS or make sure it is up and running..."); 87 | } 88 | } 89 | rej(new Error(`Bad response: status: ${err.status}, body: ${JSON.stringify(err.error, null, " ")}`)); 90 | } 91 | }); 92 | }); 93 | return p; 94 | } 95 | } 96 | 97 | // Rest of the APIs are implemented in the file 98 | ``` 99 | 100 | In this case, this calls the `api/tasks` route on the Function App. Let's take a look at this Function. It's under [`./src/task-function/task-api`](https://github.com/christopheranderson/squirebot/blob/master/src/tasks-functions/task-api/index.js#L21-L53) 101 | 102 | The code below is the code that will run in response to the requests from our client when there is a GET request. 103 | 104 | ```javascript 105 | function run(context, req) { 106 | // ... // 107 | switch (context.req.method) { 108 | case "GET": 109 | if (context.bindingData.id) { 110 | taskService.getTask(context.bindingData.id) 111 | .catch(results => { 112 | context.res.status(404).json({ message: results }); 113 | }) 114 | .then(results => { 115 | context.res.status(200).json(results); 116 | }); 117 | } else { 118 | const count = context.req.query.count; 119 | const offset = context.req.query.offset; 120 | const name = context.req.query.name; 121 | 122 | if (name) { 123 | taskService.getTaskByName(name) 124 | .catch(results => { 125 | context.res.status(404).json({ message: results }); 126 | }) 127 | .then(results => { 128 | context.res.status(200).json(results); 129 | }); 130 | } else { 131 | taskService.getTasks(count, offset) 132 | .catch(results => { 133 | context.res.status(400).json({ message: results }); 134 | }) 135 | .then(results => { 136 | context.res.status(200).json(results); 137 | }); 138 | } 139 | } 140 | break; 141 | //... 142 | ``` 143 | 144 | There are a few different approaches that people use with HTTP Triggered Functions: 145 | 146 | 1. 1 Function per Route and Method 147 | 2. 1 Function per Route 148 | 3. 1 Function for many routes 149 | 150 | In general, the best practice with Functions is for a Function to "do 1 thing". If we wanted to hold true to that, we might go for option #1. In this case, though, we've done #2, mainly to keep the number of Functions we have to manage low. The nice thing about all of this is that it isn't very hard to refactor this later if we want to. 151 | 152 | If you look closer at the logic handling that GET request, it's calling `taskService.getTask(...)`. This `taskService` is coming from a shared module that our Functions use to talk to our database. You can take a look at it in the [`./src/tasks-functions/lib/tasks.js`](https://github.com/christopheranderson/squirebot/blob/master/src/tasks-functions/lib/tasks.js#L145). 153 | 154 | ```javascript 155 | getTasks(count, offset) { 156 | if (!count) { 157 | count = 20; 158 | } 159 | 160 | if (!offset) { 161 | offset = 0; 162 | } 163 | 164 | if (this.useInMemory) { 165 | return Promise.resolve(LOCAL_TASKS.filter((task, index) => { 166 | return (index >= offset && index < offset + count); 167 | })); 168 | } else { 169 | return db.get(TASKS_COLLECTION, count, offset); 170 | } 171 | } 172 | ``` 173 | 174 | This example shows how the taskService's `getTasks` method is implemented. In this case, we've done something that isn't necessarily best practice, but we've done it to help you get started. You'll notice that there is an "if" statement which is looking to see if we're going to "useInMemory". This is because we wanted you to be able to get started trying the app without a database created. Normally, I'd encourage you to just have a database for development purposes because reimplementing both pieces of logic isn't helpful. Since we're using MongoDB's client, we could use a locally running MongoDB instance. 175 | 176 | When we do provide a `MONGO_URL` environment variable, we will use the MongoDB driver to talk to a database. In this case, we just have a simple MongoDB helper to make it simple to use. You could also use something like Mongoose if you wanted something nicer than raw MongoDB queries. 177 | 178 | For those of you familiar with Azure Functions, you might ask why we didn't use the Cosmos DB bindings for this, rather than using a Mongo library. In this case, we did it to show that you can use Mongo DB and other database libraries in Azure Functions very easily. As nice as bindings are to help reduce the amount of code you write, there's no limitations to using other code and it's worth having some samples that show that off. Just because we're not using Cosmos DB bindings doesn't mean we're not using Cosmos DB, though... 179 | 180 | Now that we've looked at how it all works, let's start getting it on Azure. 181 | 182 | ## 4. Working with CosmosDB 183 | 184 | You may be thinking, "Cosmos DB? I thought you just said Mongo DB?". 185 | 186 | In case you're not aware, Cosmos DB is a managed multi-model database that Microsoft Azure offers with a number of great features for serverless applications. One of the most interesting features for this sample, though, is that it has a Mongo DB compatible endpoint. This is helpful for us since we don't need to standup a Mongo DB instance and pay VM pricing for that. 187 | 188 | You can create a Cosmos DB via the CLI or portal. 189 | 190 | #### Create the Cosmos DB Database and Collection via the Azure CLI in the Portal 191 | 192 | You can [install the Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) on your machine, or use it from the [Cloud Shell inside the Azure Portal](https://docs.microsoft.com/en-us/azure/cloud-shell/overview). 193 | 194 | Using the Azure CLI, or the Cloud Shell button on the menu in the upper-right of the Azure portal, replace the value of `databaseAccountname` with a unique name, and run the following file to create our Cosmos DB account and collection: 195 | 196 | > Note: the Cosmos DB account create step takes a few minutes to finish 197 | 198 | ```sh 199 | #!/bin/bash 200 | 201 | # Set variables for the new account, database, and collection 202 | resourceGroupName='squire' 203 | location='eastus' 204 | databaseAccountname='<<<>>>' 205 | databaseName='squire' 206 | 207 | # Create a resource group 208 | az group create \ 209 | --name $resourceGroupName \ 210 | --location $location 211 | 212 | # Create a DocumentDB API Cosmos DB account 213 | az cosmosdb create \ 214 | --name $databaseAccountname \ 215 | --resource-group $resourceGroupName \ 216 | --kind MongoDB 217 | 218 | # Create a database 219 | az cosmosdb database create \ 220 | --name $databaseAccountname \ 221 | --db-name $databaseName \ 222 | --resource-group $resourceGroupName 223 | 224 | # Get the database account connection strings 225 | az cosmosdb list-keys \ 226 | --name $databaseAccountname \ 227 | --resource-group $resourceGroupName 228 | ``` 229 | At the end of running the commands you should have access to the Cosmos DB account keys. Copy the value of the primaryMasterKey and save it. Soon we will add it to the local settings file in Visual Studio Code. 230 | 231 | #### Optional: Create the Cosmos DB Database and Collection via the Azure Portal 232 | 233 | Follow these instructions if you didn't use the Azure CLI or Cloud Shell to create the data store and prefer to use the Azure Portal. 234 | 235 | 1. Login in to the [Azure Portal](https://portal.azure.com) 236 | 237 | 2. In the left pane, click New, click Databases, and then under Azure Cosmos DB, click Create. Create a new Cosmos DB account wit the following values: 238 | 239 | Field | Value 240 | ------------ | ------------- 241 | Id | <<>> 242 | API | MongoDB 243 | Subscription | Your subscription (should already be selected) 244 | Resource Group | Create new, squire 245 | Location | East US 246 | 247 | Here is an example: 248 | 249 | ![Create Cosmos DB Account](../5-voting-service/src/Content/Images/CosmosDB-1.PNG) 250 | 251 | > Note: this Cosmos DB account create step takes a few minutes to finish 252 | 253 | 3. After the account is created, create a new database named "squire" by selecting `Browse` underneath the `Collections` heading and clicking on `Add Database`. We don't need to create collections because MongoDB's client will do this for us automatically. 254 | 255 | 4. Next we need to get the database connection string.You can find it under the section `Connection String` in the left menu on the main screen for your Azure Cosmos DB account 256 | 257 | ![Get Connection string for Azure Cosmos DB](images/connection_string.png) 258 | 259 | Copy the value of the Primary Connection String and save it. Soon we will add it to the local settings file in Visual Studio Code. 260 | 261 | #### Updating local settings 262 | 263 | You'll now have a url that looks something like: `mongodb://username:password@host:10255/?ssl=true`. This will use the "test" collection by default. I'd recommend creating a "squire" collection and using that as the database and setting the setting, so your URL should be closer to `mongodb://username:password@host:10255/[database]?ssl=true`. 264 | 265 | You should be able to create a new property in `./src/tasks-functions/local.settings.json` called `MONGO_URL`. 266 | 267 | It should look something like: 268 | 269 | ```json 270 | { 271 | "IsEncrypted": false, 272 | "Host": { 273 | "CORS": "*" 274 | }, 275 | "Values": { 276 | "UseInMemoryStore":"false", 277 | "MONGO_URL":"mongodb://username:password@host:10255/[database]?ssl=true" 278 | } 279 | } 280 | ``` 281 | 282 | Be sure that "UseInMemoryStore" is set to false!!! 283 | 284 | Now go back and restart your Function's host. If you reload the webpage, it should have no Tasks on it now. 285 | 286 | Let's deploy the app to Azure before we start adding new tasks. 287 | 288 | ## 5. Deploying to Azure 289 | 290 | ### 1. Create a Function App 291 | In order to deploy to Azure, we need to create a Function App. A Function App acts as a collection of Functions, which lets us deploy multiple Functions to the same Function App. This let's us share a common endpoint and more. 292 | 293 | You can create a new Function App via the Portal CLI or just Portal menu options. 294 | 295 | #### CLI 296 | 297 | - Setup Azure Portal CLI following the instructions here - https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-azure-function-azure-cli 298 | 299 | #### Portal 300 | 301 | 1. Click on "+ New" in the portal 302 | 2. Search for "Function App" and click on it 303 | 3. Fill out the details however you'd like try to use 304 | - name like "-squire" 305 | - consumption plan (should be default) 306 | - east us 307 | - You should try to use the same resource group as your Cosmos DB to make clean up easy 308 | - Create a new Storage Account (this is different from your Cosmos DB) 309 | - Enable App Insights (not needed, but a good idea) 310 | 4. Click create and then wait for it to create (might take a few seconds). 311 | 312 | ### 2. Package our Functions 313 | 314 | Before we publish, one thing we can do to reduce cold start is to pack all our Functions into a single file. One of the largest impacts to Node.js cold start is module resolution. 315 | 316 | We've written a tool for Azure Functions which will webpack your code for you. You can download this tool via npm: 317 | 318 | ```bash 319 | npm i -g azure-functions-pack 320 | ``` 321 | 322 | then run at the base of your Function App 323 | 324 | ```bash 325 | funcpack pack . 326 | ``` 327 | 328 | which should result in: 329 | 330 | ```bash 331 | info: Generating project files/metadata 332 | info: Webpacking project 333 | info: Complete! 334 | ``` 335 | 336 | Now that we've packed our functions, we can even remove our node_modules directory and it still works. I recommend doing this to speed up deployment. You can just reinstall them later. 337 | 338 | ```bash 339 | rm -r node_modules 340 | ``` 341 | 342 | ### 3. Publish via Azure Functions Core Tools 343 | 344 | Now, we can publish our code via the Azure Functions core tools: 345 | 346 | 1. Login to azure 347 | 348 | ```bash 349 | func azure login 350 | ``` 351 | 352 | 2. Publish to Function App 353 | 354 | ```bash 355 | func azure functionapp publish -i -y 356 | ``` 357 | 358 | This should publish your directory to your Function App. 359 | 360 | 3. (Optional) Now that you've published you can unpack and install your node modules. It's helpful to write a simple script for this, but we wanted you to do it by hand to understand how it works. 361 | 362 | ```bash 363 | funcpack unpack . 364 | npm i 365 | ``` 366 | 367 | Now that your Function is deployed you should be able to go access "http://<YOURNAME>.azurewebsites.net/api/tasks" via a browser and it will return data (not the webapp, yet) to confirm everything works. 368 | 369 | ### 4. Pointing our Angular App at Azure 370 | 371 | In order to access our Function App, we need to point it to the right base URL. 372 | 373 | Go to the `./src/webapp/src/environments/environment.prod.ts` and add your base URL so it looks like this: 374 | 375 | ```typescript 376 | export const environment = { 377 | production: true, 378 | mocked: false, 379 | baseUrl: "https://.azurewebsites.net" 380 | }; 381 | ``` 382 | 383 | Now you can open a terminal to "./src/webapp/" and run `npm run build`. This will create a `dist` directory with a static/tree shaken copy of our application. 384 | 385 | If you go to your Resource Group you created for your Function App, you should already have a storage account created with your Function App. We'll use this to create a storage container for our static content. 386 | 387 | Be sure you create your container with the name "content". If you change it, you need to modify the package.json "build" script to use a different name. Also be sure you create it with "Blob" selected for anonymous access. 388 | 389 | ![Container create screenshot](./images/container-create.png) 390 | 391 | Now click upload and select all the content in the `dist` directory. 392 | 393 | Now if you click on your "index.html" file you just uploaded and open up the URL is gives you, it should load in the browser. But if you bring up your dev console, it won't be able to talk to the Function App as CORS is not enabled. 394 | 395 | We have two options: 396 | 397 | 1. Enable CORS 398 | 2. Route calls for our static content through an Azure Functions proxy 399 | 400 | I'll list both options. #2 is the one with the nicest outcome because it will be all 1 URL, not a storage account URL. #1 is fast though and useful to learn as well. 401 | 402 | #### 1. Enable CORS 403 | 404 | 1. Copy your storage account domain. Should looks like "https://chrandesquire.blob.core.windows.net" 405 | 406 | 2. Go to your Function App blade and click on your Platform Features option. Then find the CORS option. 407 | 408 | 3. Add your URL to the bottom of the list and click "save" 409 | 410 | ![CORS Screenshot](./images/cors-create.png) 411 | 412 | Now you should be able to see add new tasks and see your Function App. As a reminder, the tasks you previously saw when running the application locally will not show up as you are now pointing at the CosmosDB in Azure. 413 | 414 | #### 2. Use Azure Functions Proxies 415 | 416 | 1. Go to your Function App blade 417 | 418 | 2. Click on Function App Settings and use the toggle to enable Azure Functions Proxies 419 | 420 | 3. Click on the "+" button next to Proxies on the left menu 421 | 422 | 4. Create a new proxy. The name doesn't matter, but I used "index". Set the "route" property to "/" or "/index.html" (or whatever you want). Set the backend URL to the URL to your storage container's index.html file. (Should be "https://.blob.core.windows.net/content/index.html"). Click save. 423 | 424 | 5. Now we need to create a route for the rest of the content. Create another new proxy. The name can be "content", the route property should be "/content/{*restOfPath}", the backend URL should be the container (or the same as your index.html page, without index.html at the end) with at the end (should look like "https://.blob.core.windows.net/content/{restOfPath}". Click save. 425 | 426 | Note that we overwrote the base route for the Function App in the first proxy. Second thing to note is the `{*restOfPath}` token which grabs the rest of the path for us and gives us a token we can use in the backend URL. Importantly, we remove the "*" from the token there, so it is just `{restOfPath}`. 427 | 428 | Now go to the route you created for your index file and view your webpage working! No CORS needed. As a reminder, the tasks you previously saw when running the application locally will not show up as you are now pointing at the CosmosDB in Azure. 429 | 430 | ### 5. Connecting your bot to the Bot Framework 431 | 432 | 1. To get started, go to the [bot framework developer portal](https://dev.botframework.com/bots/provision?createFirstBot=true) and, if you haven't already, sign up for an account. 433 | 434 | 2. Click the button to create a new bot and choose `Register an existing bot built using Bot Builder SDK`: 435 | ![Create a Bot](./images/create-a-bot.png) 436 | 437 | 3. Fill out the details of your bot. In the `messaging endpoint` value, enter in the Configuration section, enter the url of the `bot` function in your Function App in Azure, including the key in the URL. Then create the Microsoft App ID and Password and save those values for later. 438 | ![Configure the Bot](./images/configure-bot.png) 439 | 440 | 3. Go to your Function App on Azure and set the app settings with the App ID and secret from the previous step: 441 | - MICROSOFT_APP_ID 442 | - MICROSOFT_APP_PASSWORD 443 | 444 | 4. You should be able to then "test" your bot via the "test" button. This opens a web based bot interface. Type "hello" in there and then try to tell it to do something (like "fetch me my lance"). 445 | 446 | It doesn't know how to do that, probably. Well go back to your newly deployed squire bot webpage (either the CORS or non-CORS version) and create a new task. This time, point it to your "hello" funciton in your Function App that you've already deployed (copy+paste the URL from the Functions UI to get the API key). I'd recommend calling the bot "hello world" rather than "hello" since this conflicts with the default greeting. Make sure to create a parameter named "hello" with a prompt like "What is your name?". 447 | 448 | Now your bot should be able to say hello back! 449 | 450 | If you'd like to, you can connect your bot to Slack or Microsoft teams by following the instructions on the [Bots documentation page](https://docs.microsoft.com/en-us/bot-framework/portal-configure-channels). 451 | 452 | At this point, you've completed the module! It was a long journey, but you've done a lot! You've created a Function app and deployed it to Azure. You've learned how to use Cosmos DB. You've learned how to use the Microsoft Bot Framework. You've created a whole new service for extending chat applications with webhooks! Continue on to the next step to learn how to create more plugins for the squirebot. 453 | -------------------------------------------------------------------------------- /5-voting-service.v1/README.md: -------------------------------------------------------------------------------- 1 | # Voting Service 2 | 3 | ## Create a serverless service using Node.js and Azure Functions 4 | 5 | ### Version Note 6 | This walkthrough is for V1 of Azure Functions. If you are running the V2 of Azure Functions, please refer to [this guide](../5-voting-service/). 7 | 8 | ## 1. Overview 9 | 10 | In this part of the workshop we will create a voting service that allows a team to create polls to vote on, and then surface it from the Squire bot. 11 | 12 | ![Architecture](src/Content/Images/Architecture.PNG) 13 | 14 | We will be using Node.js, JSON documents, Cosmos DB, and Azure Functions to implement the service. 15 | 16 | ### 1.1 Voting Session Document 17 | 18 | The Voting Session document will keep all the information about the voting session, including the question, avaialable options, and the votes. Here is an example JSON document for a Voting session after it's been filled out with a few votes: 19 | 20 | ```javascript 21 | { 22 | "votingname": "pizzavote", 23 | "name": "Pizza Voting", 24 | "isOpen": true, 25 | "question": "What pizza do you want?", 26 | "options": [ 27 | { 28 | "text": "Pepperoni", 29 | "votes": 3, 30 | "voters": ["Thiago", "Jeff", "Raman"] 31 | 32 | }, 33 | { 34 | "text": "Mushrooms", 35 | "votes": 1, 36 | "voters": ["David"] 37 | }, 38 | { 39 | "text": "Margherita", 40 | "votes": 3, 41 | "voters": ["Donna", "Kanio", "Chris"] 42 | }, 43 | { 44 | "text": "Quattro Stagioni", 45 | "votes": 0 46 | } 47 | ] 48 | } 49 | ``` 50 | 51 | ### 1.3 Voting Service Operations 52 | 53 | Operations | Details 54 | ------------ | ------------- 55 | Create Voting Session | Create a new voting session 56 | Close / Re-Open Voting| Change status of the voting moving it from active to inactive or vice versa 57 | Vote | Submit user's vote 58 | Voting Status | Get the latest results from the poll 59 | Delete Voting Session | Remove the voting session from the data store 60 | 61 | Each of the operations will map to an Azure Function that we will develop in this module. While Azure Functions can accept any HTTP verb, we are using POST for all of these functions so it can integrate with the Squire Bot at the end of the module. 62 | 63 | ## 2. Prerequisites 64 | 65 | Ensure you have the following prerequisites before proceeding: 66 | 67 | - An active Azure account. If you don't have one, you can sign up for a free account https://azure.microsoft.com/en-us/free/ 68 | 69 | - Node.js and npm https://nodejs.org/en/download/ 70 | 71 | - Visual Studio Code https://code.visualstudio.com/Download 72 | 73 | - Azure Functions Core Tools https://www.npmjs.com/package/azure-functions-core-tools 74 | 75 | - RESTful Client that will help you test the functions both locally and when deployed to Azure. One option is Postman https://www.getpostman.com/ 76 | 77 | - Bot Framework Emulator - https://github.com/Microsoft/BotFramework-Emulator/releases/ 78 | 79 | ## 3. Development 80 | 81 | Now it's time to start building our service. 82 | 83 | ### 3.1. Serverless Data Store - Azure Cosmos DB 84 | 85 | First, let's prepare our data store - [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/introduction). Cosmos DB is Microsoft's globally distributed, multi-model data service. We will use it's document database API called DocumentDB to store the voting session contents for our service. DocumentDB provides rich and familiar SQL query capabilities with consistent low latencies over schema-less JSON data, which is perfect for our service. You can create it on your Azure subscription via the Azure CLI or the Azure Portal: 86 | 87 | #### 3.1.1 Create the Cosmos DB Database and Collection via the Azure CLI in the Portal 88 | 89 | You can [install the Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) on your machine, or use it from the [Cloud Shell inside the Azure Portal](https://docs.microsoft.com/en-us/azure/cloud-shell/overview). 90 | 91 | Using the Azure CLI, or the Cloud Shell button on the menu in the upper-right of the Azure portal, replace the value of `databaseAccountname` with a unique name, and run the following file to create our Cosmos DB account and collection: 92 | 93 | > Note: the Cosmos DB account create step takes a few minutes to finish 94 | 95 | ```sh 96 | #!/bin/bash 97 | 98 | # Set variables for the new account, database, and collection 99 | resourceGroupName='votingbot' 100 | location='eastus' 101 | databaseAccountname='<<<>>>' 102 | databaseName='votingbot' 103 | collectionName='votingbot' 104 | partitionkeypath='/votingname' 105 | 106 | # Create a resource group 107 | az group create \ 108 | --name $resourceGroupName \ 109 | --location $location 110 | 111 | # Create a DocumentDB API Cosmos DB account 112 | az cosmosdb create \ 113 | --name $databaseAccountname \ 114 | --resource-group $resourceGroupName 115 | 116 | # Create a database 117 | az cosmosdb database create \ 118 | --name $databaseAccountname \ 119 | --db-name $databaseName \ 120 | --resource-group $resourceGroupName 121 | 122 | # Create a collection 123 | az cosmosdb collection create \ 124 | --collection-name $collectionName \ 125 | --partition-key-path $partitionkeypath \ 126 | --name $databaseAccountname \ 127 | --db-name $databaseName \ 128 | --throughput 400 \ 129 | --resource-group $resourceGroupName 130 | 131 | # Get the database account connection strings 132 | az cosmosdb list-keys \ 133 | --name $databaseAccountname \ 134 | --resource-group $resourceGroupName 135 | ``` 136 | At the end of running the commands you should have access to the Cosmos DB account keys. Copy the value of the primaryMasterKey and save it. Soon we will add it to the local settings file in Visual Studio Code. 137 | 138 | #### 3.1.2 Optional: Create the Cosmos DB Database and Collection via the Azure Portal 139 | 140 | Follow these instructions if you didn't use the Azure CLI or Cloud Shell to create the data store and prefer to use the Azure Portal. 141 | 142 | 1. Login in to the [Azure Portal](https://portal.azure.com) 143 | 144 | 2. In the left pane, click New, click Databases, and then under Azure Cosmos DB, click Create. Create a new Cosmos DB account wit the following values: 145 | 146 | Field | Value 147 | ------------ | ------------- 148 | Id | <<>> 149 | API | SQL (DocumentDB) 150 | Subscription | Your subscription (should already be selected) 151 | Resource Group | Create new, votingbot 152 | Location | East US 153 | 154 | Here is an example: 155 | 156 | ![Create Cosmos DB Account](src/Content/Images/CosmosDB-1.PNG) 157 | 158 | > Note: this Cosmos DB account create step takes a few minutes to finish 159 | 160 | 3. After the account is created, create a new collection and a database with the following values (the same values as in the pritscreen): 161 | 162 | Field | Value 163 | ------------ | ------------- 164 | Collection Id | votingbot 165 | Storage Capacity | Fixed (10GB) 166 | Initial Throughput Capacity | 400 167 | Partition Key | /votingname 168 | Database | Use Existing, votingbot 169 | 170 | A partition key is a property (or path) within your documents that is used to distribute your data among the servers or partitions for scale. 171 | 172 | ![Create database and collection in your Azure Cosmos DB account](src/Content/Images/CosmosDB-2.PNG) 173 | 174 | 4. Next we need to get the database connection string.You can find it under the section Keys in the left menu on the main screen for your Azure Cosmos DB account 175 | 176 | ![Get Connection string for Azure Cosmos DB](src/Content/Images/CosmosDB-3.PNG) 177 | 178 | Copy the value of the Primary Connection String and save it. Soon we will add it to the local settings file in Visual Studio Code. 179 | 180 | Now we are ready to start creating our voting service using the Azure Functions CLI and Visual Studio Code! In the next section we will walk you through the process. 181 | 182 | ### 3.2. Create Voting Function 183 | 184 | Create a folder called `VotingBot` on your machine. It will contain all the code for our functions. 185 | Next let's use the Azure Functions CLI to create a new Function app in the current folder, initialize a git repo, and create the first function in the app. From the Terminal, in the `VotingBot` folder, enter the following commands: 186 | 187 | ```javascript 188 | //This initializes the function app and repo 189 | func init 190 | 191 | //This creates a new function in the function app 192 | func new 193 | // next select JavaScript 194 | // next select HttpTrigger 195 | // next provide CreateVotingNode as the name of the function 196 | 197 | // this will open Visual Studio Code with your function app and the new CreateVotingNode function in it 198 | code . 199 | ``` 200 | 201 | Next let's modify the main files for this function. 202 | 203 | Modify the `appsettings.json` file in the `VotingBot` folder and add a `votingbot_DOCUMENTDB` entry to the values section, then update the connection string with the values for your Cosmos DB account name and the primary key, or connection string, that you copied earlier: 204 | 205 | ```javascript 206 | { 207 | "IsEncrypted": false, 208 | "Values": { 209 | "AzureWebJobsStorage": "", 210 | "votingbot_DOCUMENTDB": "AccountEndpoint=https://<<<>>>.documents.azure.com:443/;AccountKey=<<<>>>;" 211 | } 212 | } 213 | 214 | ``` 215 | 216 | This `local.settings.json` file is where you can put local values to be used by all your functions in this function app. 217 | 218 | The function will be triggered via an HTTP call (HttpTrigger), and will have a Cosmos DB Binding. Before continuing [learn more about Azure Functions triggers and bindings here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings). 219 | 220 | In Visual Studio Code modify the `function.json` file in the `CreateVotingode` folder to define the trigger and bindings as follows: 221 | 222 | ```javascript 223 | { 224 | "bindings": [ 225 | { 226 | "authLevel": "function", 227 | "type": "httpTrigger", 228 | "direction": "in", 229 | "methods": [ "post" ], 230 | "name": "req" 231 | }, 232 | { 233 | "type": "http", 234 | "direction": "out", 235 | "name": "res" 236 | }, 237 | { 238 | "type": "documentDB", 239 | "name": "outputDocument", 240 | "databaseName": "votingbot", 241 | "collectionName": "votingbot", 242 | "createIfNotExists": true, 243 | "connection": "votingbot_DOCUMENTDB", 244 | "direction": "out", 245 | "partitionKey": "/votingname" 246 | } 247 | ], 248 | "disabled": false 249 | } 250 | ``` 251 | 252 | The main logic inside the function will be to add votes and voters fields to the voting options, and then use the documentDB binding to save the session to our data store. 253 | Now change the contents of `index.js` with the following code. This file contains all the code logic for this function: 254 | 255 | ```javascript 256 | 257 | module.exports = function (context, req) { 258 | context.log('JavaScript HTTP trigger function processed a request.'); 259 | 260 | if (req.body && req.body.votingname && req.body.question && req.body.options) { 261 | var body = req.body; 262 | var votingname = body.votingname.replace(/\s/g,'').toLowerCase(); 263 | body.votingname = votingname; 264 | body.id = votingname; 265 | var optionsValues = req.body.options.replace(/\s/g,'').split(","); 266 | var options = []; 267 | for(var i=0; i< optionsValues.length; i++){ 268 | var option = {}; 269 | option.text = optionsValues[i]; 270 | option.votes = 0; 271 | option.voters = []; 272 | options.push(option); 273 | } 274 | 275 | body.options = options; 276 | 277 | context.bindings.outputDocument = body; 278 | 279 | var responseBody = {}; 280 | responseBody.voting = body; 281 | responseBody.message = "Wow! Voting with id '" + votingname + "' was created!"; 282 | 283 | context.res = { 284 | status: 201, 285 | body:responseBody 286 | }; 287 | } 288 | else { 289 | context.res = { 290 | status: 400, 291 | body: { "message" : "Please pass a voting object in the request body"} 292 | }; 293 | } 294 | context.done(); 295 | }; 296 | 297 | ``` 298 | 299 | Now it is time to test the function. Again, in the Integrated Terminal, type 300 | ```javascript 301 | cd .. 302 | func host start 303 | ``` 304 | 305 | Follow the instructions in the prompt and soon you will see from the logs that the function is running locally on http://localhost:7071/api/CreateVotingNode 306 | 307 | Now we are ready to test the function. In this function we create the voting session by accepting POST requests with the following JSON body, so this will be the object we process in the body of the function: 308 | ```javascript 309 | { 310 | "votingname": "pizza vote", 311 | "isOpen": true, 312 | "question": "What pizza do you want?", 313 | "options": "Pepperoni, Mushrooms, Margherita, Quattro Stagioni" 314 | } 315 | ``` 316 | 317 | One option is to use Postman on that URL, with the content of a Voting Session without the ids. As a result, if it's successful, we will get the document we created in Azure Cosmos DB with the ids: 318 | 319 | ![Postman testing](src/Content/Images/CreateVoting-Postman.PNG) 320 | 321 | You can also debug the function - once the function is running, in Visual Studio Code, in the Debug view, select Attach to Azure Functions. You can attach breakpoints, inspect variables, and step through code. Try it out! 322 | 323 | ### 3.3. Serverless Function - Close / Re-Open Voting Session 324 | 325 | In this function we will disable or enable a voting session by modifying the value of `isOpen` in the stored voting session document. 326 | 327 | From the `VotingBot` folder in the command prompt executing the following commands: 328 | 329 | ```javascript 330 | //This creates a new function in the function app 331 | func new 332 | // next select JavaScript 333 | // next select HttpTrigger 334 | // next provide CloseVotingNode as the name of the function 335 | 336 | // this will open Visual Studio Code with your function app and the new CloseVotingNode function in it. If you already have it open you can skip this command 337 | code . 338 | ``` 339 | 340 | This function will accept POST request with the following request object: 341 | 342 | ```javascript 343 | { 344 | "id":"pizzavote", 345 | "isOpen":false 346 | } 347 | ``` 348 | 349 | You can change isOpen to true or false to open or close a voting session. 350 | 351 | The bindings configuration for this function is different from the Create Voting function. We define both an in binding to find the existing document using the `votingname` value from the request, and an output binding to update the document on our data store. Update `function.json` in the `CloseVotingNode` folder with the following: 352 | 353 | ```javascript 354 | { 355 | "disabled": false, 356 | "bindings": [ 357 | { 358 | "authLevel": "function", 359 | "type": "httpTrigger", 360 | "direction": "in", 361 | "methods": [ "post" ], 362 | "name": "req" 363 | }, 364 | { 365 | "type": "http", 366 | "direction": "out", 367 | "name": "res" 368 | }, 369 | { 370 | "type": "documentDB", 371 | "name": "inputDocument", 372 | "databaseName": "votingbot", 373 | "collectionName": "votingbot", 374 | "sqlQuery": "SELECT * from c where c.id = {id}", 375 | "connection": "votingbot_DOCUMENTDB", 376 | "direction": "in" 377 | }, 378 | { 379 | "type": "documentDB", 380 | "name": "outputDocument", 381 | "databaseName": "votingbot", 382 | "collectionName": "votingbot", 383 | "connection": "votingbot_DOCUMENTDB", 384 | "direction": "out" 385 | } 386 | ] 387 | } 388 | ``` 389 | 390 | In this function the bindings do a lot of the work for us. Update `index.js` in the `CloseVotingNode` folder with the following: 391 | 392 | ```javascript 393 | module.exports = function (context, req) { 394 | context.log('JavaScript HTTP trigger function processed a request.'); 395 | 396 | if (req.body && req.body.id && req.body.isOpen != null) { 397 | 398 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 399 | { 400 | var voting = context.bindings.inputDocument[0]; 401 | context.bindings.outputDocument = voting; 402 | context.bindings.outputDocument.isOpen = req.body.isOpen; 403 | 404 | var responseBody = { 405 | "voting" : { 406 | "votingname" : voting.votingname, 407 | "isOpen" : voting.isOpen, 408 | "question" : voting.question, 409 | "options" : voting.options, 410 | "id" : voting.id 411 | }, 412 | "message" : "Nice! Voting with id '" + req.body.id + "' was updated!" 413 | }; 414 | 415 | context.res = { 416 | status: 200, 417 | body: responseBody 418 | }; 419 | context.done(null, context.res); 420 | } 421 | else { 422 | context.res = { 423 | status: 400, 424 | body: { "message" : "Record with this votingname can not be found. Please pass a votingname of an existing document in the request body"} 425 | }; 426 | context.done(null, context.res); 427 | }; 428 | } 429 | else { 430 | res = { 431 | status: 400, 432 | body: "Please pass a votingname and isOpen value in the request body" 433 | }; 434 | 435 | context.done(null, res); 436 | } 437 | }; 438 | ``` 439 | 440 | Let's run and test the function in the same way we did in Create Voting function. You will notice that the URL for this function is at http://localhost:7071/api/CloseVotingNode and you can test it as follows: 441 | 442 | ![Postman testing](src/Content/Images/CloseVoting-Postman.PNG) 443 | 444 | ### 3.4. Serverless Function - Vote on a Voting Session 445 | 446 | In this function we will receive votes and update the voting session. 447 | 448 | From the `VotingBot` folder in the command prompt executing the following commands: 449 | 450 | ```javascript 451 | //This creates a new function in the function app 452 | func new 453 | // next select JavaScript 454 | // next select HttpTrigger 455 | // next provide VoteNode as the name of the function 456 | 457 | // this will open Visual Studio Code with your function app and the new VoteNode function in it. If you already have it open you can skip this command 458 | code . 459 | ``` 460 | 461 | The function will work with the following request object: 462 | 463 | ```javascript 464 | { 465 | "id": "pizzavote", 466 | "user": "Ted", 467 | "option": "QuattroStagioni" 468 | } 469 | ``` 470 | 471 | Update `function.json` in the `VoteNode` folder as follows to include an output binding for our data store, it's the same as the previous function: 472 | 473 | 474 | ```javascript 475 | { 476 | "disabled": false, 477 | "bindings": [ 478 | { 479 | "authLevel": "function", 480 | "type": "httpTrigger", 481 | "direction": "in", 482 | "methods": [ "post" ], 483 | "name": "req" 484 | }, 485 | { 486 | "type": "http", 487 | "direction": "out", 488 | "name": "res" 489 | }, 490 | { 491 | "type": "documentDB", 492 | "name": "inputDocument", 493 | "databaseName": "votingbot", 494 | "collectionName": "votingbot", 495 | "sqlQuery": "SELECT * from c where c.id = {id}", 496 | "connection": "votingbot_DOCUMENTDB", 497 | "direction": "in" 498 | }, 499 | { 500 | "type": "documentDB", 501 | "name": "outputDocument", 502 | "databaseName": "votingbot", 503 | "collectionName": "votingbot", 504 | "connection": "votingbot_DOCUMENTDB", 505 | "direction": "out" 506 | } 507 | ] 508 | } 509 | ``` 510 | 511 | The logic for this function is to update the Voting Session document with a new vote based on the data we are getting from the request. Update `index.js` in the `VoteNode` folder with the following: 512 | 513 | ```javascript 514 | module.exports = function (context, req) { 515 | context.log('JavaScript HTTP trigger function processed a request.'); 516 | 517 | if (req.body && req.body.id && req.body.user && req.body.option) { 518 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 519 | { 520 | var body = context.bindings.inputDocument[0]; 521 | var found = false; 522 | var alreadyset = false; 523 | for (var index = 0; index < body.options.length; ++index) { 524 | if (body.options[index].text.toLowerCase() == req.body.option.toLowerCase()) { 525 | found = true; 526 | for (var index2 = 0; index2 < body.options[index].voters.length; index2++) { 527 | if (body.options[index].voters[index2].toLowerCase() == req.body.user.toLowerCase()) { 528 | context.res = { 529 | status: 201, 530 | body: { "message" : "Vote was already there, nothing updated" } 531 | }; 532 | alreadyset = true; 533 | break; 534 | } 535 | } 536 | if (found & !alreadyset){ 537 | body.options[index].votes++; 538 | body.options[index].voters.push(req.body.user); 539 | } 540 | break; 541 | } 542 | } 543 | if (found & !alreadyset){ 544 | context.bindings.outputDocument = body; 545 | 546 | var responseBody = { 547 | "voting" : { 548 | "votingname" : body.votingname, 549 | "isOpen" : body.isOpen, 550 | "question" : body.question, 551 | "options" : body.options, 552 | "id" : body.id 553 | }, 554 | "message" : "Nice! Your vote was counted!" 555 | }; 556 | 557 | context.res = { 558 | status: 201, 559 | body: responseBody 560 | }; 561 | } 562 | else { 563 | if (!alreadyset){ 564 | context.res = { 565 | status: 400, 566 | body: { "message" : "No vote option found with value " + req.body.option + " in voting session " + req.body.id } 567 | } 568 | } 569 | } 570 | } 571 | else { 572 | context.res = { 573 | status: 400, 574 | body: { "message" : "Record with this id can not be found. Please pass a id of an existing document in the request body" } 575 | }; 576 | context.done(null, context.res); 577 | }; 578 | } 579 | else { 580 | context.res = { 581 | status: 400, 582 | body: { "message" : "Please pass a vote object in the request body" } 583 | }; 584 | } 585 | context.done(); 586 | }; 587 | ``` 588 | Run and test it similarly to the previous two functions. You should get the new document back. For example: 589 | ![Postman testing](src/Content/Images/Vote-Postman.PNG) 590 | 591 | ### 3.5. Serverless Function - Voting Session Status 592 | 593 | Voting Status will give us the score of a voting session. 594 | 595 | From the `VotingBot` folder in the command prompt executing the following commands: 596 | 597 | ```javascript 598 | //This creates a new function in the function app 599 | func new 600 | // next select JavaScript 601 | // next select HttpTrigger 602 | // next provide VotingStatusNode as the name of the function 603 | 604 | // this will open Visual Studio Code with your function app and the new VotingStatusNode function in it. If you already have it open you can skip this command 605 | code . 606 | ``` 607 | 608 | This functions will accept POST HTTP method in order to integrate with the Squire. The function requires id value in the body of the request. Below you can find function.json file contents. We map the votingname to the documentDB outputbinding sqlQuery. 609 | 610 | Update `function.json` with the following: 611 | 612 | ```javascript 613 | { 614 | "disabled": false, 615 | "bindings": [ 616 | { 617 | "authLevel": "function", 618 | "type": "httpTrigger", 619 | "direction": "in", 620 | "methods": [ "post" ], 621 | "name": "req" 622 | }, 623 | { 624 | "type": "http", 625 | "direction": "out", 626 | "name": "res" 627 | }, 628 | { 629 | "type": "documentDB", 630 | "name": "inputDocument", 631 | "databaseName": "votingbot", 632 | "collectionName": "votingbot", 633 | "sqlQuery": "SELECT * from c where c.id = {id}", 634 | "connection": "votingbot_DOCUMENTDB", 635 | "direction": "in" 636 | } 637 | ] 638 | } 639 | 640 | ``` 641 | 642 | The binding will return the voting document with all of the votes. We then return the whole voting document: 643 | 644 | ```javascript 645 | module.exports = function (context, req) { 646 | context.log('JavaScript HTTP trigger function processed a request.'); 647 | 648 | if (context.bindings.inputDocument && context.bindings.inputDocument.length == 1) 649 | { 650 | var voting = context.bindings.inputDocument[0]; 651 | 652 | var message = "Here we go, these are the current results - "; 653 | 654 | for(var i=0; i < voting.options.length; i++){ 655 | var votes = voting.options[i].votes; 656 | message += voting.options[i].text + " has " + votes + (votes === 1 ? " vote" : " votes"); 657 | if(i < voting.options.length-1 ) { message += ", "} 658 | } 659 | 660 | var responseBody = { 661 | "voting" : { 662 | "votingname" : voting.votingname, 663 | "isOpen" : voting.isOpen, 664 | "question" : voting.question, 665 | "options" : voting.options, 666 | "id" : voting.id 667 | }, 668 | "message" : message 669 | }; 670 | 671 | context.res = { 672 | status : 200, 673 | body : responseBody 674 | }; 675 | context.done(null, context.res); 676 | } 677 | else { 678 | context.res = { 679 | status : 400, 680 | body: { "message" : "Record with this id can not be found. Please pass an id of an existing document in the request body" } 681 | }; 682 | context.done(null, context.res); 683 | } 684 | }; 685 | ``` 686 | 687 | Run and test it similarly to the previous functions. For example: 688 | ![Postman testing](src/Content/Images/VotingStatus-Postman.PNG) 689 | 690 | ### 3.6. Serverless Function - Delete Voting Session 691 | 692 | Delete Voting Session will delete the voting session document with all its votes. 693 | 694 | From the `VotingBot` folder in the command prompt executing the following commands: 695 | 696 | ```javascript 697 | //This creates a new function in the function app 698 | func new 699 | // next select JavaScript 700 | // next select HttpTrigger 701 | // next provide DeleteVotingNode as the name of the function 702 | 703 | // this will open Visual Studio Code with your function app and the new DeleteVotingNode function in it. If you already have it open you can skip this command 704 | code . 705 | ``` 706 | 707 | This functions will expect the voting session id in the request body: 708 | 709 | ```javascript 710 | { 711 | "id":"pizzavote" 712 | } 713 | ``` 714 | 715 | Update `function.json` with the following: 716 | ```javascript 717 | { 718 | "disabled": false, 719 | "bindings": [ 720 | { 721 | "authLevel": "function", 722 | "type": "httpTrigger", 723 | "direction": "in", 724 | "name": "req" 725 | }, 726 | { 727 | "type": "http", 728 | "direction": "out", 729 | "name": "res" 730 | }, 731 | { 732 | "type": "documentDB", 733 | "name": "inputDocument", 734 | "databaseName": "votingbot", 735 | "collectionName": "votingbot", 736 | "sqlQuery": "SELECT * from c where c.id = {id}", 737 | "connection": "votingbot_DOCUMENTDB", 738 | "direction": "in" 739 | } 740 | ] 741 | } 742 | ``` 743 | 744 | We can't delete CosmosDB documents using bindings, so we will use the `documentdb` npm package to delete the document. Update `index.js` with the following in the `DeleteVoting` folder: 745 | 746 | ```javascript 747 | var documentClient = require("documentdb").DocumentClient; 748 | var connectionString = process.env["votingbot_DOCUMENTDB"]; 749 | var arr = connectionString.split(';'); 750 | var endpoint = arr[0].split('=')[1]; 751 | var primaryKey = arr[1].split('=')[1] + "=="; 752 | var collectionUrl = 'dbs/votingbot/colls/votingbot'; 753 | var client = new documentClient(endpoint, { "masterKey": primaryKey }); 754 | 755 | module.exports = function (context, req) { 756 | context.log('JavaScript HTTP trigger function processed a request.'); 757 | 758 | if (req.body && req.body.id) { 759 | if(context.bindings.inputDocument && context.bindings.inputDocument.length == 1) { 760 | deleteDocument(req.body.id, context.bindings.inputDocument[0].id).then((result) => { 761 | console.log(`Deleted document: ${req.body.id}`); 762 | context.res = { 763 | status : 201, 764 | body: { "message" : `Deleted document: ${req.body.id}` } 765 | }; 766 | context.done(null, context.res); 767 | }, 768 | (err) => { 769 | context.log('error: ', err); 770 | context.res = { 771 | body: {"message" : "Error: " + JSON.stringify(err) } 772 | }; 773 | context.done(null, context.res); 774 | }); 775 | } 776 | else { 777 | context.res = { 778 | status : 400, 779 | body: { "message" : "Record with this id can not be found. Please pass a id of an existing document in the request body" } 780 | }; 781 | context.done(null, context.res); 782 | } 783 | } 784 | else { 785 | res = { 786 | status: 400, 787 | body: {"message" : "Please pass a name on the query string or in the request body" } 788 | }; 789 | context.done(null, res); 790 | } 791 | }; 792 | 793 | function deleteDocument(partitionKey, id) { 794 | let documentUrl = `${collectionUrl}/docs/${id}`; 795 | console.log(`Deleting document:\n${id}\n`); 796 | 797 | return new Promise((resolve, reject) => { 798 | client.deleteDocument(documentUrl, { 799 | partitionKey: [partitionKey] }, (err, result) => { 800 | if (err) reject(err); 801 | else { 802 | resolve(result); 803 | } 804 | }); 805 | }); 806 | }; 807 | ``` 808 | 809 | In this file we are using the `documentdb` npm package. So that you can test the function locally, you need to install the package too. You can easily do that in Visual Studio Code or from the command line / terminal: 810 | 811 | 1. Open the Integrated Terminal in Visual Studio Code by clicking Ctrl + ` or from the menu View -> Integrated Terminal 812 | 813 | 2. Navigate to the folder for your function: 814 | ```javascript 815 | cd DeleteVotingNode 816 | ``` 817 | 818 | 3. Then type 819 | ```javascript 820 | npm install documentdb 821 | ``` 822 | 823 | Now it is time to test the function. Again, in the Integrated Terminal, type 824 | ```javascript 825 | cd .. 826 | func host start 827 | ``` 828 | 829 | Run and test it similarly to the previous functions. For example: 830 | 831 | ![Postman testing](src/Content/Images/DeleteVoting-Postman.PNG) 832 | 833 | ## 4. Azure Configuration 834 | 835 | In order to get the code running in Azure you need a little bit more work. 836 | 837 | 1. Push your code in GitHub 838 | 2. Setup Azure Portal CLI following the instructions here - https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-azure-function-azure-cli 839 | 3. Then in the command prompt navigate to the folder of your funtion app and execute the following command:\ 840 | 841 | ```javascript 842 | 843 | func azure functionapp publish 844 | 845 | ``` 846 | Where FunctionAppName is the name of the Azure Function App. 847 | 848 | The last step is the configure App Settings. Go again to Platform Features of your Function App and select Application Settings. 849 | 850 | You have to set the following App Setting: 851 | 852 | Variable | Details 853 | ------------ | ------------- 854 | votingbot_DOCUMENTDB | connection string for DocumentDB as per previous steps 855 | 856 | ## 5. Integrate into Squire Bot 857 | 858 | In this final step of the lab we will integrate Voting Service with Squire Bot. 859 | 860 | We will add Voting Service to the features of Squire Bot. The first step is to add all the functions we created as tasks in Squire Bot's web app. 861 | 862 | Please follow the instructions for starting Squire Bot locally. Then access the webapp at http://localhost:4200 and configure each of the tasks providing name, description, POST url and required parametets. You can see below how Create Voting was setup: 863 | 864 | ![Create Voting Task](src/Content/Images/CreateVotingTask.PNG) 865 | 866 | Please use the following table to configure the rest of the tasks 867 | 868 | Task | Parameters 869 | ------------ | ------------- 870 | Create Voting | votingname, isOpen, question, options 871 | Close / Re-Open Voting | id, isOpen 872 | Vote | id, user, option 873 | Voting Status | id 874 | Delete Voting | id 875 | 876 | Now it is time to test Voting Service with Squire Bot. 877 | 878 | Please start Bot Framework emulator and connect to http://localhost:7071/api/bot 879 | 880 | Then start the dialog by typing the name of the task you defined in Squire Bot web app. Here it is an example for the voting task: 881 | 882 | ![Voting Task in Bot Emulator](src/Content/Images/SquireBotInAction.PNG) 883 | 884 | Great! You completed the module and now our Squire Bot is even smarter and can help you collect votes from friends and colleagues! 885 | --------------------------------------------------------------------------------