├── 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 | 
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 | 
34 |
35 | - Then type Logic Apps in the Search Bar
36 |
37 | 
38 |
39 | - Press Add button
40 |
41 | 
42 |
43 | - And fill the required data and press the create button at the bottom
44 |
45 | 
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 | 
50 |
51 | - In the Connector Search select Request / Response Connector
52 |
53 | 
54 |
55 | - Press “Use sample payload to generate schema” button
56 |
57 | 
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 | 
71 |
72 | - The result will look like
73 |
74 | 
75 |
76 | - Then press “New Step” button, select “Add an action” and select GitHub Connector with Action – “Create an issue”
77 |
78 | 
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 | 
83 |
84 | - For title and body will pick dynamic content fields - title and text
85 |
86 | 
87 |
88 | - Press “New step” and select Slack with Action – “Post message”
89 |
90 | 
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 | 
97 |
98 | - Save your work and now you are ready to test. Go to the Request / Response connector step and copy the URL
99 | 
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 | 
103 |
104 | - For the body use the JSON schema we defined several steps ago
105 | 
106 |
107 | - You should receive the message in Slack as follows
108 | 
109 |
110 | - You can go to your Logic App home screen and see the history of execution and troubleshoot
111 | 
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 | 
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 | 
35 |
36 | - Then type Logic Apps in the Search Bar
37 |
38 | 
39 |
40 | - Press Add button
41 |
42 | 
43 |
44 | - And fill the required data and press Create button at the bottom
45 |
46 | 
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 | 
51 |
52 | - In the Connector Search select Request / Response Connector
53 |
54 | 
55 |
56 | - Press “Use sample payload to generate schema” button
57 |
58 | 
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 | 
72 |
73 | - The result will look like
74 |
75 | 
76 |
77 | - Then press “New Step” button, select “Add an action” and select GitHub Connector with Action – “Create an issue”
78 |
79 | 
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 | 
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 | 
92 |
93 | - Press “New step” and search for Response and select Request - Response step:
94 |
95 | 
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 | 
110 |
111 | - Save your work and now you are ready to test. Go to the Request / Response connector step and copy the URL
112 | 
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 | 
117 |
118 | - For the body use the JSON schema we defined several steps ago
119 | 
120 |
121 | - You should receive the following reponse in Postman:
122 | 
123 |
124 | - You can go to your Logic App home screen and see the history of execution and troubleshoot
125 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
108 | 1. Add an action - **Google Calendar - List the events on a calendar**
109 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------