diff --git a/.fx/configs/azure.parameters.dev.json b/.fx/configs/azure.parameters.dev.json
new file mode 100644
index 0000000..d4c8e5f
--- /dev/null
+++ b/.fx/configs/azure.parameters.dev.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "provisionParameters": {
+ "value": {
+ "botAadAppClientId": "{{state.teams-bot.botId}}",
+ "botAadAppClientSecret": "{{state.teams-bot.botPassword}}",
+ "resourceBaseName": "openaiteam2ab769"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.fx/configs/config.dev.json b/.fx/configs/config.dev.json
new file mode 100644
index 0000000..8c5c3e7
--- /dev/null
+++ b/.fx/configs/config.dev.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://aka.ms/teamsfx-env-config-schema",
+ "description": "You can customize the TeamsFx config for different environments. Visit https://aka.ms/teamsfx-env-config to learn more about this.",
+ "manifest": {
+ "appName": {
+ "short": "openai-teams-bot",
+ "full": "Full name for openai-teams-bot"
+ },
+ "description": {
+ "short": "Short description of openai-teams-bot",
+ "full": "Full description of openai-teams-bot"
+ },
+ "icons": {
+ "color": "resources/color.png",
+ "outline": "resources/outline.png"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.fx/configs/projectSettings.json b/.fx/configs/projectSettings.json
new file mode 100644
index 0000000..3832d44
--- /dev/null
+++ b/.fx/configs/projectSettings.json
@@ -0,0 +1,60 @@
+{
+ "appName": "openai-teams-bot",
+ "projectId": "7b645719-832e-4408-a185-7d1d52f4011e",
+ "version": "2.1.0",
+ "components": [
+ {
+ "name": "teams-bot",
+ "hosting": "azure-web-app",
+ "provision": false,
+ "deploy": true,
+ "capabilities": [
+ "bot"
+ ],
+ "build": true,
+ "folder": "bot"
+ },
+ {
+ "name": "bot-service",
+ "provision": true
+ },
+ {
+ "name": "azure-web-app",
+ "scenario": "Bot",
+ "connections": [
+ "identity",
+ "teams-bot"
+ ]
+ },
+ {
+ "name": "identity",
+ "provision": true
+ }
+ ],
+ "programmingLanguage": "typescript",
+ "solutionSettings": {
+ "name": "fx-solution-azure",
+ "version": "1.0.0",
+ "hostType": "Azure",
+ "azureResources": [],
+ "capabilities": [
+ "Bot"
+ ],
+ "activeResourcePlugins": [
+ "fx-resource-local-debug",
+ "fx-resource-appstudio",
+ "fx-resource-cicd",
+ "fx-resource-api-connector",
+ "fx-resource-bot",
+ "fx-resource-identity"
+ ]
+ },
+ "pluginSettings": {
+ "fx-resource-bot": {
+ "host-type": "app-service",
+ "capabilities": [
+ "bot"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4a6b9ac
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+# TeamsFx files
+node_modules
+.fx/configs/localSettings.json
+.fx/states/*.userdata
+.DS_Store
+.env.teamsfx.local
+subscriptionInfo.json
+build
+.fx/configs/config.local.json
+.fx/states/state.local.json
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..cbfec1f
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,95 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch Remote (Edge)",
+ "type": "pwa-msedge",
+ "request": "launch",
+ "url": "https://teams.microsoft.com/l/app/${teamsAppId}?installAppPackage=true&webjoin=true&${account-hint}",
+ "presentation": {
+ "group": "remote",
+ "order": 1
+ },
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Launch Remote (Chrome)",
+ "type": "pwa-chrome",
+ "request": "launch",
+ "url": "https://teams.microsoft.com/l/app/${teamsAppId}?installAppPackage=true&webjoin=true&${account-hint}",
+ "presentation": {
+ "group": "remote",
+ "order": 2
+ },
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Launch Bot (Edge)",
+ "type": "pwa-msedge",
+ "request": "launch",
+ "url": "https://teams.microsoft.com/l/app/${localTeamsAppId}?installAppPackage=true&webjoin=true&${account-hint}",
+ "cascadeTerminateToConfigurations": [
+ "Attach to Bot"
+ ],
+ "presentation": {
+ "group": "all",
+ "hidden": true
+ },
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Launch Bot (Chrome)",
+ "type": "pwa-chrome",
+ "request": "launch",
+ "url": "https://teams.microsoft.com/l/app/${localTeamsAppId}?installAppPackage=true&webjoin=true&${account-hint}",
+ "cascadeTerminateToConfigurations": [
+ "Attach to Bot"
+ ],
+ "presentation": {
+ "group": "all",
+ "hidden": true
+ },
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Attach to Bot",
+ "type": "pwa-node",
+ "request": "attach",
+ "port": 9239,
+ "restart": true,
+ "presentation": {
+ "group": "all",
+ "hidden": true
+ },
+ "internalConsoleOptions": "neverOpen"
+ }
+ ],
+ "compounds": [
+ {
+ "name": "Debug (Edge)",
+ "configurations": [
+ "Launch Bot (Edge)",
+ "Attach to Bot"
+ ],
+ "preLaunchTask": "Start Teams App Locally",
+ "presentation": {
+ "group": "all",
+ "order": 1
+ },
+ "stopAll": true
+ },
+ {
+ "name": "Debug (Chrome)",
+ "configurations": [
+ "Launch Bot (Chrome)",
+ "Attach to Bot"
+ ],
+ "preLaunchTask": "Start Teams App Locally",
+ "presentation": {
+ "group": "all",
+ "order": 2
+ },
+ "stopAll": true
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..e576a41
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "debug.onTaskErrors": "abort",
+ "json.schemas": [
+ {
+ "fileMatch": [
+ "/aad.*.json"
+ ],
+ "schema": {}
+ }
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..92431a1
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,122 @@
+// This file is automatically generated by Teams Toolkit.
+// The teamsfx tasks defined in this file require Teams Toolkit version >= 4.1.0.
+// See https://aka.ms/teamsfx-debug-tasks for details on how to customize each task and how to integrate with existing Teams Toolkit projects.
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Start Teams App Locally",
+ "dependsOn": [
+ "Validate & install prerequisites",
+ "Install npm packages",
+ "Start local tunnel",
+ "Set up bot",
+ "Build & upload Teams manifest",
+ "Start services"
+ ],
+ "dependsOrder": "sequence"
+ },
+ {
+ // Check if all required prerequisites are installed and will install them if not.
+ // See https://aka.ms/teamsfx-check-prerequisites-task to know the details and how to customize the args.
+ "label": "Validate & install prerequisites",
+ "type": "teamsfx",
+ "command": "debug-check-prerequisites",
+ "args": {
+ "prerequisites": [
+ "nodejs", // Validate if Node.js is installed.
+ "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission.
+ "ngrok", // Install Ngrok. Bot project requires a public message endpoint, and ngrok can help create public tunnel for your local service.
+ "portOccupancy" // Validate available ports to ensure those debug ones are not occupied.
+ ],
+ "portOccupancy": [
+ 3978, // bot service port
+ 9239 // bot inspector port for Node.js debugger
+ ]
+ }
+ },
+ {
+ // Check if all the npm packages are installed and will install them if not.
+ // See https://aka.ms/teamsfx-npm-package-task to know the details and how to customize the args.
+ "label": "Install npm packages",
+ "type": "teamsfx",
+ "command": "debug-npm-install",
+ "args": {
+ "projects": [
+ {
+ "cwd": "${workspaceFolder}/bot",
+ "npmInstallArgs": [
+ "--no-audit"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ // Start the local tunnel service to forward public ngrok URL to local port and inspect traffic.
+ // See https://aka.ms/teamsfx-local-tunnel-task to know the details and how to customize the args.
+ "label": "Start local tunnel",
+ "type": "teamsfx",
+ "command": "debug-start-local-tunnel",
+ "args": {
+ "ngrokArgs": "http 3978 --log=stdout --log-format=logfmt"
+ },
+ "isBackground": true,
+ "problemMatcher": "$teamsfx-local-tunnel-watch"
+ },
+ {
+ // Register resources and prepare local launch information for Bot.
+ // See https://aka.ms/teamsfx-debug-set-up-bot-task to know the details and how to customize the args.
+ "label": "Set up bot",
+ "type": "teamsfx",
+ "command": "debug-set-up-bot",
+ "args": {
+ //// Enter your own bot information if using the existing bot. ////
+ // "botId": "",
+ // "botPassword": "", // use plain text or environment variable reference like ${env:BOT_PASSWORD}
+ "botMessagingEndpoint": "/api/messages" // use your own routing "/any/path", or full URL "https://contoso.com/any/path"
+ }
+ },
+ {
+ // Build and upload Teams manifest.
+ // See https://aka.ms/teamsfx-debug-prepare-manifest-task to know the details and how to customize the args.
+ "label": "Build & upload Teams manifest",
+ "type": "teamsfx",
+ "command": "debug-prepare-manifest",
+ "args": {
+ //// Enter your own Teams app package path if using the existing Teams manifest. ////
+ // "appPackagePath": ""
+ }
+ },
+ {
+ "label": "Start services",
+ "dependsOn": [
+ "Start bot"
+ ]
+ },
+ {
+ "label": "Start bot",
+ "type": "shell",
+ "command": "npm run dev:teamsfx",
+ "isBackground": true,
+ "options": {
+ "cwd": "${workspaceFolder}/bot"
+ },
+ "problemMatcher": {
+ "pattern": [
+ {
+ "regexp": "^.*$",
+ "file": 0,
+ "location": 1,
+ "message": 2
+ }
+ ],
+ "background": {
+ "activeOnStart": true,
+ "beginsPattern": "[nodemon] starting",
+ "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed"
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/bot/.gitignore b/bot/.gitignore
new file mode 100644
index 0000000..998bdab
--- /dev/null
+++ b/bot/.gitignore
@@ -0,0 +1,10 @@
+# dependencies
+node_modules/
+
+# misc
+.env
+.deployment
+.DS_Store
+
+# build
+lib/
diff --git a/bot/.webappignore b/bot/.webappignore
new file mode 100644
index 0000000..629e316
--- /dev/null
+++ b/bot/.webappignore
@@ -0,0 +1,17 @@
+.fx
+.deployment
+.vscode
+*.js.map
+*.ts.map
+*.ts
+.git*
+.tsbuildinfo
+CHANGELOG.md
+readme.md
+local.settings.json
+test
+tsconfig.json
+.DS_Store
+node_modules/.bin
+node_modules/ts-node
+node_modules/typescript
\ No newline at end of file
diff --git a/bot/README.md b/bot/README.md
new file mode 100644
index 0000000..9efacf0
--- /dev/null
+++ b/bot/README.md
@@ -0,0 +1,119 @@
+# How to use this Bots and Message Extensions HelloWorld app
+
+A bot, chatbot, or conversational bot is an app that responds to simple commands sent in chat and replies in meaningful ways. Examples of bots in everyday use include: bots that notify about build failures, bots that provide information about the weather or bus schedules, or provide travel information. A bot interaction can be a quick question and answer, or it can be a complex conversation. Being a cloud application, a bot can provide valuable and secure access to cloud services and corporate resources.
+
+A Message Extension allows users to interact with your web service while composing messages in the Microsoft Teams client. Users can invoke your web service to assist message composition, from the message compose box, or from the search bar.
+
+Message Extensions are implemented on top of the Bot support architecture within Teams.
+
+This is a simple hello world application with both Bot and Message extension capabilities.
+
+## Prerequisites
+
+- [NodeJS](https://nodejs.org/en/)
+- An M365 account. If you do not have M365 account, apply one from [M365 developer program](https://developer.microsoft.com/en-us/microsoft-365/dev-program)
+- [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version after 1.55 or [TeamsFx CLI](https://aka.ms/teamsfx-cli)
+
+## Debug
+
+- From Visual Studio Code: Start debugging the project by hitting the `F5` key in Visual Studio Code.
+- Alternatively use the `Run and Debug Activity Panel` in Visual Studio Code and click the `Run and Debug` green arrow button.
+- From TeamsFx CLI: Start debugging the project by executing the command `teamsfx preview --local` in your project directory.
+
+## Edit the manifest
+
+You can find the Teams app manifest in `templates/appPackage` folder. The folder contains one manifest file:
+* `manifest.template.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure).
+
+This file contains template arguments with `{...}` statements which will be replaced at build time. You may add any extra properties or permissions you require to this file. See the [schema reference](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) for more information.
+
+## Deploy to Azure
+
+Deploy your project to Azure by following these steps:
+
+| From Visual Studio Code | From TeamsFx CLI |
+| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+|
- Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
- After you signed in, select a subscription under your account.
- Open the Teams Toolkit and click `Provision in the cloud` from DEPLOYMENT section or open the command palette and select: `Teams: Provision in the cloud`.
- Open the Teams Toolkit and click `Deploy to the cloud` or open the command palette and select: `Teams: Deploy to the cloud`.
| - Run command `teamsfx account login azure`.
- Run command `teamsfx account set --subscription `.
- Run command `teamsfx provision`.
- Run command: `teamsfx deploy`.
|
+
+> Note: Provisioning and deployment may incur charges to your Azure Subscription.
+
+## Preview
+
+Once the provisioning and deployment steps are finished, you can preview your app:
+
+- From Visual Studio Code
+
+ 1. Open the `Run and Debug Activity Panel`.
+ 1. Select `Launch Remote (Edge)` or `Launch Remote (Chrome)` from the launch configuration drop-down.
+ 1. Press the Play (green arrow) button to launch your app - now running remotely from Azure.
+
+- From TeamsFx CLI: execute `teamsfx preview --remote` in your project directory to launch your application.
+
+## Validate manifest file
+
+To check that your manifest file is valid:
+
+- From Visual Studio Code: open the command palette and select: `Teams: Validate manifest file`.
+- From TeamsFx CLI: run command `teamsfx validate` in your project directory.
+
+## Package
+
+- From Visual Studio Code: open the Teams Toolkit and click `Zip Teams metadata package` or open the command palette and select `Teams: Zip Teams metadata package`.
+- Alternatively, from the command line run `teamsfx package` in the project directory.
+
+## Publish to Teams
+
+Once deployed, you may want to distribute your application to your organization's internal app store in Teams. Your app will be submitted for admin approval.
+
+- From Visual Studio Code: open the Teams Toolkit and click `Publish to Teams` or open the command palette and select: `Teams: Publish to Teams`.
+- From TeamsFx CLI: run command `teamsfx publish` in your project directory.
+
+## Play with Messging Extension
+
+This template provides some sample functionality:
+
+- You can search for `npm` packages from the search bar.
+
+- You can create and send an adaptive card.
+
+ ![CreateCard](./images/AdaptiveCard.png)
+
+- You can share a message in an adaptive card form.
+
+ ![ShareMessage](./images/ShareMessage.png)
+
+- You can paste a link that "unfurls" (`.botframwork.com` is monitored in this template) and a card will be rendered.
+
+ ![ComposeArea](./images/LinkUnfurlingImage.png)
+
+To trigger these functions, there are multiple entry points:
+
+- `@mention` Your message extension, from the `search box area`.
+
+ ![AtBotFromSearch](./images/AtBotFromSearch.png)
+
+- `@mention` your message extension from the `compose message area`.
+
+ ![AtBotFromMessage](./images/AtBotInMessage.png)
+
+- Click the `...` under compose message area, find your message extension.
+
+ ![ComposeArea](./images/ThreeDot.png)
+
+- Click the `...` next to any messages you received or sent.
+
+ ![ComposeArea](./images/ThreeDotOnMessage.png)
+
+## Further reading
+
+### Bot
+
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Bot Framework Documentation](https://docs.botframework.com/)
+- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
+
+### Message Extension
+
+- [Search Command](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command)
+- [Action Command](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command)
+- [Link Unfurling](https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=dotnet)
diff --git a/bot/adaptiveCards/learn.json b/bot/adaptiveCards/learn.json
new file mode 100644
index 0000000..59d6cc7
--- /dev/null
+++ b/bot/adaptiveCards/learn.json
@@ -0,0 +1,45 @@
+{
+ "type": "AdaptiveCard",
+ "body": [
+ {
+ "type": "TextBlock",
+ "size": "Medium",
+ "weight": "Bolder",
+ "text": "Learn Adaptive Card and Commands"
+ },
+ {
+ "type": "TextBlock",
+ "text": "Now you have triggered a command that sends this card! Go to documentations to learn more about Adaptive Card and Commands in Teams Bot. Click on \"I like this\" below if you think this is helpful.",
+ "wrap": true
+ },
+ {
+ "type": "FactSet",
+ "facts": [
+ {
+ "title": "Like Count:",
+ "value": "${likeCount}"
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "Action.Execute",
+ "title": "I Like This!",
+ "verb": "userlike",
+ "fallback": "Action.Submit"
+ },
+ {
+ "type": "Action.OpenUrl",
+ "title": "Adaptive Card Docs",
+ "url": "https://docs.microsoft.com/en-us/adaptive-cards/"
+ },
+ {
+ "type": "Action.OpenUrl",
+ "title": "Bot Command Docs",
+ "url": "https://docs.microsoft.com/en-us/microsoftteams/platform/bots/how-to/create-a-bot-commands-menu?tabs=desktop%2Cdotnet"
+ }
+ ],
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.4"
+}
diff --git a/bot/adaptiveCards/welcome.json b/bot/adaptiveCards/welcome.json
new file mode 100644
index 0000000..f59bb0d
--- /dev/null
+++ b/bot/adaptiveCards/welcome.json
@@ -0,0 +1,30 @@
+{
+ "type": "AdaptiveCard",
+ "body": [
+ {
+ "type": "TextBlock",
+ "size": "Medium",
+ "weight": "Bolder",
+ "text": "Your Hello World Bot is Running"
+ },
+ {
+ "type": "TextBlock",
+ "text": "Congratulations! Your hello world bot is running. Click the documentation below to learn more about Bots and the Teams Toolkit.",
+ "wrap": true
+ }
+ ],
+ "actions": [
+ {
+ "type": "Action.OpenUrl",
+ "title": "Bot Framework Docs",
+ "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0"
+ },
+ {
+ "type": "Action.OpenUrl",
+ "title": "Teams Toolkit Docs",
+ "url": "https://aka.ms/teamsfx-docs"
+ }
+ ],
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.4"
+}
diff --git a/bot/config.ts b/bot/config.ts
new file mode 100644
index 0000000..e9b800a
--- /dev/null
+++ b/bot/config.ts
@@ -0,0 +1,6 @@
+const config = {
+ botId: process.env.BOT_ID,
+ botPassword: process.env.BOT_PASSWORD,
+};
+
+export default config;
diff --git a/bot/images/AdaptiveCard.png b/bot/images/AdaptiveCard.png
new file mode 100644
index 0000000..98cfad6
Binary files /dev/null and b/bot/images/AdaptiveCard.png differ
diff --git a/bot/images/AtBotFromSearch.png b/bot/images/AtBotFromSearch.png
new file mode 100644
index 0000000..5cf1bf5
Binary files /dev/null and b/bot/images/AtBotFromSearch.png differ
diff --git a/bot/images/AtBotInMessage.png b/bot/images/AtBotInMessage.png
new file mode 100644
index 0000000..e5f8767
Binary files /dev/null and b/bot/images/AtBotInMessage.png differ
diff --git a/bot/images/LinkUnfurlingImage.png b/bot/images/LinkUnfurlingImage.png
new file mode 100644
index 0000000..f288ff5
Binary files /dev/null and b/bot/images/LinkUnfurlingImage.png differ
diff --git a/bot/images/ShareMessage.png b/bot/images/ShareMessage.png
new file mode 100644
index 0000000..702769a
Binary files /dev/null and b/bot/images/ShareMessage.png differ
diff --git a/bot/images/ThreeDot.png b/bot/images/ThreeDot.png
new file mode 100644
index 0000000..bbc1df4
Binary files /dev/null and b/bot/images/ThreeDot.png differ
diff --git a/bot/images/ThreeDotOnMessage.png b/bot/images/ThreeDotOnMessage.png
new file mode 100644
index 0000000..f7e8c43
Binary files /dev/null and b/bot/images/ThreeDotOnMessage.png differ
diff --git a/bot/index.ts b/bot/index.ts
new file mode 100644
index 0000000..6c728de
--- /dev/null
+++ b/bot/index.ts
@@ -0,0 +1,56 @@
+// Import required packages
+import * as restify from "restify";
+
+// Import required bot services.
+// See https://aka.ms/bot-services to learn more about the different parts of a bot.
+import { BotFrameworkAdapter, TurnContext } from "botbuilder";
+
+// This bot's main dialog.
+import { TeamsBot } from "./teamsBot";
+import config from "./config";
+
+// Create adapter.
+// See https://aka.ms/about-bot-adapter to learn more about adapters.
+const adapter = new BotFrameworkAdapter({
+ appId: config.botId,
+ appPassword: config.botPassword,
+});
+
+// Catch-all for errors.
+const onTurnErrorHandler = async (context: TurnContext, error: Error) => {
+ // This check writes out errors to console log .vs. app insights.
+ // NOTE: In production environment, you should consider logging this to Azure
+ // application insights.
+ console.error(`\n [onTurnError] unhandled error: ${error}`);
+
+ // Send a trace activity, which will be displayed in Bot Framework Emulator
+ await context.sendTraceActivity(
+ "OnTurnError Trace",
+ `${error}`,
+ "https://www.botframework.com/schemas/error",
+ "TurnError"
+ );
+
+ // Send a message to the user
+ await context.sendActivity(`The bot encountered unhandled error:\n ${error.message}`);
+ await context.sendActivity("To continue to run this bot, please fix the bot source code.");
+};
+
+// Set the onTurnError for the singleton BotFrameworkAdapter.
+adapter.onTurnError = onTurnErrorHandler;
+
+// Create the bot that will handle incoming messages.
+const bot = new TeamsBot();
+
+// Create HTTP server.
+const server = restify.createServer();
+server.listen(process.env.port || process.env.PORT || 3978, () => {
+ console.log(`\nBot Started, ${server.name} listening to ${server.url}`);
+});
+
+// Listen for incoming requests.
+server.post("/api/messages", async (req, res) => {
+ await adapter.processActivity(req, res, async (context) => {
+ await bot.run(context);
+ });
+});
diff --git a/bot/package.json b/bot/package.json
new file mode 100644
index 0000000..a70609f
--- /dev/null
+++ b/bot/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "echobot",
+ "version": "1.0.0",
+ "description": "Microsoft Teams Toolkit hello world Bot sample",
+ "author": "Microsoft",
+ "license": "MIT",
+ "main": "./lib/index.js",
+ "scripts": {
+ "dev:teamsfx": "env-cmd --silent -f .env.teamsfx.local npm run dev",
+ "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./index.ts",
+ "build": "tsc --build && shx cp -r ./adaptiveCards ./lib/",
+ "start": "node ./lib/index.js",
+ "watch": "nodemon --exec \"npm run start\"",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com"
+ },
+ "dependencies": {
+ "@microsoft/adaptivecards-tools": "^1.0.0",
+ "botbuilder": "^4.18.0",
+ "restify": "^8.5.1"
+ },
+ "devDependencies": {
+ "@types/restify": "8.4.2",
+ "env-cmd": "^10.1.0",
+ "ts-node": "^10.4.0",
+ "typescript": "^4.4.4",
+ "nodemon": "^2.0.7",
+ "shx": "^0.3.3"
+ }
+}
\ No newline at end of file
diff --git a/bot/teamsBot.ts b/bot/teamsBot.ts
new file mode 100644
index 0000000..e43b7d4
--- /dev/null
+++ b/bot/teamsBot.ts
@@ -0,0 +1,241 @@
+import { default as axios } from "axios";
+import * as querystring from "querystring";
+import {
+ TeamsActivityHandler,
+ CardFactory,
+ TurnContext,
+ AdaptiveCardInvokeValue,
+ AdaptiveCardInvokeResponse,
+} from "botbuilder";
+import rawWelcomeCard from "./adaptiveCards/welcome.json";
+import rawLearnCard from "./adaptiveCards/learn.json";
+import { AdaptiveCards } from "@microsoft/adaptivecards-tools";
+
+export interface DataInterface {
+ likeCount: number;
+}
+
+export class TeamsBot extends TeamsActivityHandler {
+ // record the likeCount
+ likeCountObj: { likeCount: number };
+
+ constructor() {
+ super();
+
+ this.likeCountObj = { likeCount: 0 };
+
+ this.onMessage(async (context, next) => {
+ console.log("Running with Message Activity.");
+
+ let txt = context.activity.text;
+ const removedMentionText = TurnContext.removeRecipientMention(context.activity);
+ if (removedMentionText) {
+ // Remove the line break
+ txt = removedMentionText.toLowerCase().replace(/\n|\r/g, "").trim();
+ }
+
+ // Trigger command by IM text
+ switch (txt) {
+ case "welcome": {
+ const card = AdaptiveCards.declareWithoutData(rawWelcomeCard).render();
+ await context.sendActivity({ attachments: [CardFactory.adaptiveCard(card)] });
+ break;
+ }
+ case "learn": {
+ this.likeCountObj.likeCount = 0;
+ const card = AdaptiveCards.declare(rawLearnCard).render(this.likeCountObj);
+ await context.sendActivity({ attachments: [CardFactory.adaptiveCard(card)] });
+ break;
+ }
+ /**
+ * case "yourCommand": {
+ * await context.sendActivity(`Add your response here!`);
+ * break;
+ * }
+ */
+ }
+
+ // By calling next() you ensure that the next BotHandler is run.
+ await next();
+ });
+
+ this.onMembersAdded(async (context, next) => {
+ const membersAdded = context.activity.membersAdded;
+ for (let cnt = 0; cnt < membersAdded.length; cnt++) {
+ if (membersAdded[cnt].id) {
+ const card = AdaptiveCards.declareWithoutData(rawWelcomeCard).render();
+ await context.sendActivity({ attachments: [CardFactory.adaptiveCard(card)] });
+ break;
+ }
+ }
+ await next();
+ });
+ }
+
+ // Invoked when an action is taken on an Adaptive Card. The Adaptive Card sends an event to the Bot and this
+ // method handles that event.
+ async onAdaptiveCardInvoke(
+ context: TurnContext,
+ invokeValue: AdaptiveCardInvokeValue
+ ): Promise {
+ // The verb "userlike" is sent from the Adaptive Card defined in adaptiveCards/learn.json
+ if (invokeValue.action.verb === "userlike") {
+ this.likeCountObj.likeCount++;
+ const card = AdaptiveCards.declare(rawLearnCard).render(this.likeCountObj);
+ await context.updateActivity({
+ type: "message",
+ id: context.activity.replyToId,
+ attachments: [CardFactory.adaptiveCard(card)],
+ });
+ return { statusCode: 200, type: undefined, value: undefined };
+ }
+ }
+
+ // Message extension Code
+ // Action.
+ public async handleTeamsMessagingExtensionSubmitAction(
+ context: TurnContext,
+ action: any
+ ): Promise {
+ switch (action.commandId) {
+ case "createCard":
+ return createCardCommand(context, action);
+ case "shareMessage":
+ return shareMessageCommand(context, action);
+ default:
+ throw new Error("NotImplemented");
+ }
+ }
+
+ // Search.
+ public async handleTeamsMessagingExtensionQuery(context: TurnContext, query: any): Promise {
+ const searchQuery = query.parameters[0].value;
+ const response = await axios.get(
+ `http://registry.npmjs.com/-/v1/search?${querystring.stringify({
+ text: searchQuery,
+ size: 8,
+ })}`
+ );
+
+ const attachments = [];
+ response.data.objects.forEach((obj) => {
+ const heroCard = CardFactory.heroCard(obj.package.name);
+ const preview = CardFactory.heroCard(obj.package.name);
+ preview.content.tap = {
+ type: "invoke",
+ value: { name: obj.package.name, description: obj.package.description },
+ };
+ const attachment = { ...heroCard, preview };
+ attachments.push(attachment);
+ });
+
+ return {
+ composeExtension: {
+ type: "result",
+ attachmentLayout: "list",
+ attachments: attachments,
+ },
+ };
+ }
+
+ public async handleTeamsMessagingExtensionSelectItem(
+ context: TurnContext,
+ obj: any
+ ): Promise {
+ return {
+ composeExtension: {
+ type: "result",
+ attachmentLayout: "list",
+ attachments: [CardFactory.heroCard(obj.name, obj.description)],
+ },
+ };
+ }
+
+ // Link Unfurling.
+ public async handleTeamsAppBasedLinkQuery(context: TurnContext, query: any): Promise {
+ const attachment = CardFactory.thumbnailCard("Image Preview Card", query.url, [query.url]);
+
+ const result = {
+ attachmentLayout: "list",
+ type: "result",
+ attachments: [attachment],
+ };
+
+ const response = {
+ composeExtension: result,
+ };
+ return response;
+ }
+}
+
+async function createCardCommand(context: TurnContext, action: any): Promise {
+ // The user has chosen to create a card by choosing the 'Create Card' context menu command.
+ const data = action.data;
+ const heroCard = CardFactory.heroCard(data.title, data.text);
+ heroCard.content.subtitle = data.subTitle;
+ const attachment = {
+ contentType: heroCard.contentType,
+ content: heroCard.content,
+ preview: heroCard,
+ };
+
+ return {
+ composeExtension: {
+ type: "result",
+ attachmentLayout: "list",
+ attachments: [attachment],
+ },
+ };
+}
+
+async function shareMessageCommand(context: TurnContext, action: any): Promise {
+ // The user has chosen to share a message by choosing the 'Share Message' context menu command.
+ let userName = "unknown";
+ if (
+ action.messagePayload &&
+ action.messagePayload.from &&
+ action.messagePayload.from.user &&
+ action.messagePayload.from.user.displayName
+ ) {
+ userName = action.messagePayload.from.user.displayName;
+ }
+
+ // This Message Extension example allows the user to check a box to include an image with the
+ // shared message. This demonstrates sending custom parameters along with the message payload.
+ let images = [];
+ const includeImage = action.data.includeImage;
+ if (includeImage === "true") {
+ images = [
+ "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU",
+ ];
+ }
+ const heroCard = CardFactory.heroCard(
+ `${userName} originally sent this message:`,
+ action.messagePayload.body.content,
+ images
+ );
+
+ if (
+ action.messagePayload &&
+ action.messagePayload.attachment &&
+ action.messagePayload.attachments.length > 0
+ ) {
+ // This sample does not add the MessagePayload Attachments. This is left as an
+ // exercise for the user.
+ heroCard.content.subtitle = `(${action.messagePayload.attachments.length} Attachments not included)`;
+ }
+
+ const attachment = {
+ contentType: heroCard.contentType,
+ content: heroCard.content,
+ preview: heroCard,
+ };
+
+ return {
+ composeExtension: {
+ type: "result",
+ attachmentLayout: "list",
+ attachments: [attachment],
+ },
+ };
+}
diff --git a/bot/tsconfig.json b/bot/tsconfig.json
new file mode 100644
index 0000000..479f586
--- /dev/null
+++ b/bot/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "target": "es2016",
+ "module": "commonjs",
+ "outDir": "./lib",
+ "rootDir": "./",
+ "sourceMap": true,
+ "incremental": true,
+ "tsBuildInfoFile": "./lib/.tsbuildinfo",
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d61ecbf
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "openai-teams-bot",
+ "version": "0.0.1",
+ "description": "",
+ "author": "",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "devDependencies": {
+ "@microsoft/teamsfx-cli": "1.*"
+ },
+ "license": "MIT"
+}
\ No newline at end of file
diff --git a/templates/appPackage/manifest.template.json b/templates/appPackage/manifest.template.json
new file mode 100644
index 0000000..8f087d4
--- /dev/null
+++ b/templates/appPackage/manifest.template.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.14/MicrosoftTeams.schema.json",
+ "manifestVersion": "1.14",
+ "version": "1.0.0",
+ "id": "{{state.fx-resource-appstudio.teamsAppId}}",
+ "packageName": "com.microsoft.teams.extension",
+ "developer": {
+ "name": "Teams App, Inc.",
+ "websiteUrl": "https://www.example.com",
+ "privacyUrl": "https://www.example.com/termofuse",
+ "termsOfUseUrl": "https://www.example.com/privacy"
+ },
+ "icons": {
+ "color": "{{config.manifest.icons.color}}",
+ "outline": "{{config.manifest.icons.outline}}"
+ },
+ "name": {
+ "short": "{{config.manifest.appName.short}}",
+ "full": "{{config.manifest.appName.full}}"
+ },
+ "description": {
+ "short": "{{config.manifest.description.short}}",
+ "full": "{{config.manifest.description.full}}"
+ },
+ "accentColor": "#FFFFFF",
+ "bots": [
+ {
+ "botId": "{{state.fx-resource-bot.botId}}",
+ "scopes": [
+ "personal",
+ "team",
+ "groupchat"
+ ],
+ "supportsFiles": false,
+ "isNotificationOnly": false,
+ "commandLists": [
+ {
+ "scopes": [
+ "personal",
+ "team",
+ "groupchat"
+ ],
+ "commands": [
+ {
+ "title": "welcome",
+ "description": "Resend welcome card of this Bot"
+ },
+ {
+ "title": "learn",
+ "description": "Learn about Adaptive Card and Bot Command"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "composeExtensions": [],
+ "configurableTabs": [],
+ "staticTabs": [],
+ "permissions": [
+ "identity",
+ "messageTeamMembers"
+ ],
+ "validDomains": [
+ "{{state.fx-resource-bot.domain}}"
+ ]
+}
\ No newline at end of file
diff --git a/templates/appPackage/resources/color.png b/templates/appPackage/resources/color.png
new file mode 100644
index 0000000..f27ccf2
Binary files /dev/null and b/templates/appPackage/resources/color.png differ
diff --git a/templates/appPackage/resources/outline.png b/templates/appPackage/resources/outline.png
new file mode 100644
index 0000000..e8cb4b6
Binary files /dev/null and b/templates/appPackage/resources/outline.png differ
diff --git a/templates/azure/config.bicep b/templates/azure/config.bicep
new file mode 100644
index 0000000..9f38338
--- /dev/null
+++ b/templates/azure/config.bicep
@@ -0,0 +1,16 @@
+@secure()
+param provisionParameters object
+param provisionOutputs object
+
+// Get existing app settings for merge
+var currentAppSettings = list('${ provisionOutputs.azureWebAppBotOutput.value.resourceId }/config/appsettings', '2021-02-01').properties
+
+// Merge TeamsFx configurations to Bot resources
+module teamsFxAzureWebAppBotConfig './teamsFx/azureWebAppBotConfig.bicep' = {
+ name: 'teamsFxAzureWebAppBotConfig'
+ params: {
+ provisionParameters: provisionParameters
+ provisionOutputs: provisionOutputs
+ currentAppSettings: currentAppSettings
+ }
+}
\ No newline at end of file
diff --git a/templates/azure/main.bicep b/templates/azure/main.bicep
new file mode 100644
index 0000000..2ad02e7
--- /dev/null
+++ b/templates/azure/main.bicep
@@ -0,0 +1,18 @@
+@secure()
+param provisionParameters object
+
+module provision './provision.bicep' = {
+ name: 'provisionResources'
+ params: {
+ provisionParameters: provisionParameters
+ }
+}
+output provisionOutput object = provision
+module config './config.bicep' = {
+ name: 'configureResources'
+ params: {
+ provisionParameters: provisionParameters
+ provisionOutputs: provision
+ }
+}
+output configOutput object = contains(reference(resourceId('Microsoft.Resources/deployments', config.name), '2020-06-01'), 'outputs') ? config : {}
diff --git a/templates/azure/provision.bicep b/templates/azure/provision.bicep
new file mode 100644
index 0000000..3c7178f
--- /dev/null
+++ b/templates/azure/provision.bicep
@@ -0,0 +1,51 @@
+@secure()
+param provisionParameters object
+
+// Merge TeamsFx configurations to Bot service
+module botProvision './provision/botService.bicep' = {
+ name: 'botProvision'
+ params: {
+ provisionParameters: provisionParameters
+ botEndpoint: azureWebAppBotProvision.outputs.siteEndpoint
+ }
+}
+
+// Resources web app
+module azureWebAppBotProvision './provision/azureWebAppBot.bicep' = {
+ name: 'azureWebAppBotProvision'
+ params: {
+ provisionParameters: provisionParameters
+ userAssignedIdentityId: userAssignedIdentityProvision.outputs.identityResourceId
+ }
+}
+
+
+output azureWebAppBotOutput object = {
+ teamsFxPluginId: 'teams-bot'
+ skuName: azureWebAppBotProvision.outputs.skuName
+ siteName: azureWebAppBotProvision.outputs.siteName
+ domain: azureWebAppBotProvision.outputs.domain
+ appServicePlanName: azureWebAppBotProvision.outputs.appServicePlanName
+ resourceId: azureWebAppBotProvision.outputs.resourceId
+ siteEndpoint: azureWebAppBotProvision.outputs.siteEndpoint
+}
+
+output BotOutput object = {
+ domain: azureWebAppBotProvision.outputs.domain
+ endpoint: azureWebAppBotProvision.outputs.siteEndpoint
+}
+
+// Resources for identity
+module userAssignedIdentityProvision './provision/identity.bicep' = {
+ name: 'userAssignedIdentityProvision'
+ params: {
+ provisionParameters: provisionParameters
+ }
+}
+
+output identityOutput object = {
+ teamsFxPluginId: 'identity'
+ identityName: userAssignedIdentityProvision.outputs.identityName
+ identityResourceId: userAssignedIdentityProvision.outputs.identityResourceId
+ identityClientId: userAssignedIdentityProvision.outputs.identityClientId
+}
\ No newline at end of file
diff --git a/templates/azure/provision/azureWebAppBot.bicep b/templates/azure/provision/azureWebAppBot.bicep
new file mode 100644
index 0000000..d4a0f81
--- /dev/null
+++ b/templates/azure/provision/azureWebAppBot.bicep
@@ -0,0 +1,61 @@
+@secure()
+param provisionParameters object
+param userAssignedIdentityId string
+
+var resourceBaseName = provisionParameters.resourceBaseName
+var serverfarmsName = contains(provisionParameters, 'webAppServerfarmsName') ? provisionParameters['webAppServerfarmsName'] : '${resourceBaseName}bot' // Try to read name for App Service Plan from parameters
+var webAppSKU = contains(provisionParameters, 'webAppSKU') ? provisionParameters['webAppSKU'] : 'B1' // Try to read SKU for Azure Web App from parameters
+var webAppName = contains(provisionParameters, 'webAppSitesName') ? provisionParameters['webAppSitesName'] : '${resourceBaseName}bot' // Try to read name for Azure Web App from parameters
+
+// Compute resources for your Web App
+resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = {
+ kind: 'app'
+ location: resourceGroup().location
+ name: serverfarmsName
+ sku: {
+ name: webAppSKU
+ }
+ properties: {}
+}
+
+// Web App that hosts your app
+resource webApp 'Microsoft.Web/sites@2021-02-01' = {
+ kind: 'app'
+ location: resourceGroup().location
+ name: webAppName
+ properties: {
+ serverFarmId: serverfarm.id
+ keyVaultReferenceIdentity: userAssignedIdentityId // Use given user assigned identity to access Key Vault
+ httpsOnly: true
+ siteConfig: {
+ alwaysOn: true
+ appSettings: [
+ {
+ name: 'WEBSITE_NODE_DEFAULT_VERSION'
+ value: '~16' // Set NodeJS version to 16.x for your site
+ }
+ {
+ name: 'SCM_SCRIPT_GENERATOR_ARGS'
+ value: '--node' // Register as node server
+ }
+ {
+ name: 'RUNNING_ON_AZURE'
+ value: '1'
+ }
+ ]
+ }
+ }
+ identity: {
+ type: 'UserAssigned'
+ userAssignedIdentities: {
+ '${userAssignedIdentityId}': {} // The identity is used to access other Azure resources
+ }
+ }
+}
+
+output skuName string = webAppSKU
+output siteName string = webAppName
+output domain string = webApp.properties.defaultHostName
+output appServicePlanName string = serverfarmsName
+output resourceId string = webApp.id
+output siteEndpoint string = 'https://${webApp.properties.defaultHostName}'
\ No newline at end of file
diff --git a/templates/azure/provision/botService.bicep b/templates/azure/provision/botService.bicep
new file mode 100644
index 0000000..a8019ae
--- /dev/null
+++ b/templates/azure/provision/botService.bicep
@@ -0,0 +1,33 @@
+@secure()
+param provisionParameters object
+param botEndpoint string
+var resourceBaseName = provisionParameters.resourceBaseName
+var botAadAppClientId = provisionParameters['botAadAppClientId'] // Read AAD app client id for Azure Bot Service from parameters
+var botServiceName = contains(provisionParameters, 'botServiceName') ? provisionParameters['botServiceName'] : '${resourceBaseName}' // Try to read name for Azure Bot Service from parameters
+var botServiceSku = contains(provisionParameters, 'botServiceSku') ? provisionParameters['botServiceSku'] : 'F0' // Try to read SKU for Azure Bot Service from parameters
+var botDisplayName = contains(provisionParameters, 'botDisplayName') ? provisionParameters['botDisplayName'] : '${resourceBaseName}' // Try to read display name for Azure Bot Service from parameters
+
+// Register your web service as a bot with the Bot Framework
+resource azureBot 'Microsoft.BotService/botServices@2021-03-01' = {
+ kind: 'azurebot'
+ location: 'global'
+ name: botServiceName
+ properties: {
+ displayName: botDisplayName
+ endpoint: uri(botEndpoint, '/api/messages')
+ msaAppId: botAadAppClientId
+ }
+ sku: {
+ name: botServiceSku // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add botServiceSku property to provisionParameters to override the default value "F0".
+ }
+}
+
+// Connect the bot service to Microsoft Teams
+resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = {
+ parent: azureBot
+ location: 'global'
+ name: 'MsTeamsChannel'
+ properties: {
+ channelName: 'MsTeamsChannel'
+ }
+}
\ No newline at end of file
diff --git a/templates/azure/provision/identity.bicep b/templates/azure/provision/identity.bicep
new file mode 100644
index 0000000..da46eb3
--- /dev/null
+++ b/templates/azure/provision/identity.bicep
@@ -0,0 +1,15 @@
+@secure()
+param provisionParameters object
+var resourceBaseName = provisionParameters.resourceBaseName
+var identityName = contains(provisionParameters, 'userAssignedIdentityName') ? provisionParameters['userAssignedIdentityName'] : '${resourceBaseName}' // Try to read name for user assigned identity from parameters
+
+// user assigned identity will be used to access other Azure resources
+resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
+ name: identityName
+ location: resourceGroup().location
+}
+
+output identityName string = identityName
+output identityClientId string = managedIdentity.properties.clientId
+output identityResourceId string = managedIdentity.id
+output identityPrincipalId string = managedIdentity.properties.principalId
\ No newline at end of file
diff --git a/templates/azure/teamsFx/azureWebAppBotConfig.bicep b/templates/azure/teamsFx/azureWebAppBotConfig.bicep
new file mode 100644
index 0000000..6d7b78f
--- /dev/null
+++ b/templates/azure/teamsFx/azureWebAppBotConfig.bicep
@@ -0,0 +1,20 @@
+// Auto generated content, please customize files under provision folder
+
+@secure()
+param provisionParameters object
+param provisionOutputs object
+@secure()
+param currentAppSettings object
+
+var webAppName = split(provisionOutputs.azureWebAppBotOutput.value.resourceId, '/')[8]
+var botAadAppClientId = provisionParameters['botAadAppClientId']
+var botAadAppClientSecret = provisionParameters['botAadAppClientSecret']
+
+resource webAppSettings 'Microsoft.Web/sites/config@2021-02-01' = {
+ name: '${webAppName}/appsettings'
+ properties: union({
+ BOT_ID: botAadAppClientId // ID of your bot
+ BOT_PASSWORD: botAadAppClientSecret // Secret of your bot
+ IDENTITY_ID: provisionOutputs.identityOutput.value.identityClientId // User assigned identity id, the identity is used to access other Azure resources
+ }, currentAppSettings)
+}
\ No newline at end of file