diff --git a/.github/workflows/build-complete-samples.yml b/.github/workflows/build-complete-samples.yml index 9e357bec34..7bf377b65a 100644 --- a/.github/workflows/build-complete-samples.yml +++ b/.github/workflows/build-complete-samples.yml @@ -164,9 +164,9 @@ jobs: name: 'msgext-link-unfurling-reddit' version: '6.0.x' - - project_path: 'samples/app-region-selection/csharp/RegionSectionApp/RegionSelectionApp.csproj' + - project_path: 'samples/app-region-selection/csharp/app-region-selection/app-region-selection.csproj' name: 'app-region-selection' - version: '6.0.x' + version: '10.0.x' - project_path: 'samples/tab-external-auth/csharp/TabExternalAuth/TabExternalAuth.csproj' name: 'tab-external-auth' @@ -328,9 +328,9 @@ jobs: name: 'meetings-live-caption' version: '6.0.x' - - project_path: 'samples/meetings-transcription/csharp/MeetingTranscription/MeetingTranscription.csproj' + - project_path: 'samples/meetings-transcription/csharp/meetings-transcription/meetings-transcription.csproj' name: 'meetings-transcription' - version: '6.0.x' + version: '10.0.x' - project_path: 'samples/meetings-share-to-stage-signing/csharp/Source/MeetingSigning.Domain.Tests/Microsoft.Teams.Samples.MeetingSigning.Domain.Tests.csproj' name: 'meetings-share-to-stage-signing' @@ -1202,10 +1202,6 @@ jobs: name: 'bot-initiate-thread-in-channel' version: '3.10.x' - - project_path: 'samples/graph-pinned-messages/python' - name: 'graph-pinned-messages' - version: '3.10.x' - - project_path: 'samples/msgext-search/python' name: 'msgext-search' version: '3.10.x' @@ -1221,10 +1217,6 @@ jobs: - project_path: 'samples/msgext-link-unfurling/python' name: 'msgext-link-unfurling' version: '3.10.x' - - - project_path: 'samples/msgext-message-reminder/python' - name: 'msgext-message-reminder' - version: '3.10.x' - project_path: 'samples/msgext-search-auth-config/python' name: 'msgext-search-auth-config' @@ -1254,10 +1246,6 @@ jobs: name: 'agent-knowledge-hub' version: '3.13.x' - - project_path: 'samples/bot-all-cards/python' - name: 'bot-all-cards' - version: '3.13.x' - fail-fast: false name: Build all "${{ matrix.name }}" python defaults: diff --git a/samples/app-region-selection/csharp/M365Agent/.gitignore b/samples/app-region-selection/csharp/M365Agent/.gitignore new file mode 100644 index 0000000000..c5cae9258c --- /dev/null +++ b/samples/app-region-selection/csharp/M365Agent/.gitignore @@ -0,0 +1,10 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +appsettings.Development.json +.deployment + +# User-specific files +*.user diff --git a/samples/app-region-selection/csharp/M365Agent/M365Agent.ttkproj b/samples/app-region-selection/csharp/M365Agent/M365Agent.atkproj similarity index 51% rename from samples/app-region-selection/csharp/M365Agent/M365Agent.ttkproj rename to samples/app-region-selection/csharp/M365Agent/M365Agent.atkproj index 3e76a8d49a..124eb75046 100644 --- a/samples/app-region-selection/csharp/M365Agent/M365Agent.ttkproj +++ b/samples/app-region-selection/csharp/M365Agent/M365Agent.atkproj @@ -1,12 +1,8 @@ - + - 1d1ae3f7-cf5e-4793-b1ea-11e95cbce7b2 + b069b3bd-f6bc-cc40-82ab-3fcc2ea50fdf - - - - diff --git a/samples/app-region-selection/csharp/M365Agent/appPackage/color.png b/samples/app-region-selection/csharp/M365Agent/appPackage/color.png index b8cf81afbe..01aa37e347 100644 Binary files a/samples/app-region-selection/csharp/M365Agent/appPackage/color.png and b/samples/app-region-selection/csharp/M365Agent/appPackage/color.png differ diff --git a/samples/app-region-selection/csharp/M365Agent/appPackage/manifest.json b/samples/app-region-selection/csharp/M365Agent/appPackage/manifest.json index 8bca92abe2..239362c3c8 100644 --- a/samples/app-region-selection/csharp/M365Agent/appPackage/manifest.json +++ b/samples/app-region-selection/csharp/M365Agent/appPackage/manifest.json @@ -1,54 +1,54 @@ { - "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.19/MicrosoftTeams.schema.json", - "manifestVersion": "1.19", - "version": "1.0.0", - "id": "${{TEAMS_APP_ID}}", - "developer": { - "name": "Microsoft", - "websiteUrl": "https://www.microsoft.com", - "privacyUrl": "https://www.microsoft.com/privacy", - "termsOfUseUrl": "https://www.microsoft.com/termsofuse" - }, - "icons": { - "color": "color.png", - "outline": "outline.png" - }, - "name": { - "short": "Region Selection App", - "full": "It will help to select Data center's region" - }, - "description": { - "short": "Microsoft Teams app for region selection using Bot and Tab with adaptive cards.", - "full": "This Microsoft Teams app allows users to select and set a region using a Bot and Tab." - }, - "accentColor": "#FFFFFF", - "configurableTabs": [ - { - "configurationUrl": "https://${{BOT_DOMAIN}}/configure", - "canUpdateConfiguration": true, - "scopes": [ - "team", - "groupChat" - ] - } - ], - "bots": [ - { - "botId": "${{AAD_APP_CLIENT_ID}}", - "scopes": [ - "personal", - "team", - "groupChat" - ], - "supportsFiles": false, - "isNotificationOnly": false - } - ], - "permissions": [ - "identity", - "messageTeamMembers" - ], - "validDomains": [ - "${{BOT_DOMAIN}}" - ] + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json", + "manifestVersion": "1.23", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Microsoft", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.microsoft.com/privacy", + "termsOfUseUrl": "https://www.microsoft.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "Region Selection App", + "full": "It will help to select Data center's region" + }, + "description": { + "short": "Microsoft Teams app for region selection using Bot and Tab with adaptive cards.", + "full": "This Microsoft Teams app allows users to select and set a region using a Bot and Tab." + }, + "accentColor": "#FFFFFF", + "configurableTabs": [ + { + "configurationUrl": "https://${{BOT_DOMAIN}}/configure", + "canUpdateConfiguration": true, + "scopes": [ + "team", + "groupChat" + ] + } + ], + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "${{BOT_DOMAIN}}" + ] } \ No newline at end of file diff --git a/samples/app-region-selection/csharp/M365Agent/appPackage/outline.png b/samples/app-region-selection/csharp/M365Agent/appPackage/outline.png index 2c3bf6fa65..f7a4c86447 100644 Binary files a/samples/app-region-selection/csharp/M365Agent/appPackage/outline.png and b/samples/app-region-selection/csharp/M365Agent/appPackage/outline.png differ diff --git a/samples/app-region-selection/csharp/M365Agent/env/.env.dev b/samples/app-region-selection/csharp/M365Agent/env/.env.dev new file mode 100644 index 0000000000..df4f9da508 --- /dev/null +++ b/samples/app-region-selection/csharp/M365Agent/env/.env.dev @@ -0,0 +1,15 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= \ No newline at end of file diff --git a/samples/app-region-selection/csharp/M365Agent/env/.env.local b/samples/app-region-selection/csharp/M365Agent/env/.env.local index b5844ecfe7..86dfacc35a 100644 --- a/samples/app-region-selection/csharp/M365Agent/env/.env.local +++ b/samples/app-region-selection/csharp/M365Agent/env/.env.local @@ -7,19 +7,8 @@ APP_NAME_SUFFIX=local # Generated during provision, you can also add your own variables. BOT_ID= TEAMS_APP_ID= -RESOURCE_SUFFIX= -AZURE_SUBSCRIPTION_ID= -AZURE_RESOURCE_GROUP_NAME= -AAD_APP_CLIENT_ID= -AAD_APP_OBJECT_ID= -AAD_APP_TENANT_ID= -AAD_APP_OAUTH_AUTHORITY= -AAD_APP_OAUTH_AUTHORITY_HOST= TEAMS_APP_TENANT_ID= -MICROSOFT_APP_TYPE= -MICROSOFT_APP_TENANT_ID= - - +BOT_OBJECT_ID= TEAMSFX_M365_USER_NAME= BOT_ENDPOINT= diff --git a/samples/app-region-selection/csharp/M365Agent/infra/azure.bicep b/samples/app-region-selection/csharp/M365Agent/infra/azure.bicep index c3ce051b3d..658e412a21 100644 --- a/samples/app-region-selection/csharp/M365Agent/infra/azure.bicep +++ b/samples/app-region-selection/csharp/M365Agent/infra/azure.bicep @@ -3,42 +3,84 @@ @description('Used to generate names for all resources in this file') param resourceBaseName string -@description('Required when create Azure Bot service') -param botAadAppClientId string - -param botAppDomain string +param webAppSKU string @maxLength(42) param botDisplayName string -param botServiceName string = resourceBaseName -param botServiceSku string = 'F0' -param microsoftAppType string -param microsoftAppTenantId string +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location -// Register your web service as a bot with the Bot Framework -resource botService 'Microsoft.BotService/botServices@2021-03-01' = { - kind: 'azurebot' - location: 'global' - name: botServiceName - properties: { - displayName: botDisplayName - endpoint: 'https://${botAppDomain}/api/messages' - msaAppId: botAadAppClientId - msaAppType: microsoftAppType - msaAppTenantId: microsoftAppType == 'SingleTenant' ? microsoftAppTenantId : '' - } +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + location: location + name: identityName +} + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app' + location: location + name: serverfarmsName sku: { - name: botServiceSku + name: webAppSKU } } -// Connect the bot service to Microsoft Teams -resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { - parent: botService - location: 'global' - name: 'MsTeamsChannel' +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName properties: { - channelName: 'MsTeamsChannel' + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' + } + { + name: 'Teams__ClientId' + value: identity.properties.clientId + } + { + name: 'Teams__TenantId' + value: identity.properties.tenantId + } + { + name: 'Teams__BotType' + value: 'UserAssignedMsi' + } + ] + ftpsState: 'FtpsOnly' + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + identityClientId: identity.properties.clientId + identityResourceId: identity.id + identityTenantId: identity.properties.tenantId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName } } + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName +output BOT_ID string = identity.properties.clientId +output BOT_TENANT_ID string = identity.properties.tenantId diff --git a/samples/app-region-selection/csharp/M365Agent/infra/azure.parameters.json b/samples/app-region-selection/csharp/M365Agent/infra/azure.parameters.json index 0c43dd3659..2274faf691 100644 --- a/samples/app-region-selection/csharp/M365Agent/infra/azure.parameters.json +++ b/samples/app-region-selection/csharp/M365Agent/infra/azure.parameters.json @@ -1,24 +1,15 @@ { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "resourceBaseName": { - "value": "bot${{RESOURCE_SUFFIX}}" - }, - "botAadAppClientId": { - "value": "${{AAD_APP_CLIENT_ID}}" - }, - "botAppDomain": { - "value": "${{BOT_DOMAIN}}" - }, - "botDisplayName": { - "value": "app-region-selection" - }, - "microsoftAppType": { - "value": "${{MICROSOFT_APP_TYPE}}" - }, - "microsoftAppTenantId": { - "value": "${{MICROSOFT_APP_TENANT_ID}}" + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "bot${{RESOURCE_SUFFIX}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "app-region-selection" + } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/samples/app-region-selection/csharp/M365Agent/infra/botRegistration/azurebot.bicep b/samples/app-region-selection/csharp/M365Agent/infra/botRegistration/azurebot.bicep new file mode 100644 index 0000000000..a5a27b8fe4 --- /dev/null +++ b/samples/app-region-selection/csharp/M365Agent/infra/botRegistration/azurebot.bicep @@ -0,0 +1,42 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param identityResourceId string +param identityClientId string +param identityTenantId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: identityClientId + msaAppMSIResourceId: identityResourceId + msaAppTenantId:identityTenantId + msaAppType:'UserAssignedMSI' + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/samples/app-region-selection/csharp/M365Agent/launchSettings.json b/samples/app-region-selection/csharp/M365Agent/launchSettings.json index d6491ef52c..2af8ce7a8a 100644 --- a/samples/app-region-selection/csharp/M365Agent/launchSettings.json +++ b/samples/app-region-selection/csharp/M365Agent/launchSettings.json @@ -1,15 +1,25 @@ { - "profiles": { - // Debug project within Teams - "Microsoft Teams (browser)": { - "commandName": "Project", - "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" - }, - // Launch project within Teams without prepare app dependencies - "Microsoft Teams (browser) (skip update app)": { - "commandName": "Project", - "environmentVariables": { "UPDATE_TEAMS_APP": "false" }, - "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" - } - } + "profiles": { + // Launch project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground (browser)": { + "commandName": "Project", + "environmentVariables": { + "UPDATE_TEAMS_APP": "false", + "M365_AGENTS_PLAYGROUND_TARGET_SDK": "teams-ai-v2-dotnet" + }, + "launchTestTool": true, + "launchUrl": "http://localhost:56150", + }, + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + }, + // Launch project within Teams without prepare app dependencies + "Microsoft Teams (browser) (skip update app)": { + "commandName": "Project", + "environmentVariables": { "UPDATE_TEAMS_APP": "false" }, + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" + }, + } } \ No newline at end of file diff --git a/samples/app-region-selection/csharp/M365Agent/m365agents.local.yml b/samples/app-region-selection/csharp/M365Agent/m365agents.local.yml index 3b298b3a1c..08e8df9b27 100644 --- a/samples/app-region-selection/csharp/M365Agent/m365agents.local.yml +++ b/samples/app-region-selection/csharp/M365Agent/m365agents.local.yml @@ -1,73 +1,67 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.2/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.11/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.2 - -additionalMetadata: - sampleTag: Microsoft-Teams-Samples:app-region-selection-csharp +version: v1.11 provision: - - uses: aadApp/create # Creates a new Azure Active Directory (AAD) app to authenticate users if the environment variable that stores clientId is empty - with: - name: app-region-selection-aad # Note: when you run aadApp/update, the AAD app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in AAD manifest is the same with the name defined here. - generateClientSecret: true # If the value is false, the action will not generate client secret for you - signInAudience: "AzureADMultipleOrgs" # Multitenant - writeToEnvironmentFile: # Write the information of created resources into environment file for the specified environment variable(s). - clientId: AAD_APP_CLIENT_ID - clientSecret: SECRET_AAD_APP_CLIENT_SECRET # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file - objectId: AAD_APP_OBJECT_ID - tenantId: AAD_APP_TENANT_ID - authority: AAD_APP_OAUTH_AUTHORITY - authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST - # Creates a Teams app - uses: teamsApp/create with: # Teams app name - name: app-region-selection-${{TEAMSFX_ENV}} + name: app-region-selection${{APP_NAME_SUFFIX}} # Write the information of created resources into environment file for # the specified environment variable(s). - writeToEnvironmentFile: + writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID - - uses: script + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create with: - run: - # echo "::set-teamsfx-env MICROSOFT_APP_TYPE=SingleTenant"; - echo "::set-teamsfx-env MICROSOFT_APP_TYPE=MultiTenant"; - echo "::set-teamsfx-env MICROSOFT_APP_TENANT_ID=${{AAD_APP_TENANT_ID}}"; + # The Microsoft Entra application's display name + name: app-region-selection${{APP_NAME_SUFFIX}} + generateClientSecret: true + generateServicePrincipal: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile with: - target: ../RegionSectionApp/appsettings.json + target: ../app-region-selection/appsettings.Development.json content: - MicrosoftAppId: ${{AAD_APP_CLIENT_ID}} - MicrosoftAppPassword: ${{SECRET_AAD_APP_CLIENT_SECRET}} + Teams: + ClientId: ${{BOT_ID}} + ClientSecret: ${{SECRET_BOT_PASSWORD}} + TenantId: ${{TEAMS_APP_TENANT_ID}} - - uses: arm/deploy # Deploy given ARM templates parallelly. + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create with: - subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} # The AZURE_SUBSCRIPTION_ID is a built-in environment variable. TeamsFx will ask you select one subscription if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select subscription if it's empty in this case. - resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} # The AZURE_RESOURCE_GROUP_NAME is a built-in environment variable. TeamsFx will ask you to select or create one resource group if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select or create resource grouop if it's empty in this case. - templates: - - path: ./infra/azure.bicep - parameters: ./infra/azure.parameters.json - deploymentName: Create-resources-for-bot - bicepCliVersion: v0.9.1 # Microsoft 365 Agents Toolkit will download this bicep CLI version from github for you, will use bicep CLI in PATH if you remove this config. + botId: ${{BOT_ID}} + name: app-region-selection + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams # Validate using manifest schema - uses: teamsApp/validateManifest with: # Path to manifest template manifestPath: ./appPackage/manifest.json - # Build Teams app package with latest env value - uses: teamsApp/zipAppPackage with: # Path to manifest template manifestPath: ./appPackage/manifest.json outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip - outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + outputFolder: ./appPackage/build # Validate app package using validation rules - uses: teamsApp/validateAppPackage with: @@ -80,4 +74,4 @@ provision: - uses: teamsApp/update with: # Relative path to this file. This is the path for built zip file. - appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip \ No newline at end of file + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip diff --git a/samples/app-region-selection/csharp/M365Agent/m365agents.yml b/samples/app-region-selection/csharp/M365Agent/m365agents.yml index bb6b4d6a73..c8e20d5ef2 100644 --- a/samples/app-region-selection/csharp/M365Agent/m365agents.yml +++ b/samples/app-region-selection/csharp/M365Agent/m365agents.yml @@ -1,9 +1,88 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.2/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.9/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.2 +version: v1.9 -additionalMetadata: - sampleTag: Microsoft-Teams-Samples:app-region-selection-csharp +environmentFolderPath: ./env -environmentFolderPath: ./env \ No newline at end of file +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: app-region-selection${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-bot + # Microsoft 365 Agents Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsapp deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release --runtime win-x86 --self-contained + app-region-selection.csproj + workingDirectory: ../app-region-selection + # Deploy your application to Azure App Service using the zip deploy feature. + # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. + - uses: azureAppService/zipDeploy + with: + # Deploy base folder + artifactFolder: bin/Release/net10.0/win-x86/publish + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} + workingDirectory: ../app-region-selection diff --git a/samples/app-region-selection/csharp/README.md b/samples/app-region-selection/csharp/README.md index 3abefaa362..67187dbbf6 100644 --- a/samples/app-region-selection/csharp/README.md +++ b/samples/app-region-selection/csharp/README.md @@ -27,7 +27,7 @@ This bot has been created using [Bot Framework](https://dev.botframework.com), f * Adaptive Cards ## Interaction with app -![region-selection-bot ](RegionSectionApp/Images/region-selection.gif) +![region-selection-bot ](app-region-selection/Images/region-selection.gif) ## Try it yourself - experience the App in your Microsoft Teams client Please find below demo manifest which is deployed on Microsoft Azure and you can try it yourself by uploading the app package (.zip file link below) to your teams and/or as a personal app. (Uploading must be enabled for your tenant, [see steps here](https://docs.microsoft.com/microsoftteams/platform/concepts/build-and-test/prepare-your-o365-tenant#enable-custom-teams-apps-and-turn-on-custom-app-uploading)). @@ -70,16 +70,16 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool > NOTE: When you create your bot you will create an App ID and App password - make sure you keep these for later. ### 2. Setup NGROK -1) Run ngrok - point to port 3978 +1) Run ngrok - point to port 5130 ```bash - ngrok http 3978 --host-header="localhost:3978" + ngrok http 5130 --host-header="localhost:5130" ``` Alternatively, you can also use the `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: ```bash - devtunnel host -p 3978 --allow-anonymous + devtunnel host -p 5130 --allow-anonymous ``` ### 3. Register your app with Azure AD. @@ -90,13 +90,14 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool * Choose the **supported account types** (any account type will work) * Leave **Redirect URI** empty. * Choose **Register**. - 3. On the overview page, copy and save the **Application (client) ID, Directory (tenant) ID**. You'll need those later when updating your Teams application manifest and in the appsettings.json. + 3. On the overview page, copy and save the **Application (client) ID, Directory (tenant) ID**. You'll need those later when updating your Teams application manifest and in the appsettings.Development.json. 4. Navigate to **API Permissions**, and make sure to add the follow permissions: Select Add a permission * Select Add a permission * Select Microsoft Graph -\> Delegated permissions. * `User.Read` (enabled by default) * Click on Add permissions. Please make sure to grant the admin consent for the required permissions. + * Under **Certificates & secrets**, create a new **Client secret** and save the value. You'll need this for the `ClientSecret` in appsettings.Development.json. ### 4. Setup for code @@ -121,14 +122,14 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool - Launch Visual Studio - File -> Open -> Project/Solution - Navigate to `samples/app-region-selection/RegionSelectionApp` folder - - Select `RegionSelectionApp.sln` file + - Select `RegionSelectionApp.slnx` file - Press `F5` to run the project - - Update the `appsettings.json` configuration for the bot to use the MicrosoftAppId, MicrosoftAppPassword generated in Step 1 (Setup for Bot). (Note the App Password is referred to as the "client secret" in the azure portal and you can always create a new client secret anytime.) + - Update the `appsettings.Development.json` configuration for the bot to use the ClientId, ClientSecret generated in Step 1 (Setup for Bot). (Note the App Password is referred to as the "client secret" in the azure portal and you can always create a new client secret anytime.) ### 5. Setup Manifest for Teams 1) __*This step is specific to Teams.*__ - - **Edit** the `manifest.json` contained in the `appPackage` folder to replace your Microsoft App Id (that was created when you registered your bot earlier) *everywhere* you see the place holder string `<>` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`) + - **Edit** the `manifest.json` contained in the `appPackage` folder to replace your Microsoft App Id (that was created when you registered your bot earlier) *everywhere* you see the place holder string `{{TEAMS_APP_ID}} and {{BOT_ID}}` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`) - **Edit** the `manifest.json` for `configurationUrl` inside `configurableTabs` . Replace `{{domain-name}}` with base Url domain. E.g. if you are using ngrok it would be `https://1234.ngrok-free.app` then your domain-name will be `1234.ngrok-free.app` and if you are using dev tunnels then your domain will be like: `12345.devtunnels.ms`. - **Edit** the `manifest.json` for `validDomains` with base Url domain. E.g. if you are using ngrok it would be `https://1234.ngrok-free.app` then your domain-name will be `1234.ngrok-free.app` and if you are using dev tunnels then your domain will be like: `12345.devtunnels.ms`. - **Zip** up the contents of the `appPackage` folder to create a `manifest.zip` (Make sure that zip file does not contains any subfolder otherwise you will get error while uploading your .zip package) @@ -136,22 +137,22 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool - Add the app to personal/team/groupChat scope (Supported scopes) -**Note**: If you are facing any issue in your app, please uncomment [this](https://github.com/OfficeDev/Microsoft-Teams-Samples/blob/main/samples/app-region-selection/csharp/RegionSectionApp/AdapterWithErrorHandler.cs#L25) line and put your debugger for local debug. +**Note**: If you are facing any issue in your app, please uncomment [this](https://github.com/OfficeDev/Microsoft-Teams-Samples/blob/main/samples/app-region-selection/csharp/app-region-selection/AdapterWithErrorHandler.cs#L25) line and put your debugger for local debug. ## Running the sample Install the Region Selection App manifest in Microsoft Teams. @mention the region selection bot to start the conversation. - Bot sends an Adaptive card in chat -![image](RegionSectionApp/Images/region-details-bot.png) +![image](app-region-selection/Images/region-details-bot.png) - Select the region from the card. Bot sets the selected region and notify user in chat -![image](RegionSectionApp/Images/region-change-bot.png) +![image](app-region-selection/Images/region-change-bot.png) ## Interacting with Region Selection Tab - Set up the region selection app as a Tab in channel -![image](RegionSectionApp/Images/region-config.png) +![image](app-region-selection/Images/region-config.png) - Tab will display the selected region -![image](RegionSectionApp/Images/region-details.png) +![image](app-region-selection/Images/region-details.png) ## Deploy the bot to Azure diff --git a/samples/app-region-selection/csharp/RegionSectionApp/AdapterWithErrorHandler.cs b/samples/app-region-selection/csharp/RegionSectionApp/AdapterWithErrorHandler.cs deleted file mode 100644 index 78994bca01..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/AdapterWithErrorHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Builder.TraceExtensions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Microsoft.BotBuilderSamples -{ - public class AdapterWithErrorHandler : BotFrameworkHttpAdapter - { - public AdapterWithErrorHandler(IConfiguration configuration, ILogger logger) - : base(configuration, logger) - { - OnTurnError = async (turnContext, exception) => - { - // Log any leaked exception from the application. - // NOTE: In production environment, you should consider logging this to - // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how - // to add telemetry capture to your bot. - logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); - - // Uncomment below commented line for local debugging. - // await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}"); - - // Send a trace activity, which will be displayed in the Bot Framework Emulator - await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); - }; - } - } -} diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Bots/RegionSelectionBot.cs b/samples/app-region-selection/csharp/RegionSectionApp/Bots/RegionSelectionBot.cs deleted file mode 100644 index 3bddf6d2ce..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/Bots/RegionSelectionBot.cs +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Bot.Builder; -using Microsoft.Bot.Schema; -using Microsoft.BotBuilderSamples.Models; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.BotBuilderSamples.Bots -{ - /// - /// Handles region selection activities for the bot. - /// - public class RegionSelectionBot : ActivityHandler - { - private readonly BotState _userState; - - public RegionSelectionBot(UserState userState) - { - _userState = userState; - } - - /// - /// Handles incoming message activities. - /// - protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - var welcomeUserStateAccessor = _userState.CreateProperty(nameof(WelcomeUserState)); - var didBotWelcomeUser = await welcomeUserStateAccessor.GetAsync(turnContext, () => new WelcomeUserState(), cancellationToken); - - var text = turnContext.Activity.Text.ToLowerInvariant(); - - if (didBotWelcomeUser.DidUserSelectDomain && (text == "change" || text == "yes")) - { - await SendChangeDomainConfirmationCardAsync(turnContext, cancellationToken); - return; - } - - switch (text) - { - case "reset": - case "change": - case "yes": - await SendDomainListsCardAsync(turnContext, cancellationToken); - break; - - case "no": - case "cancel": - await WelcomeCardAsync(turnContext, cancellationToken); - break; - - default: - await SendWelcomeIntroCardAsync(turnContext, cancellationToken); - break; - } - } - - /// - /// Handles members added activities. - /// - protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) - { - foreach (var member in membersAdded) - { - if (member.Id != turnContext.Activity.Recipient.Id) - { - var welcomeUserStateAccessor = _userState.CreateProperty(nameof(WelcomeUserState)); - var didBotWelcomeUser = await welcomeUserStateAccessor.GetAsync(turnContext, () => new WelcomeUserState(), cancellationToken); - - if (didBotWelcomeUser.DidUserSelectDomain) - { - didBotWelcomeUser.DidUserSelectDomain = false; - didBotWelcomeUser.SelectedRegion = string.Empty; - didBotWelcomeUser.SelectedDomain = string.Empty; - } - - await SendWelcomeIntroCardAsync(turnContext, cancellationToken); - } - } - } - - /// - /// Sends a welcome introduction card. - /// - private async Task SendWelcomeIntroCardAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - string domain; - string region; - - if (turnContext.Activity.Text != null && IsAnyDomainSelected(turnContext.Activity.Text)) - { - await WelcomeCardAsync(turnContext, cancellationToken); // Set state - return; - } - - var welcomeUserStateAccessor = _userState.CreateProperty(nameof(WelcomeUserState)); - var didBotWelcomeUser = await welcomeUserStateAccessor.GetAsync(turnContext, () => new WelcomeUserState(), cancellationToken); - if (didBotWelcomeUser.DidUserSelectDomain) - { - domain = didBotWelcomeUser.SelectedDomain; - region = didBotWelcomeUser.SelectedRegion; - } - else - { - var data = GetDefaultInfo(turnContext); - domain = data.domain; - region = data.region; - } - - string welcomeMsg = $"Your default Region is {region}."; - - var card = new HeroCard - { - Title = "Welcome to Region Selection App!", - Subtitle = "This will help you to choose your data center's region.", - Text = welcomeMsg + " Would you like to change region?", - Buttons = new List - { - new CardAction(ActionTypes.MessageBack, "Yes", null, "Yes", "Yes"), - new CardAction(ActionTypes.MessageBack, "No", null, "No", "No") - } - }; - - var response = MessageFactory.Attachment(card.ToAttachment()); - await turnContext.SendActivityAsync(response, cancellationToken); - } - - /// - /// Gets the default region and domain information. - /// - private (string region, string domain) GetDefaultInfo(ITurnContext turnContext) - { - string serviceUrl = turnContext.Activity.ServiceUrl; - string domain = serviceUrl.Substring(serviceUrl.LastIndexOf(".")).Trim('/'); - string region = turnContext.Activity.Locale; - - return (region, domain); - } - - /// - /// Gets the selected region and domain information based on the provided text. - /// - private (string region, string domain) GetSelectedInfo(string text) - { - string domain = text.Split("-").FirstOrDefault()?.Trim() ?? string.Empty; - - string file = Path.GetFullPath("ConfigData/Regions.json"); - string json = File.ReadAllText(file); - var selectedInfo = JsonSerializer.Deserialize(json).RegionDomains.FirstOrDefault(c => c.Region == domain); - - return selectedInfo != null ? (selectedInfo.Region, selectedInfo.Domain) : (string.Empty, string.Empty); - } - - /// - /// Sends a card with a list of available domains. - /// - private async Task SendDomainListsCardAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - // Get the JSON file path - string file = Path.GetFullPath("ConfigData/Regions.json"); - - // Deserialize JSON from file - string json = System.IO.File.ReadAllText(file); - var rootObject = JsonSerializer.Deserialize(json); - - if (rootObject?.RegionDomains == null || !rootObject.RegionDomains.Any()) - { - await turnContext.SendActivityAsync(MessageFactory.Text("No regions available."), cancellationToken); - return; - } - - var regionButtonList = rootObject.RegionDomains.Select(c => - new CardAction( - ActionTypes.MessageBack, - $"{c.Region} - {c.Country}", - null, - $"{c.Region} - {c.Country}", - $"{c.Region} - {c.Country}", - "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" - )).ToList(); - - var card = new HeroCard - { - Text = "Please select your region,", - Buttons = regionButtonList, - }; - - var response = MessageFactory.Attachment(card.ToAttachment()); - await turnContext.SendActivityAsync(response, cancellationToken); - } - - /// - /// Sends a welcome card with the user's selected region and domain. - /// - private async Task WelcomeCardAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - var welcomeUserStateAccessor = _userState.CreateProperty(nameof(WelcomeUserState)); - var didBotWelcomeUser = await welcomeUserStateAccessor.GetAsync(turnContext, () => new WelcomeUserState(), cancellationToken); - - var userName = turnContext.Activity.From.Name; - var data = GetSelectedInfo(turnContext.Activity.Text); - string domainName = data.domain; - string regionName = data.region; - - if (string.IsNullOrEmpty(domainName) && didBotWelcomeUser.DidUserSelectDomain) - { - domainName = didBotWelcomeUser.SelectedDomain; - regionName = didBotWelcomeUser.SelectedRegion; - } - - if (string.IsNullOrEmpty(domainName)) - { - var defaultData = GetDefaultInfo(turnContext); - domainName = defaultData.domain; - regionName = defaultData.region; - } - - var card = new HeroCard - { - Title = $"Welcome {userName},", - Subtitle = $"You are in {regionName} Region's Data Center", - Text = "If you want to change data center's region, please enter text 'Change'.", - }; - - didBotWelcomeUser.DidUserSelectDomain = true; - didBotWelcomeUser.SelectedDomain = domainName; - didBotWelcomeUser.SelectedRegion = regionName; - - await _userState.SaveChangesAsync(turnContext, cancellationToken: cancellationToken); - - var response = MessageFactory.Attachment(card.ToAttachment()); - await turnContext.SendActivityAsync(response, cancellationToken); - } - - /// - /// Sends a confirmation card to change the domain. - /// - private async Task SendChangeDomainConfirmationCardAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - var userName = turnContext.Activity.From.Name; - var welcomeUserStateAccessor = _userState.CreateProperty(nameof(WelcomeUserState)); - var didBotWelcomeUser = await welcomeUserStateAccessor.GetAsync(turnContext, () => new WelcomeUserState(), cancellationToken); - - var domainButtonList = new List - { - new CardAction(ActionTypes.MessageBack, "Reset", null, "Reset", "Reset", "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0"), - new CardAction(ActionTypes.MessageBack, "Cancel", null, "Cancel", "Cancel", "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0"), - }; - - var card = new HeroCard - { - Text = $"Hi {userName}, You have already selected your data center region and that is {didBotWelcomeUser.SelectedRegion}. Would you like to change this?", - Buttons = domainButtonList, - }; - - var response = MessageFactory.Attachment(card.ToAttachment()); - await turnContext.SendActivityAsync(response, cancellationToken); - } - - /// - /// Checks if any domain is selected based on the provided text. - /// - private bool IsAnyDomainSelected(string text) - { - string domain = text.Split("-").FirstOrDefault()?.Trim() ?? string.Empty; - - if (string.IsNullOrEmpty(domain)) - return false; - - string file = Path.GetFullPath("ConfigData/Regions.json"); - string json = File.ReadAllText(file); - bool isAnyDomainSelected = JsonSerializer.Deserialize(json).RegionDomains.Any(c => c.Region == domain); - - return isAnyDomainSelected; - } - } -} diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Controllers/BotController.cs b/samples/app-region-selection/csharp/RegionSectionApp/Controllers/BotController.cs deleted file mode 100644 index 1225704d56..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/Controllers/BotController.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.BotBuilderSamples.Models; - -namespace Microsoft.BotBuilderSamples.Controllers -{ - // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot - // implementation at runtime. Multiple different IBot implementations running at different endpoints can be - // achieved by specifying a more specific type for the bot constructor argument. - [Route("api/messages")] - [ApiController] - public class BotController : ControllerBase - { - private readonly IBotFrameworkHttpAdapter Adapter; - private readonly IBot Bot; - - public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) - { - Adapter = adapter; - Bot = bot; - } - - [HttpPost, HttpGet] - public async Task PostAsync() - { - // Delegate the processing of the HTTP POST to the adapter. - // The adapter will invoke the bot. - await Adapter.ProcessAsync(Request, Response, Bot); - } - } -} diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Program.cs b/samples/app-region-selection/csharp/RegionSectionApp/Program.cs deleted file mode 100644 index e9bba41e18..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace Microsoft.BotBuilderSamples -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.ConfigureLogging((logging) => - { - logging.AddDebug(); - logging.AddConsole(); - }); - webBuilder.UseStartup(); - }); - } -} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Properties/ServiceDependencies/RegionSelectionAppTestHarikrishnanSai - Zip Deploy/profile.arm.json b/samples/app-region-selection/csharp/RegionSectionApp/Properties/ServiceDependencies/RegionSelectionAppTestHarikrishnanSai - Zip Deploy/profile.arm.json deleted file mode 100644 index 45cf147502..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/Properties/ServiceDependencies/RegionSelectionAppTestHarikrishnanSai - Zip Deploy/profile.arm.json +++ /dev/null @@ -1,174 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_dependencyType": "compute.function.windows.appService" - }, - "parameters": { - "resourceGroupName": { - "type": "string", - "defaultValue": "washaikDevCommTesting", - "metadata": { - "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." - } - }, - "resourceGroupLocation": { - "type": "string", - "defaultValue": "eastus", - "metadata": { - "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." - } - }, - "resourceName": { - "type": "string", - "defaultValue": "RegionSelectionAppTestHarikrishnanSai", - "metadata": { - "description": "Name of the main resource to be created by this template." - } - }, - "resourceLocation": { - "type": "string", - "defaultValue": "[parameters('resourceGroupLocation')]", - "metadata": { - "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." - } - } - }, - "resources": [ - { - "type": "Microsoft.Resources/resourceGroups", - "name": "[parameters('resourceGroupName')]", - "location": "[parameters('resourceGroupLocation')]", - "apiVersion": "2019-10-01" - }, - { - "type": "Microsoft.Resources/deployments", - "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", - "resourceGroup": "[parameters('resourceGroupName')]", - "apiVersion": "2019-10-01", - "dependsOn": [ - "[parameters('resourceGroupName')]" - ], - "properties": { - "mode": "Incremental", - "expressionEvaluationOptions": { - "scope": "inner" - }, - "parameters": { - "resourceGroupName": { - "value": "[parameters('resourceGroupName')]" - }, - "resourceGroupLocation": { - "value": "[parameters('resourceGroupLocation')]" - }, - "resourceName": { - "value": "[parameters('resourceName')]" - }, - "resourceLocation": { - "value": "[parameters('resourceLocation')]" - } - }, - "template": { - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "resourceGroupName": { - "type": "string" - }, - "resourceGroupLocation": { - "type": "string" - }, - "resourceName": { - "type": "string" - }, - "resourceLocation": { - "type": "string" - } - }, - "variables": { - "storage_name": "[toLower(concat('storage', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId))))]", - "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", - "storage_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Storage/storageAccounts/', variables('storage_name'))]", - "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]", - "function_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/sites/', parameters('resourceName'))]" - }, - "resources": [ - { - "location": "[parameters('resourceLocation')]", - "name": "[parameters('resourceName')]", - "type": "Microsoft.Web/sites", - "apiVersion": "2015-08-01", - "tags": { - "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" - }, - "dependsOn": [ - "[variables('appServicePlan_ResourceId')]", - "[variables('storage_ResourceId')]" - ], - "kind": "functionapp", - "properties": { - "name": "[parameters('resourceName')]", - "kind": "functionapp", - "httpsOnly": true, - "reserved": false, - "serverFarmId": "[variables('appServicePlan_ResourceId')]", - "siteConfig": { - "alwaysOn": true - } - }, - "identity": { - "type": "SystemAssigned" - }, - "resources": [ - { - "name": "appsettings", - "type": "config", - "apiVersion": "2015-08-01", - "dependsOn": [ - "[variables('function_ResourceId')]" - ], - "properties": { - "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]", - "FUNCTIONS_EXTENSION_VERSION": "~3", - "FUNCTIONS_WORKER_RUNTIME": "dotnet" - } - } - ] - }, - { - "location": "[parameters('resourceGroupLocation')]", - "name": "[variables('storage_name')]", - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2017-10-01", - "tags": { - "[concat('hidden-related:', concat('/providers/Microsoft.Web/sites/', parameters('resourceName')))]": "empty" - }, - "properties": { - "supportsHttpsTrafficOnly": true - }, - "sku": { - "name": "Standard_LRS" - }, - "kind": "Storage" - }, - { - "location": "[parameters('resourceGroupLocation')]", - "name": "[variables('appServicePlan_name')]", - "type": "Microsoft.Web/serverFarms", - "apiVersion": "2015-08-01", - "sku": { - "name": "S1", - "tier": "Standard", - "family": "S", - "size": "S1" - }, - "properties": { - "name": "[variables('appServicePlan_name')]" - } - } - ] - } - } - } - ] -} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Properties/launchSettings.json b/samples/app-region-selection/csharp/RegionSectionApp/Properties/launchSettings.json deleted file mode 100644 index ff9d8fe153..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/Properties/launchSettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "profiles": { - "Start Project": { - "commandName": "Project", - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:7130;http://localhost:5130", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "hotReloadProfile": "aspnetcore" - } - } -} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/RegionSectionApp/RegionSelectionApp.csproj b/samples/app-region-selection/csharp/RegionSectionApp/RegionSelectionApp.csproj deleted file mode 100644 index 4e43cfd789..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/RegionSelectionApp.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - latest - - - - - - - - - - - Always - - - - diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Startup.cs b/samples/app-region-selection/csharp/RegionSectionApp/Startup.cs deleted file mode 100644 index 0062fd49fe..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/Startup.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -using Microsoft.BotBuilderSamples.Bots; -using Microsoft.Extensions.Hosting; - -namespace Microsoft.BotBuilderSamples -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - //services.AddControllers().AddNewtonsoftJson(); - services.AddControllersWithViews().AddNewtonsoftJson(); - - // Create the Bot Framework Adapter with error handling enabled. - services.AddSingleton(); - - // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) - services.AddSingleton(); - - // Create the User state. - services.AddSingleton(); - - // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. - services.AddTransient(); - - services.AddMvcCore(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseDefaultFiles() - .UseStaticFiles() - .UseWebSockets() - .UseRouting() - .UseAuthorization() - .UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - - // app.UseHttpsRedirection(); - } - } -} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/RegionSectionApp/appsettings.Development.json b/samples/app-region-selection/csharp/RegionSectionApp/appsettings.Development.json deleted file mode 100644 index e203e9407e..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/samples/app-region-selection/csharp/RegionSectionApp/appsettings.json b/samples/app-region-selection/csharp/RegionSectionApp/appsettings.json deleted file mode 100644 index 97dff81a1b..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/appsettings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "MicrosoftAppId": "", - "MicrosoftAppPassword": "" -} diff --git a/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Content/Site.css b/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Content/Site.css deleted file mode 100644 index 6c4c11c6b4..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Content/Site.css +++ /dev/null @@ -1,8 +0,0 @@ -html, body, div.surface, div.panel { - height: 100%; - margin: 0; -} - -div.panel { - padding: 15px; -} diff --git a/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Content/msteams-16.css b/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Content/msteams-16.css deleted file mode 100644 index 54f033cae0..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Content/msteams-16.css +++ /dev/null @@ -1,1272 +0,0 @@ -.theme-light .surface { - background-color: #F0F2F4; - color: #16233A; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem -} - -.theme-light .panel { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background-color: #FFFFFF; - border-color: transparent; - border-radius: 0.1875rem; - border-style: solid; - border-width: 0.125rem; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow: hidden -} - -.theme-light .panel-header { - flex: 0 0 auto; - margin-left: 2rem; - margin-right: 2rem; - margin-top: 2rem -} - -.theme-light .panel-body { - flex: 1 1 auto; - margin-left: 2rem; - margin-right: 2rem; - overflow: auto -} - -.theme-light .panel-footer { - flex: 0 0 auto; - margin-bottom: 2rem; - margin-left: 2rem; - margin-right: 2rem -} - -.theme-light .button-primary { - background: #5558AF; - border: 0.125rem solid; - border-color: transparent; - border-radius: 0.1875rem; - color: #FFFFFF; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-light .button-primary:hover:enabled { - background: #4C509D; - border-color: transparent; - color: #FFFFFF - } - - .theme-light .button-primary:active { - background: #454A92; - border-color: transparent; - color: #FFFFFF - } - - .theme-light .button-primary:disabled { - background: #F3F4F5; - border-color: transparent; - color: #ABB0B8 - } - - .theme-light .button-primary:focus { - background: #4C509D; - border-color: transparent; - color: #FFFFFF; - outline: 0.125rem solid #FFFFFF; - outline-offset: -0.25rem - } - -.theme-light .button-secondary { - background: #FFFFFF; - border: 0.125rem solid; - border-color: #ABB0B8; - border-radius: 0.1875rem; - color: #525C6D; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-light .button-secondary:hover:enabled { - background: #ABB0B8; - border-color: transparent; - color: #16233A - } - - .theme-light .button-secondary:active { - background: #858C98; - border-color: transparent; - color: #16233A - } - - .theme-light .button-secondary:disabled { - background: #FFFFFF; - border-color: #F3F4F5; - color: #ABB0B8 - } - - .theme-light .button-secondary:focus { - background: #ABB0B8; - border-color: transparent; - color: #16233A; - outline: 0.125rem solid #16233A; - outline-offset: -0.25rem - } - -.theme-light .radio-container { - align-items: center; - background: transparent; - border: none; - display: flex; - outline: none -} - - .theme-light .radio-container + .radio-container { - margin-top: 0.5rem - } - -.theme-light .radio-button { - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - background: transparent; - border: 0.0625rem solid; - border-color: #525C6D; - border-radius: 100%; - cursor: pointer; - display: inline-block; - font: inherit; - height: 0.75rem; - margin: 0.125rem; - margin-left: 0.375rem; - padding: 0; - position: relative; - width: 0.75rem -} - - .theme-light .radio-button:hover { - background: transparent; - border-color: #525C6D - } - - .theme-light .radio-button:disabled { - background: #F0F2F4; - border-color: #ABB0B8 - } - - .theme-light .radio-button:disabled + label { - color: #ABB0B8; - cursor: default - } - - .theme-light .radio-button:focus { - box-shadow: 0 0 0 0.125rem #9FA4FE; - outline: none - } - -.theme-light .hidden-input:checked + .radio-button { - background: #5558AF; - border-color: #5558AF -} - - .theme-light .hidden-input:checked + .radio-button + label { - color: #16233A - } - -.theme-light .radio-label { - color: #525C6D; - cursor: pointer; - font-size: 0.75rem; - line-height: 1rem; - margin-left: 0.625rem -} - -.theme-light .radio-group { - display: inline-block -} - -.theme-light .tab-group { - border-bottom: 0.0625rem solid #F3F4F5; - margin: 0; - padding: 0; - width: 100% -} - - .theme-light .tab-group .tab { - background: 0; - border: 0; - border-bottom: transparent 0.25rem solid; - color: #525C6D; - cursor: pointer; - display: inline-block; - font: inherit; - margin: 0; - margin-right: 1.25rem; - outline: none; - padding: 0.25rem - } - - .theme-light .tab-group .tab:hover { - border-bottom-color: #9496CA - } - - .theme-light .tab-group .tab:focus { - background-color: #9FA4FE; - color: #FFFFFF - } - - .theme-light .tab-group .tab-active { - border-bottom-color: #5558AF; - color: #5558AF - } - -.theme-light .tab-active:focus { - border-bottom-color: #FFFFFF -} - -.theme-light .hidden-input { - display: none -} - -.theme-light .toggle { - display: inline-block; - line-height: 1 -} - -.theme-light .toggle-ball { - background-color: #F0F2F4; - border: 0; - border-radius: 1.25rem; - cursor: pointer; - height: 1.25rem; - margin: 0.125rem; - outline: none; - padding: 0; - position: relative; - width: 3.75rem -} - - .theme-light .toggle-ball:before { - background-color: #454A92; - border-radius: 50%; - content: ""; - height: 0.875rem; - left: 0.1875rem; - position: absolute; - top: 0.18750000000000003rem; - transition: 0.2s; - width: 0.875rem - } - -.theme-light .hidden-input:checked + .toggle-ball:before { - background-color: #4C509D; - transform: translateX(2.5rem) -} - -.theme-light .toggle-ball:focus { - box-shadow: 0 0 0 0.125rem #5558AF; - outline: none -} - -.theme-light .hidden-input:checked + .toggle-ball { - background-color: #7FBA00 -} - -.theme-light .font-title { - font-size: 1.5rem; - line-height: 2rem -} - -.theme-light .font-title2 { - font-size: 1.125rem; - line-height: 1.5rem -} - -.theme-light .font-base { - font-size: 0.875rem; - line-height: 1.25rem -} - -.theme-light .font-caption { - font-size: 0.75rem; - line-height: 1rem -} - -.theme-light .font-xsmall { - font-size: 0.625rem; - line-height: 0.6875rem -} - -.theme-light .font-semilight { - font-family: 'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 300 -} - -.theme-light .font-regular { - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 400 -} - -.theme-light .font-semibold { - font-family: 'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 600 -} - -.theme-light .font-bold { - font-family: 'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 700 -} - -.theme-light .input-container { - overflow: hidden; - position: relative -} - -.theme-light .input-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #F0F2F4; - border: 0.125rem solid transparent; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #525C6D; - font: inherit; - height: 2rem; - margin: 0; - outline: none; - padding: 0.5rem 0.75rem; - width: 100% -} - -.theme-light .input-error-icon { - bottom: 0.5625rem; - color: #C50E2E; - position: absolute; - right: 0.75rem -} - -.theme-light .label { - border: 0; - color: #4E586A; - display: inline-block; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-light .error-label { - border: 0; - color: #C50E2E; - float: right; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-light .textarea-container { - display: flex; - flex-direction: column; - overflow: hidden; - position: relative -} - -.theme-light .textarea-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #F0F2F4; - border: 0.125rem solid transparent; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #525C6D; - flex: 1; - font: inherit; - margin: 0; - min-height: 3.75rem; - outline: none; - padding: 0.5rem 0.75rem; - resize: none -} - - .theme-light .input-field:hover:inactive:enabled, .theme-light .textarea-field:hover:inactive:enabled { - background: #F0F2F4; - border-bottom-color: transparent - } - - .theme-light .input-field:disabled, .theme-light .textarea-field:disabled { - background: #F3F4F5; - border-bottom-color: transparent; - color: #DEE0E3 - } - - .theme-light .input-field:active:enabled, .theme-light .input-field:focus, .theme-light .textarea-field:active:enabled, .theme-light .textarea-field:focus { - background: #F0F2F4; - border-bottom-color: #5558AF - } - -.theme-light .textarea-error-icon { - color: #C50E2E; - position: absolute; - right: 0.75rem; - top: 50% -} - -.theme-dark .surface { - background-color: #2B2B30; - color: #FFFFFF; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem -} - -.theme-dark .panel { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background-color: #404045; - border-color: transparent; - border-radius: 0.1875rem; - border-style: solid; - border-width: 0.125rem; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow: hidden -} - -.theme-dark .panel-header { - flex: 0 0 auto; - margin-left: 2rem; - margin-right: 2rem; - margin-top: 2rem -} - -.theme-dark .panel-body { - flex: 1 1 auto; - margin-left: 2rem; - margin-right: 2rem; - overflow: auto -} - -.theme-dark .panel-footer { - flex: 0 0 auto; - margin-bottom: 2rem; - margin-left: 2rem; - margin-right: 2rem -} - -.theme-dark .button-primary { - background: #9FA4FE; - border: 0.125rem solid; - border-color: transparent; - border-radius: 0.1875rem; - color: #2B2B30; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-dark .button-primary:hover:enabled { - background: #AEB2FF; - border-color: transparent; - color: #2B2B30 - } - - .theme-dark .button-primary:active { - background: #B8BBFF; - border-color: transparent; - color: #2B2B30 - } - - .theme-dark .button-primary:disabled { - background: #35353A; - border-color: transparent; - color: #77777A - } - - .theme-dark .button-primary:focus { - background: #9FA4FE; - border-color: transparent; - color: #2B2B30; - outline: 0.125rem solid #2B2B30; - outline-offset: -0.25rem - } - -.theme-dark .button-secondary { - background: #404045; - border: 0.125rem solid; - border-color: #77777A; - border-radius: 0.1875rem; - color: #C8C8C9; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-dark .button-secondary:hover:enabled { - background: #77777A; - border-color: transparent; - color: #FFFFFF - } - - .theme-dark .button-secondary:active { - background: #48484D; - border-color: transparent; - color: #FFFFFF - } - - .theme-dark .button-secondary:disabled { - background: #404045; - border-color: #35353A; - color: #77777A - } - - .theme-dark .button-secondary:focus { - background: #77777A; - border-color: transparent; - color: #FFFFFF; - outline: 0.125rem solid #FFFFFF; - outline-offset: -0.25rem - } - -.theme-dark .radio-container { - align-items: center; - background: transparent; - border: none; - display: flex; - outline: none -} - - .theme-dark .radio-container + .radio-container { - margin-top: 0.5rem - } - -.theme-dark .radio-button { - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - background: transparent; - border: 0.0625rem solid; - border-color: #C8C8C9; - border-radius: 100%; - cursor: pointer; - display: inline-block; - font: inherit; - height: 0.75rem; - margin: 0.125rem; - margin-left: 0.375rem; - padding: 0; - position: relative; - width: 0.75rem -} - - .theme-dark .radio-button:hover { - background: transparent; - border-color: #C8C8C9 - } - - .theme-dark .radio-button:disabled { - background: #404045; - border-color: #77777A - } - - .theme-dark .radio-button:disabled + label { - color: #77777A; - cursor: default - } - - .theme-dark .radio-button:focus { - box-shadow: 0 0 0 0.125rem #5558AF; - outline: none - } - -.theme-dark .hidden-input:checked + .radio-button { - background: #9FA4FE; - border-color: #9FA4FE -} - - .theme-dark .hidden-input:checked + .radio-button + label { - color: #FFFFFF - } - -.theme-dark .radio-label { - color: #C8C8C9; - cursor: pointer; - font-size: 0.75rem; - line-height: 1rem; - margin-left: 0.625rem -} - -.theme-dark .radio-group { - display: inline-block -} - -.theme-dark .tab-group { - border-bottom: 0.0625rem solid #000000; - margin: 0; - padding: 0; - width: 100% -} - - .theme-dark .tab-group .tab { - background: 0; - border: 0; - border-bottom: transparent 0.25rem solid; - color: #C8C8C9; - cursor: pointer; - display: inline-block; - font: inherit; - margin: 0; - margin-right: 1.25rem; - outline: none; - padding: 0.25rem - } - - .theme-dark .tab-group .tab:hover { - border-bottom-color: #7174AA - } - - .theme-dark .tab-group .tab:focus { - background-color: #5558AF; - color: #FFFFFF - } - - .theme-dark .tab-group .tab-active { - border-bottom-color: #9FA4FE; - color: #9FA4FE - } - -.theme-dark .tab-active:focus { - border-bottom-color: #FFFFFF -} - -.theme-dark .hidden-input { - display: none -} - -.theme-dark .toggle { - display: inline-block; - line-height: 1 -} - -.theme-dark .toggle-ball { - background-color: #2B2B30; - border: 0; - border-radius: 1.25rem; - cursor: pointer; - height: 1.25rem; - margin: 0.125rem; - outline: none; - padding: 0; - position: relative; - width: 3.75rem -} - - .theme-dark .toggle-ball:before { - background-color: #C8C8C9; - border-radius: 50%; - content: ""; - height: 0.875rem; - left: 0.1875rem; - position: absolute; - top: 0.18750000000000003rem; - transition: 0.2s; - width: 0.875rem - } - -.theme-dark .hidden-input:checked + .toggle-ball:before { - background-color: #FFFFFF; - transform: translateX(2.5rem) -} - -.theme-dark .toggle-ball:focus { - box-shadow: 0 0 0 0.125rem #9FA4FE; - outline: none -} - -.theme-dark .hidden-input:checked + .toggle-ball { - background-color: #88BC2B -} - -.theme-dark .font-title { - font-size: 1.5rem; - line-height: 2rem -} - -.theme-dark .font-title2 { - font-size: 1.125rem; - line-height: 1.5rem -} - -.theme-dark .font-base { - font-size: 0.875rem; - line-height: 1.25rem -} - -.theme-dark .font-caption { - font-size: 0.75rem; - line-height: 1rem -} - -.theme-dark .font-xsmall { - font-size: 0.625rem; - line-height: 0.6875rem -} - -.theme-dark .font-semilight { - font-family: 'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 300 -} - -.theme-dark .font-regular { - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 400 -} - -.theme-dark .font-semibold { - font-family: 'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 600 -} - -.theme-dark .font-bold { - font-family: 'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 700 -} - -.theme-dark .input-container { - overflow: hidden; - position: relative -} - -.theme-dark .input-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #2B2B30; - border: 0.125rem solid transparent; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #C8C8C9; - font: inherit; - height: 2rem; - margin: 0; - outline: none; - padding: 0.5rem 0.75rem; - width: 100% -} - -.theme-dark .input-error-icon { - bottom: 0.5625rem; - color: #ED1B3E; - position: absolute; - right: 0.75rem -} - -.theme-dark .label { - border: 0; - color: #FFFFFF; - display: inline-block; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-dark .error-label { - border: 0; - color: #ED1B3E; - float: right; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-dark .textarea-container { - display: flex; - flex-direction: column; - overflow: hidden; - position: relative -} - -.theme-dark .textarea-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #2B2B30; - border: 0.125rem solid transparent; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #C8C8C9; - flex: 1; - font: inherit; - margin: 0; - min-height: 3.75rem; - outline: none; - padding: 0.5rem 0.75rem; - resize: none -} - - .theme-dark .input-field:hover:inactive:enabled, .theme-dark .textarea-field:hover:inactive:enabled { - background: #2B2B30; - border-bottom-color: transparent - } - - .theme-dark .input-field:disabled, .theme-dark .textarea-field:disabled { - background: #35353A; - border-bottom-color: transparent; - color: #48484D - } - - .theme-dark .input-field:active:enabled, .theme-dark .input-field:focus, .theme-dark .textarea-field:active:enabled, .theme-dark .textarea-field:focus { - background: #2B2B30; - border-bottom-color: #9FA4FE - } - -.theme-dark .textarea-error-icon { - color: #ED1B3E; - position: absolute; - right: 0.75rem; - top: 50% -} - -.theme-contrast .surface { - background-color: #000000; - color: #FFFFFF; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem -} - -.theme-contrast .panel { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background-color: #000000; - border-color: #FFFFFF; - border-radius: 0.1875rem; - border-style: solid; - border-width: 0.125rem; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow: hidden -} - -.theme-contrast .panel-header { - flex: 0 0 auto; - margin-left: 2rem; - margin-right: 2rem; - margin-top: 2rem -} - -.theme-contrast .panel-body { - flex: 1 1 auto; - margin-left: 2rem; - margin-right: 2rem; - overflow: auto -} - -.theme-contrast .panel-footer { - flex: 0 0 auto; - margin-bottom: 2rem; - margin-left: 2rem; - margin-right: 2rem -} - -.theme-contrast .button-primary { - background: #FFFFFF; - border: 0.125rem solid; - border-color: transparent; - border-radius: 0.1875rem; - color: #000000; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-contrast .button-primary:disabled { - background: #30F42C; - border-color: transparent; - color: #000000 - } - -.theme-contrast .button-secondary { - background: #000000; - border: 0.125rem solid; - border-color: #FFFFFF; - border-radius: 0.1875rem; - color: #FFFFFF; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-contrast .button-primary:hover:enabled, .theme-contrast .button-primary:active, .theme-contrast .button-secondary:hover:enabled, .theme-contrast .button-secondary:active { - background: #FFFF00; - border-color: transparent; - color: #000000 - } - - .theme-contrast .button-secondary:disabled { - background: #000000; - border-color: #30F42C; - color: #30F42C - } - - .theme-contrast .button-primary:focus, .theme-contrast .button-secondary:focus { - background: #FFFF00; - border-color: transparent; - color: #000000; - outline: 0.125rem solid transparent; - outline-offset: -0.25rem - } - -.theme-contrast .radio-container { - align-items: center; - background: transparent; - border: none; - display: flex; - outline: none -} - - .theme-contrast .radio-container + .radio-container { - margin-top: 0.5rem - } - -.theme-contrast .radio-button { - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - background: transparent; - border: 0.0625rem solid; - border-color: #FFFFFF; - border-radius: 100%; - cursor: pointer; - display: inline-block; - font: inherit; - height: 0.75rem; - margin: 0.125rem; - margin-left: 0.375rem; - padding: 0; - position: relative; - width: 0.75rem -} - - .theme-contrast .radio-button:hover { - background: transparent; - border-color: #FFFFFF - } - - .theme-contrast .radio-button:disabled { - background: transparent; - border-color: #30F42C - } - - .theme-contrast .radio-button:disabled + label { - color: #30F42C; - cursor: default - } - - .theme-contrast .radio-button:focus { - box-shadow: 0 0 0 0.125rem #FFFF00; - outline: none - } - -.theme-contrast .hidden-input:checked + .radio-button { - background: #00EBFF; - border-color: #00EBFF -} - - .theme-contrast .hidden-input:checked + .radio-button + label { - color: #FFFFFF - } - -.theme-contrast .radio-label { - color: #FFFFFF; - cursor: pointer; - font-size: 0.75rem; - line-height: 1rem; - margin-left: 0.625rem -} - -.theme-contrast .radio-group { - display: inline-block -} - -.theme-contrast .tab-group { - border-bottom: 0.0625rem solid #30F42C; - margin: 0; - padding: 0; - width: 100% -} - - .theme-contrast .tab-group .tab { - background: 0; - border: 0; - border-bottom: transparent 0.25rem solid; - color: #FFFFFF; - cursor: pointer; - display: inline-block; - font: inherit; - margin: 0; - margin-right: 1.25rem; - outline: none; - padding: 0.25rem - } - - .theme-contrast .tab-group .tab:hover { - border-bottom-color: #FFFF00 - } - - .theme-contrast .tab-group .tab:focus { - background-color: #FFFF00; - color: #000000 - } - - .theme-contrast .tab-group .tab-active { - border-bottom-color: #00EBFF; - color: #FFFFFF - } - -.theme-contrast .tab-active:focus { - border-bottom-color: #000000 -} - -.theme-contrast .hidden-input { - display: none -} - -.theme-contrast .toggle { - display: inline-block; - line-height: 1 -} - -.theme-contrast .toggle-ball { - background-color: #FFFFFF; - border: 0; - border-radius: 1.25rem; - cursor: pointer; - height: 1.25rem; - margin: 0.125rem; - outline: none; - padding: 0; - position: relative; - width: 3.75rem -} - - .theme-contrast .toggle-ball:before { - background-color: #FFFF00; - border-radius: 50%; - content: ""; - height: 0.875rem; - left: 0.1875rem; - position: absolute; - top: 0.18750000000000003rem; - transition: 0.2s; - width: 0.875rem - } - -.theme-contrast .hidden-input:checked + .toggle-ball:before { - background-color: #4C509D; - transform: translateX(2.5rem) -} - -.theme-contrast .toggle-ball:focus { - box-shadow: 0 0 0 0.125rem #30F42C; - outline: none -} - -.theme-contrast .hidden-input:checked + .toggle-ball { - background-color: #7FBA00 -} - -.theme-contrast .font-title { - font-size: 1.5rem; - line-height: 2rem -} - -.theme-contrast .font-title2 { - font-size: 1.125rem; - line-height: 1.5rem -} - -.theme-contrast .font-base { - font-size: 0.875rem; - line-height: 1.25rem -} - -.theme-contrast .font-caption { - font-size: 0.75rem; - line-height: 1rem -} - -.theme-contrast .font-xsmall { - font-size: 0.625rem; - line-height: 0.6875rem -} - -.theme-contrast .font-semilight { - font-family: 'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 300 -} - -.theme-contrast .font-regular { - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 400 -} - -.theme-contrast .font-semibold { - font-family: 'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 600 -} - -.theme-contrast .font-bold { - font-family: 'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 700 -} - -.theme-contrast .input-container { - overflow: hidden; - position: relative -} - -.theme-contrast .input-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #000000; - border: 0.125rem solid #FFFFFF; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #FFFFFF; - font: inherit; - height: 2rem; - margin: 0; - outline: none; - padding: 0.5rem 0.75rem; - width: 100% -} - -.theme-contrast .input-error-icon { - bottom: 0.5625rem; - color: #FFFF00; - position: absolute; - right: 0.75rem -} - -.theme-contrast .label { - border: 0; - color: #FFFFFF; - display: inline-block; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-contrast .error-label { - border: 0; - color: #FFFF00; - float: right; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-contrast .textarea-container { - display: flex; - flex-direction: column; - overflow: hidden; - position: relative -} - -.theme-contrast .textarea-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #000000; - border: 0.125rem solid #FFFFFF; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #FFFFFF; - flex: 1; - font: inherit; - margin: 0; - min-height: 3.75rem; - outline: none; - padding: 0.5rem 0.75rem; - resize: none -} - - .theme-contrast .input-field:hover:inactive:enabled, .theme-contrast .textarea-field:hover:inactive:enabled { - background: #000000; - border-bottom-color: transparent - } - - .theme-contrast .input-field:disabled, .theme-contrast .textarea-field:disabled { - background: #30F42C; - border-bottom-color: #FFFFFF; - color: #FFFFFF - } - - .theme-contrast .input-field:active:enabled, .theme-contrast .input-field:focus, .theme-contrast .textarea-field:active:enabled, .theme-contrast .textarea-field:focus { - background: #000000; - border-bottom-color: #FFFF00 - } - -.theme-contrast .textarea-error-icon { - color: #FFFF00; - position: absolute; - right: 0.75rem; - top: 50% -} diff --git a/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/default.html b/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/default.html deleted file mode 100644 index 4b6f86baca..0000000000 --- a/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/default.html +++ /dev/null @@ -1,418 +0,0 @@ - - - - - - - Data center's Region Selection - - - - - -
-
-
-
Data center's Region Selection
-
-
-
-
-
Your bot is ready!
-
You can test your bot in the Bot Framework Emulator
- by connecting to http://localhost:3978/api/messages.
- -
Visit Azure - Bot Service to register your bot and add it to
- various channels. The bot's endpoint URL typically looks - like this:
-
https://your_bots_hostname/api/messages
-
-
-
-
- -
- - - diff --git a/samples/app-region-selection/csharp/RegionSelectionApp.sln b/samples/app-region-selection/csharp/RegionSelectionApp.sln deleted file mode 100644 index 957dc0a314..0000000000 --- a/samples/app-region-selection/csharp/RegionSelectionApp.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.34814.14 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegionSelectionApp", "RegionSectionApp\RegionSelectionApp.csproj", "{B18CD866-7E29-498F-88EA-C3B48C96CA7C}" -EndProject -Project("{A9E3F50B-275E-4AF7-ADCE-8BE12D41E305}") = "M365Agent", "M365Agent\M365Agent.ttkproj", "{1D1AE3F7-CF5E-4793-B1EA-11E95CBCE7B2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0E3CB11F-BCCF-45F1-947C-FD9F56541E01}" - ProjectSection(SolutionItems) = preProject - RegionSelectionApp.slnLaunch.user = RegionSelectionApp.slnLaunch.user - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B18CD866-7E29-498F-88EA-C3B48C96CA7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B18CD866-7E29-498F-88EA-C3B48C96CA7C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B18CD866-7E29-498F-88EA-C3B48C96CA7C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B18CD866-7E29-498F-88EA-C3B48C96CA7C}.Release|Any CPU.Build.0 = Release|Any CPU - {1D1AE3F7-CF5E-4793-B1EA-11E95CBCE7B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1D1AE3F7-CF5E-4793-B1EA-11E95CBCE7B2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1D1AE3F7-CF5E-4793-B1EA-11E95CBCE7B2}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {1D1AE3F7-CF5E-4793-B1EA-11E95CBCE7B2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1D1AE3F7-CF5E-4793-B1EA-11E95CBCE7B2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7075E508-599E-421A-A431-696A3E92B2F9} - EndGlobalSection -EndGlobal diff --git a/samples/app-region-selection/csharp/RegionSelectionApp.slnLaunch.user b/samples/app-region-selection/csharp/RegionSelectionApp.slnLaunch.user deleted file mode 100644 index 12693ffcbe..0000000000 --- a/samples/app-region-selection/csharp/RegionSelectionApp.slnLaunch.user +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "Name": "Microsoft Teams (browser)", - "Projects": [ - { - "Path": "RegionSectionApp\\RegionSelectionApp.csproj", - "Action": "Start", - "DebugTarget": "Start Project" - }, - { - "Path": "M365Agent\\M365Agent.ttkproj", - "Action": "StartWithoutDebugging", - "DebugTarget": "Microsoft Teams (browser)" - } - ] - }, - { - "Name": "Microsoft Teams (browser) (skip update app)", - "Projects": [ - { - "Path": "RegionSectionApp\\RegionSelectionApp.csproj", - "Action": "Start", - "DebugTarget": "Start Project" - }, - { - "Path": "M365Agent\\M365Agent.ttkproj", - "Action": "StartWithoutDebugging", - "DebugTarget": "Microsoft Teams (browser) (skip update app)" - } - ] - } -] \ No newline at end of file diff --git a/samples/app-region-selection/csharp/app-region-selection.slnx b/samples/app-region-selection/csharp/app-region-selection.slnx new file mode 100644 index 0000000000..243fb98203 --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/app-region-selection/csharp/RegionSectionApp/.gitignore b/samples/app-region-selection/csharp/app-region-selection/.gitignore similarity index 74% rename from samples/app-region-selection/csharp/RegionSectionApp/.gitignore rename to samples/app-region-selection/csharp/app-region-selection/.gitignore index 7466c01f9c..034334ff54 100644 --- a/samples/app-region-selection/csharp/RegionSectionApp/.gitignore +++ b/samples/app-region-selection/csharp/app-region-selection/.gitignore @@ -3,7 +3,6 @@ build appPackage/build env/.env.*.user env/.env.local -appsettings.Development.json .deployment # User-specific files @@ -22,4 +21,8 @@ bld/ [Ll]og/ # Notification local store -.notification.localstore.json \ No newline at end of file +.notification.localstore.json +.notification.playgroundstore.json + +# devTools +devTools/ \ No newline at end of file diff --git a/samples/app-region-selection/csharp/app-region-selection/Config.cs b/samples/app-region-selection/csharp/app-region-selection/Config.cs new file mode 100644 index 0000000000..aef0d886a5 --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection/Config.cs @@ -0,0 +1,15 @@ +namespace app_region_selection +{ + public class ConfigOptions + { + public TeamsConfigOptions Teams { get; set; } + } + + public class TeamsConfigOptions + { + public string BotType { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string TenantId { get; set; } + } +} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/RegionSectionApp/ConfigData/Regions.json b/samples/app-region-selection/csharp/app-region-selection/ConfigData/Regions.json similarity index 100% rename from samples/app-region-selection/csharp/RegionSectionApp/ConfigData/Regions.json rename to samples/app-region-selection/csharp/app-region-selection/ConfigData/Regions.json diff --git a/samples/app-region-selection/csharp/app-region-selection/Controllers/Controller.cs b/samples/app-region-selection/csharp/app-region-selection/Controllers/Controller.cs new file mode 100644 index 0000000000..4eedd9f489 --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection/Controllers/Controller.cs @@ -0,0 +1,414 @@ +using Microsoft.Teams.Api.Activities; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Activities; +using Microsoft.Teams.Apps.Annotations; +using app_region_selection.Models; +using System.Text.Json; + +namespace app_region_selection.Controllers +{ + [TeamsController] + public class Controller + { + // State storage - in-memory for simplicity (use proper state provider in production) + private static readonly Dictionary _conversationState = new(); + + [Message] + public async Task OnMessage([Context] MessageActivity activity, [Context] IContext.Client client) + { + var conversationId = activity.Conversation?.Id ?? "default"; + if (!_conversationState.ContainsKey(conversationId)) + { + _conversationState[conversationId] = new WelcomeUserState(); + } + + var welcomeUserState = _conversationState[conversationId]; + var text = activity.Text?.ToLowerInvariant() ?? string.Empty; + + // Check if user selected a region from the domain list + if (IsAnyDomainSelected(activity.Text ?? string.Empty)) + { + await WelcomeCardAsync(activity, client, conversationId, welcomeUserState); + return; + } + + // If user already selected a domain and types "change" or "yes" + if (welcomeUserState.DidUserSelectDomain && (text == "change" || text == "yes")) + { + await SendChangeDomainConfirmationCardAsync(activity, client, welcomeUserState); + return; + } + + switch (text) + { + case "reset": + await SendDomainListsCardAsync(client); + break; + + case "change": + case "yes": + await SendDomainListsCardAsync(client); + break; + + case "no": + case "cancel": + await WelcomeCardAsync(activity, client, conversationId, welcomeUserState); + break; + + default: + await SendWelcomeIntroCardAsync(activity, client, welcomeUserState); + break; + } + } + + [Conversation.MembersAdded] + public async Task OnMembersAdded([Context] ConversationUpdateActivity activity, [Context] IContext.Client client) + { + foreach (var member in activity.MembersAdded) + { + if (member.Id != activity.Recipient.Id) + { + var conversationId = activity.Conversation?.Id ?? "default"; + if (!_conversationState.ContainsKey(conversationId)) + { + _conversationState[conversationId] = new WelcomeUserState(); + } + + var welcomeUserState = _conversationState[conversationId]; + + if (welcomeUserState.DidUserSelectDomain) + { + welcomeUserState.DidUserSelectDomain = false; + welcomeUserState.SelectedRegion = string.Empty; + welcomeUserState.SelectedDomain = string.Empty; + } + + await SendWelcomeIntroCardAsync(activity, client, welcomeUserState); + } + } + } + + /// + /// Sends a welcome introduction card. + /// + private async Task SendWelcomeIntroCardAsync(Activity activity, IContext.Client client, WelcomeUserState welcomeUserState) + { + string domain; + string region; + + if (welcomeUserState.DidUserSelectDomain) + { + domain = welcomeUserState.SelectedDomain; + region = welcomeUserState.SelectedRegion; + } + else + { + var data = GetDefaultInfo(activity); + domain = data.domain; + region = data.region; + } + + string welcomeMsg = $"Your default Region is {region}."; + + var cardContent = new + { + type = "AdaptiveCard", + schema = "http://adaptivecards.io/schemas/adaptive-card.json", + version = "1.5", + body = new object[] + { + new + { + type = "TextBlock", + text = "Welcome to Region Selection App!", + size = "Large", + weight = "Bolder" + }, + new + { + type = "TextBlock", + text = "This will help you to choose your data center's region.", + wrap = true + }, + new + { + type = "TextBlock", + text = $"{welcomeMsg} Would you like to change region?", + wrap = true + } + }, + actions = new object[] + { + new + { + type = "Action.Submit", + title = "Yes", + data = new + { + msteams = new + { + type = "messageBack", + displayText = "Yes", + text = "Yes" + } + } + }, + new + { + type = "Action.Submit", + title = "No", + data = new + { + msteams = new + { + type = "messageBack", + displayText = "No", + text = "No" + } + } + } + } + }; + + await SendAdaptiveCardAsync(client, cardContent); + } + + /// + /// Gets the default region and domain information. + /// + private (string region, string domain) GetDefaultInfo(Activity activity) + { + string serviceUrl = activity.ServiceUrl; + string domain = serviceUrl.Substring(serviceUrl.LastIndexOf(".")).Trim('/'); + string region = activity.Locale ?? "en-US"; + + return (region, domain); + } + + /// + /// Gets the selected region and domain information based on the provided text. + /// + private (string region, string domain) GetSelectedInfo(string text) + { + string domain = text.Split("-").FirstOrDefault()?.Trim() ?? string.Empty; + + string file = Path.GetFullPath("ConfigData/Regions.json"); + string json = File.ReadAllText(file); + var selectedInfo = JsonSerializer.Deserialize(json)?.RegionDomains.FirstOrDefault(c => c.Region == domain); + + return selectedInfo != null ? (selectedInfo.Region, selectedInfo.Domain) : (string.Empty, string.Empty); + } + + /// + /// Sends a card with a list of available domains. + /// + private async Task SendDomainListsCardAsync(IContext.Client client) + { + string file = Path.GetFullPath("ConfigData/Regions.json"); + string json = File.ReadAllText(file); + var rootObject = JsonSerializer.Deserialize(json); + + if (rootObject?.RegionDomains == null || !rootObject.RegionDomains.Any()) + { + await client.Send("No regions available."); + return; + } + + var actions = rootObject.RegionDomains.Select(region => new + { + type = "Action.Submit", + title = $"{region.Region} - {region.Country}", + data = new + { + msteams = new + { + type = "messageBack", + displayText = $"{region.Region} - {region.Country}", + text = $"{region.Region} - {region.Country}" + } + } + }).ToArray(); + + var cardContent = new + { + type = "AdaptiveCard", + schema = "http://adaptivecards.io/schemas/adaptive-card.json", + version = "1.5", + body = new object[] + { + new + { + type = "TextBlock", + text = "Please select your region,", + wrap = true + } + }, + actions + }; + + await SendAdaptiveCardAsync(client, cardContent); + } + + /// + /// Sends a welcome card with the user's selected region and domain. + /// + private async Task WelcomeCardAsync(Activity activity, IContext.Client client, string conversationId, WelcomeUserState welcomeUserState) + { + var userName = activity.From.Name; + var text = activity is MessageActivity msgActivity ? msgActivity.Text ?? string.Empty : string.Empty; + var data = GetSelectedInfo(text); + string domainName = data.domain; + string regionName = data.region; + + if (string.IsNullOrEmpty(domainName) && welcomeUserState.DidUserSelectDomain) + { + domainName = welcomeUserState.SelectedDomain; + regionName = welcomeUserState.SelectedRegion; + } + + if (string.IsNullOrEmpty(domainName)) + { + var defaultData = GetDefaultInfo(activity); + domainName = defaultData.domain; + regionName = defaultData.region; + } + + welcomeUserState.DidUserSelectDomain = true; + welcomeUserState.SelectedDomain = domainName; + welcomeUserState.SelectedRegion = regionName; + _conversationState[conversationId] = welcomeUserState; + + var cardContent = new + { + type = "AdaptiveCard", + schema = "http://adaptivecards.io/schemas/adaptive-card.json", + version = "1.5", + body = new object[] + { + new + { + type = "TextBlock", + text = $"Welcome {userName},", + size = "Large", + weight = "Bolder" + }, + new + { + type = "TextBlock", + text = $"You are in {regionName} Region's Data Center", + wrap = true + }, + new + { + type = "TextBlock", + text = "If you want to change data center's region, please enter text 'Change'.", + wrap = true + } + } + }; + + await SendAdaptiveCardAsync(client, cardContent); + } + + /// + /// Sends a confirmation card to change the domain. + /// + private async Task SendChangeDomainConfirmationCardAsync(Activity activity, IContext.Client client, WelcomeUserState welcomeUserState) + { + var cardContent = new + { + type = "AdaptiveCard", + schema = "http://adaptivecards.io/schemas/adaptive-card.json", + version = "1.5", + body = new object[] + { + new + { + type = "TextBlock", + text = $"You have already selected your data center region and that is {welcomeUserState.SelectedRegion}. Would you like to change this?", + wrap = true + } + }, + actions = new object[] + { + new + { + type = "Action.Submit", + title = "Reset", + data = new + { + msteams = new + { + type = "messageBack", + displayText = "Reset", + text = "Reset" + } + } + }, + new + { + type = "Action.Submit", + title = "Cancel", + data = new + { + msteams = new + { + type = "messageBack", + displayText = "Cancel", + text = "Cancel" + } + } + } + } + }; + + await SendAdaptiveCardAsync(client, cardContent); + } + + /// + /// Checks if any domain is selected based on the provided text. + /// + private bool IsAnyDomainSelected(string text) + { + string domain = text.Split("-").FirstOrDefault()?.Trim() ?? string.Empty; + + if (string.IsNullOrEmpty(domain)) + return false; + + string file = Path.GetFullPath("ConfigData/Regions.json"); + string json = File.ReadAllText(file); + bool isAnyDomainSelected = JsonSerializer.Deserialize(json)?.RegionDomains.Any(c => c.Region == domain) ?? false; + + return isAnyDomainSelected; + } + + /// + /// Helper method to send adaptive card through Teams SDK V2 client + /// + private async Task SendAdaptiveCardAsync(IContext.Client client, object cardContent) + { + // Serialize the card content to JSON + var cardJson = JsonSerializer.Serialize(cardContent, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + // Parse the JSON to get a JsonElement (required by Teams SDK V2) + using var jsonDoc = JsonDocument.Parse(cardJson); + var cardElement = jsonDoc.RootElement.Clone(); + + // Create attachment using the correct Teams SDK V2 types + var attachment = new Microsoft.Teams.Api.Attachment + { + ContentType = Microsoft.Teams.Api.ContentType.AdaptiveCard, + Content = cardElement + }; + + // Create reply message with attachment + var reply = new MessageActivity(); + reply.Attachments = new List { attachment }; + + await client.Send(reply); + } + } +} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Controllers/HomeController.cs b/samples/app-region-selection/csharp/app-region-selection/Controllers/HomeController.cs similarity index 79% rename from samples/app-region-selection/csharp/RegionSectionApp/Controllers/HomeController.cs rename to samples/app-region-selection/csharp/app-region-selection/Controllers/HomeController.cs index 3db268df41..f8735526d8 100644 --- a/samples/app-region-selection/csharp/RegionSectionApp/Controllers/HomeController.cs +++ b/samples/app-region-selection/csharp/app-region-selection/Controllers/HomeController.cs @@ -1,16 +1,11 @@ -using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Mvc; -using Microsoft.BotBuilderSamples.Models; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json.Serialization; +using app_region_selection.Models; using System.Text.Json; -namespace Microsoft.BotBuilderSamples.Controllers +namespace app_region_selection.Controllers { [Route("")] - public class HomeController : Controller + public class HomeController : Microsoft.AspNetCore.Mvc.Controller { /// /// Displays the configuration page with the list of regions. diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Images/region-change-bot.png b/samples/app-region-selection/csharp/app-region-selection/Images/region-change-bot.png similarity index 100% rename from samples/app-region-selection/csharp/RegionSectionApp/Images/region-change-bot.png rename to samples/app-region-selection/csharp/app-region-selection/Images/region-change-bot.png diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Images/region-config.png b/samples/app-region-selection/csharp/app-region-selection/Images/region-config.png similarity index 100% rename from samples/app-region-selection/csharp/RegionSectionApp/Images/region-config.png rename to samples/app-region-selection/csharp/app-region-selection/Images/region-config.png diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Images/region-details-bot.png b/samples/app-region-selection/csharp/app-region-selection/Images/region-details-bot.png similarity index 100% rename from samples/app-region-selection/csharp/RegionSectionApp/Images/region-details-bot.png rename to samples/app-region-selection/csharp/app-region-selection/Images/region-details-bot.png diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Images/region-details.png b/samples/app-region-selection/csharp/app-region-selection/Images/region-details.png similarity index 100% rename from samples/app-region-selection/csharp/RegionSectionApp/Images/region-details.png rename to samples/app-region-selection/csharp/app-region-selection/Images/region-details.png diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Images/region-selection.gif b/samples/app-region-selection/csharp/app-region-selection/Images/region-selection.gif similarity index 100% rename from samples/app-region-selection/csharp/RegionSectionApp/Images/region-selection.gif rename to samples/app-region-selection/csharp/app-region-selection/Images/region-selection.gif diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Models/CountryRegions.cs b/samples/app-region-selection/csharp/app-region-selection/Models/CountryRegions.cs similarity index 91% rename from samples/app-region-selection/csharp/RegionSectionApp/Models/CountryRegions.cs rename to samples/app-region-selection/csharp/app-region-selection/Models/CountryRegions.cs index d0702c8e5f..583f83b9f8 100644 --- a/samples/app-region-selection/csharp/RegionSectionApp/Models/CountryRegions.cs +++ b/samples/app-region-selection/csharp/app-region-selection/Models/CountryRegions.cs @@ -1,4 +1,4 @@ -namespace Microsoft.BotBuilderSamples.Models +namespace app_region_selection.Models { /// /// Represents the root object containing region domains. diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Models/WelcomeUserState.cs b/samples/app-region-selection/csharp/app-region-selection/Models/WelcomeUserState.cs similarity index 82% rename from samples/app-region-selection/csharp/RegionSectionApp/Models/WelcomeUserState.cs rename to samples/app-region-selection/csharp/app-region-selection/Models/WelcomeUserState.cs index de9fc7ecf9..10aae1bb00 100644 --- a/samples/app-region-selection/csharp/RegionSectionApp/Models/WelcomeUserState.cs +++ b/samples/app-region-selection/csharp/app-region-selection/Models/WelcomeUserState.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.BotBuilderSamples.Models +namespace app_region_selection.Models { /// /// Represents the state of the user in the welcome conversation. diff --git a/samples/app-region-selection/csharp/app-region-selection/Program.cs b/samples/app-region-selection/csharp/app-region-selection/Program.cs new file mode 100644 index 0000000000..97a8bf5860 --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection/Program.cs @@ -0,0 +1,67 @@ +using app_region_selection; +using app_region_selection.Controllers; +using Azure.Core; +using Azure.Identity; +using Microsoft.Teams.Api.Auth; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Extensions; +using Microsoft.Teams.Common.Http; +using Microsoft.Teams.Plugins.AspNetCore.Extensions; + +var builder = WebApplication.CreateBuilder(args); +var config = builder.Configuration.Get(); + +Func> createTokenFactory = async (string[] scopes, string? tenantId) => +{ + var clientId = config.Teams.ClientId; + + var managedIdentityCredential = new ManagedIdentityCredential(clientId); + var tokenRequestContext = new TokenRequestContext(scopes, tenantId: tenantId); + var accessToken = await managedIdentityCredential.GetTokenAsync(tokenRequestContext); + + return new TokenResponse + { + TokenType = "Bearer", + AccessToken = accessToken.Token, + }; +}; +var appBuilder = App.Builder(); + +if (config.Teams.BotType == "UserAssignedMsi") +{ + appBuilder.AddCredentials(new TokenCredentials( + config.Teams.ClientId ?? string.Empty, + async (tenantId, scopes) => + { + return await createTokenFactory(scopes, tenantId); + } + )); +} + +// Add MVC services for the tab configuration pages +builder.Services.AddControllersWithViews(); + +// Add static files support for wwwroot +builder.Services.AddSingleton( + new Microsoft.Extensions.FileProviders.PhysicalFileProvider( + Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"))); + +builder.Services.AddSingleton(); +builder.AddTeams(appBuilder); + +var app = builder.Build(); + +// Enable static files +app.UseStaticFiles(); + +// Enable routing +app.UseRouting(); + +app.UseTeams(); + +// Map controller routes for MVC +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + +app.Run(); \ No newline at end of file diff --git a/samples/app-region-selection/csharp/app-region-selection/Properties/launchSettings.json b/samples/app-region-selection/csharp/app-region-selection/Properties/launchSettings.json new file mode 100644 index 0000000000..3572a7a03f --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "profiles": { + // Debug project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Playground", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.playgroundstore.json", + "UPDATE_TEAMS_APP": "false" + }, + "hotReloadProfile": "aspnetcore" + }, + // Debug project within Teams + "Start Project": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + }, + } +} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Views/Home/Index.cshtml b/samples/app-region-selection/csharp/app-region-selection/Views/Home/Index.cshtml similarity index 94% rename from samples/app-region-selection/csharp/RegionSectionApp/Views/Home/Index.cshtml rename to samples/app-region-selection/csharp/app-region-selection/Views/Home/Index.cshtml index e2c2201302..af3f39e8a0 100644 --- a/samples/app-region-selection/csharp/RegionSectionApp/Views/Home/Index.cshtml +++ b/samples/app-region-selection/csharp/app-region-selection/Views/Home/Index.cshtml @@ -1,4 +1,4 @@ -@model Microsoft.BotBuilderSamples.Models.RootObject; +@model app_region_selection.Models.RootObject; @@ -35,5 +35,3 @@ - - diff --git a/samples/app-region-selection/csharp/RegionSectionApp/Views/Home/Welcome.cshtml b/samples/app-region-selection/csharp/app-region-selection/Views/Home/Welcome.cshtml similarity index 69% rename from samples/app-region-selection/csharp/RegionSectionApp/Views/Home/Welcome.cshtml rename to samples/app-region-selection/csharp/app-region-selection/Views/Home/Welcome.cshtml index f82f4d61fb..d64f75d0bb 100644 --- a/samples/app-region-selection/csharp/RegionSectionApp/Views/Home/Welcome.cshtml +++ b/samples/app-region-selection/csharp/app-region-selection/Views/Home/Welcome.cshtml @@ -1,10 +1,8 @@ - @{ ViewData["Title"] = "Welcome"; - var d = ViewBag.selectedDomain; + var d = ViewBag.SelectedDomain; }

Welcome!

You are now in @d Region.

- diff --git a/samples/app-region-selection/csharp/app-region-selection/app-region-selection.csproj b/samples/app-region-selection/csharp/app-region-selection/app-region-selection.csproj new file mode 100644 index 0000000000..6acf86ec30 --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection/app-region-selection.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + + + + + + + + + + + + + + + + PreserveNewest + None + + + + PreserveNewest + None + + + \ No newline at end of file diff --git a/samples/app-region-selection/csharp/app-region-selection/appsettings.Development.json b/samples/app-region-selection/csharp/app-region-selection/appsettings.Development.json new file mode 100644 index 0000000000..f225c8442d --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection/appsettings.Development.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "", + "TenantId": "" + } +} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/app-region-selection/appsettings.Playground.json b/samples/app-region-selection/csharp/app-region-selection/appsettings.Playground.json new file mode 100644 index 0000000000..0497106b3c --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection/appsettings.Playground.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "TenantId": "", + "BotType": "" + } +} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/app-region-selection/appsettings.json b/samples/app-region-selection/csharp/app-region-selection/appsettings.json new file mode 100644 index 0000000000..9e3379db53 --- /dev/null +++ b/samples/app-region-selection/csharp/app-region-selection/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "" + } +} \ No newline at end of file diff --git a/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Scripts/teamsapp.js b/samples/app-region-selection/csharp/app-region-selection/wwwroot/Scripts/teamsapp.js similarity index 99% rename from samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Scripts/teamsapp.js rename to samples/app-region-selection/csharp/app-region-selection/wwwroot/Scripts/teamsapp.js index aab0bb95bc..75aa4cb03c 100644 --- a/samples/app-region-selection/csharp/RegionSectionApp/wwwroot/Scripts/teamsapp.js +++ b/samples/app-region-selection/csharp/app-region-selection/wwwroot/Scripts/teamsapp.js @@ -1,4 +1,4 @@ -(function () { +(function () { 'use strict'; // Call the initialize API first diff --git a/samples/app-region-selection/csharp/demo-manifest/app-region-selection.zip b/samples/app-region-selection/csharp/demo-manifest/app-region-selection.zip deleted file mode 100644 index 200d9be127..0000000000 Binary files a/samples/app-region-selection/csharp/demo-manifest/app-region-selection.zip and /dev/null differ diff --git a/samples/meetings-transcription/csharp/.gitignore b/samples/meetings-transcription/csharp/.gitignore deleted file mode 100644 index 2dacf6af62..0000000000 --- a/samples/meetings-transcription/csharp/.gitignore +++ /dev/null @@ -1,365 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -Source/Microsoft.Teams.Samples.PeoplePicker/.vs/ -Source/Microsoft.Teams.Samples.PeoplePicker/obj/ -Source/Microsoft.Teams.Samples.PeoplePicker/bin/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/M365Agent/M365Agent.ttkproj b/samples/meetings-transcription/csharp/M365Agent/M365Agent.ttkproj deleted file mode 100644 index c51287454b..0000000000 --- a/samples/meetings-transcription/csharp/M365Agent/M365Agent.ttkproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 766db75a-580e-4d88-9bf9-cdbfc5fde3db - - - - - - - - - \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/M365Agent/README.md b/samples/meetings-transcription/csharp/M365Agent/README.md deleted file mode 100644 index 732169accf..0000000000 --- a/samples/meetings-transcription/csharp/M365Agent/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Welcome to Microsoft 365 Agents Toolkit! - -## Quick Start -1. Press F5, or select Debug > Start Debugging menu in Visual Studio to start your app -
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) -2. In Microsoft 365 Agents Playground, type and send anything to your bot to get a response - - -## Run the app on other platforms - -The Teams app can run in other platforms like Outlook and Microsoft 365 app. See https://aka.ms/vs-ttk-debug-multi-profiles for more details. - -## Get more info - -New to Teams app development or Microsoft 365 Agents Toolkit? Explore Teams app manifests, cloud deployment, and much more in the https://aka.ms/teams-toolkit-vs-docs. - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/samples/meetings-transcription/csharp/M365Agent/appPackage/manifest.json b/samples/meetings-transcription/csharp/M365Agent/appPackage/manifest.json index a1dae24205..f30198df38 100644 --- a/samples/meetings-transcription/csharp/M365Agent/appPackage/manifest.json +++ b/samples/meetings-transcription/csharp/M365Agent/appPackage/manifest.json @@ -1,50 +1,59 @@ { - "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.19/MicrosoftTeams.schema.json", - "manifestVersion": "1.19", - "version": "1.0.0", - "id": "${{TEAMS_APP_ID}}", - "developer": { - "name": "Microsoft", - "websiteUrl": "https://www.microsoft.com", - "privacyUrl": "https://www.microsoft.com/privacy", - "termsOfUseUrl": "https://www.microsoft.com/termsofuse" - }, - "icons": { - "color": "color.png", - "outline": "outline.png" - }, - "name": { - "short": "Meeting Transcript Bot", - "full": "Meeting Transcript Bot" - }, - "description": { - "short": "Fetch and display meeting transcripts using Microsoft Graph API in Teams.", - "full": "This sample app retrieves meeting transcripts via the Graph API and displays them in a task module." - }, - "accentColor": "#235EA5", - "bots": [ - { - "botId": "${{BOT_ID}}", - "scopes": [ "groupChat" ], - "isNotificationOnly": true - } - ], - "validDomains": [ - "${{BOT_DOMAIN}}" - ], - "permissions": [ "messageTeamMembers" ], - "webApplicationInfo": { - "id": "${{BOT_ID}}", - "resource": "https://RscPermission" - }, - "authorization": { - "permissions": { - "resourceSpecific": [ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json", + "manifestVersion": "1.23", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Microsoft", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.microsoft.com/privacy", + "termsOfUseUrl": "https://www.microsoft.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "Meeting Transcript Bot", + "full": "Meeting Transcript Bot" + }, + "description": { + "short": "Fetch and display meeting transcripts using Microsoft Graph API in Teams.", + "full": "This sample app retrieves meeting transcripts via the Graph API and displays them in a task module." + }, + "accentColor": "#235EA5", + "bots": [ { - "name": "OnlineMeeting.ReadBasic.Chat", - "type": "Application" + "botId": "${{BOT_ID}}", + "scopes": [ + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": true + } + ], + "composeExtensions": [ + ], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "messageTeamMembers" + ], + "validDomains": [ + "${{BOT_DOMAIN}}" + ], + "webApplicationInfo": { + "id": "${{BOT_ID}}", + "resource": "https://RscPermission" + }, + "authorization": { + "permissions": { + "resourceSpecific": [ + { + "name": "OnlineMeeting.ReadBasic.Chat", + "type": "Application" + } + ] } - ] } - } } \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/M365Agent/env/.env.dev b/samples/meetings-transcription/csharp/M365Agent/env/.env.dev index 1943ce1cf5..df4f9da508 100644 --- a/samples/meetings-transcription/csharp/M365Agent/env/.env.dev +++ b/samples/meetings-transcription/csharp/M365Agent/env/.env.dev @@ -12,7 +12,4 @@ RESOURCE_SUFFIX= # Generated during provision, you can also add your own variables. BOT_ID= TEAMS_APP_ID= -BOT_AZURE_APP_SERVICE_RESOURCE_ID= -TEAMS_APP_TENANT_ID= -BOT_DOMAIN= -BOT_TENANT_ID= \ No newline at end of file +BOT_AZURE_APP_SERVICE_RESOURCE_ID= \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/M365Agent/env/.env.local b/samples/meetings-transcription/csharp/M365Agent/env/.env.local index 86dfacc35a..5216a6cbb0 100644 --- a/samples/meetings-transcription/csharp/M365Agent/env/.env.local +++ b/samples/meetings-transcription/csharp/M365Agent/env/.env.local @@ -5,9 +5,10 @@ TEAMSFX_ENV=local APP_NAME_SUFFIX=local # Generated during provision, you can also add your own variables. -BOT_ID= + TEAMS_APP_ID= TEAMS_APP_TENANT_ID= +BOT_ID= BOT_OBJECT_ID= TEAMSFX_M365_USER_NAME= diff --git a/samples/meetings-transcription/csharp/M365Agent/infra/azure.bicep b/samples/meetings-transcription/csharp/M365Agent/infra/azure.bicep index 5b757c7544..658e412a21 100644 --- a/samples/meetings-transcription/csharp/M365Agent/infra/azure.bicep +++ b/samples/meetings-transcription/csharp/M365Agent/infra/azure.bicep @@ -43,16 +43,16 @@ resource webApp 'Microsoft.Web/sites@2021-02-01' = { value: '1' } { - name: 'Connections__BotServiceConnection__Settings__ClientId' + name: 'Teams__ClientId' value: identity.properties.clientId } { - name: 'Connections__BotServiceConnection__Settings__TenantId' + name: 'Teams__TenantId' value: identity.properties.tenantId } { - name: 'TokenValidation__Audiences__0' - value: identity.properties.clientId + name: 'Teams__BotType' + value: 'UserAssignedMsi' } ] ftpsState: 'FtpsOnly' diff --git a/samples/meetings-transcription/csharp/M365Agent/launchSettings.json b/samples/meetings-transcription/csharp/M365Agent/launchSettings.json index 1873805261..2af8ce7a8a 100644 --- a/samples/meetings-transcription/csharp/M365Agent/launchSettings.json +++ b/samples/meetings-transcription/csharp/M365Agent/launchSettings.json @@ -3,9 +3,12 @@ // Launch project within Microsoft 365 Agents Playground "Microsoft 365 Agents Playground (browser)": { "commandName": "Project", - "environmentVariables": { "UPDATE_TEAMS_APP": "false", "DEFAULT_CHANNEL_ID": "emulator" }, + "environmentVariables": { + "UPDATE_TEAMS_APP": "false", + "M365_AGENTS_PLAYGROUND_TARGET_SDK": "teams-ai-v2-dotnet" + }, "launchTestTool": true, - "launchUrl": "http://localhost:56150" + "launchUrl": "http://localhost:56150", }, // Launch project within Teams "Microsoft Teams (browser)": { diff --git a/samples/meetings-transcription/csharp/M365Agent/m365agents.local.yml b/samples/meetings-transcription/csharp/M365Agent/m365agents.local.yml index 858e833676..78817ae9f7 100644 --- a/samples/meetings-transcription/csharp/M365Agent/m365agents.local.yml +++ b/samples/meetings-transcription/csharp/M365Agent/m365agents.local.yml @@ -1,14 +1,14 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.7/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.11/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.7 +version: v1.11 provision: # Creates a Teams app - uses: teamsApp/create with: # Teams app name - name: MeetingTranscription${{APP_NAME_SUFFIX}} + name: meetings-transcription${{APP_NAME_SUFFIX}} # Write the information of created resources into environment file for # the specified environment variable(s). writeToEnvironmentFile: @@ -18,8 +18,9 @@ provision: - uses: aadApp/create with: # The Microsoft Entra application's display name - name: MeetingTranscription${{APP_NAME_SUFFIX}} + name: meetings-transcription${{APP_NAME_SUFFIX}} generateClientSecret: true + generateServicePrincipal: true signInAudience: AzureADMultipleOrgs writeToEnvironmentFile: # The Microsoft Entra application's client id created for bot. @@ -29,38 +30,21 @@ provision: # The Microsoft Entra application's object id created for bot. objectId: BOT_OBJECT_ID - # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile with: - target: ../MeetingTranscription/appsettings.json + target: ../meetings-transcription/appsettings.Development.json content: - MicrosoftAppId: ${{BOT_ID}} - MicrosoftAppPassword: ${{SECRET_BOT_PASSWORD}} - MicrosoftAppTenantId: ${{TEAMS_APP_TENANT_ID}} - AppBaseUrl: ${{BOT_ENDPOINT}} - - # Generate runtime appsettings to JSON file - - uses: file/createOrUpdateJsonFile - with: - target: ../MeetingTranscription/appsettings.Development.json - content: - TokenValidation: - Audiences: - ClientId: ${{BOT_ID}} - Connections: - BotServiceConnection: - Settings: - AuthType: "ClientSecret" - AuthorityEndpoint: "https://login.microsoftonline.com/botframework.com" - ClientId: ${{BOT_ID}} - ClientSecret: ${{SECRET_BOT_PASSWORD}} + Teams: + ClientId: ${{BOT_ID}} + ClientSecret: ${{SECRET_BOT_PASSWORD}} + TenantId: ${{TEAMS_APP_TENANT_ID}} # Create or update the bot registration on dev.botframework.com - uses: botFramework/create with: botId: ${{BOT_ID}} - name: MeetingTranscription + name: meetings-transcription messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages description: "" channels: @@ -71,7 +55,6 @@ provision: with: # Path to manifest template manifestPath: ./appPackage/manifest.json - # Build Teams app package with latest env value - uses: teamsApp/zipAppPackage with: diff --git a/samples/meetings-transcription/csharp/M365Agent/m365agents.yml b/samples/meetings-transcription/csharp/M365Agent/m365agents.yml index b064b0d8db..5d6174c495 100644 --- a/samples/meetings-transcription/csharp/M365Agent/m365agents.yml +++ b/samples/meetings-transcription/csharp/M365Agent/m365agents.yml @@ -1,7 +1,7 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.7/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.9/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.7 +version: v1.9 environmentFolderPath: ./env @@ -11,7 +11,7 @@ provision: - uses: teamsApp/create with: # Teams app name - name: MeetingTranscription{{APP_NAME_SUFFIX}} + name: meetings-transcription${{APP_NAME_SUFFIX}} # Write the information of created resources into environment file for # the specified environment variable(s). writeToEnvironmentFile: @@ -71,18 +71,18 @@ provision: deploy: - uses: cli/runDotnetCommand with: - args: publish --configuration Release MeetingTranscription.csproj - workingDirectory: ../MeetingTranscription + args: publish --configuration Release --runtime win-x86 --self-contained + meetings-transcription.csproj + workingDirectory: ../meetings-transcription # Deploy your application to Azure App Service using the zip deploy feature. # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. - uses: azureAppService/zipDeploy with: # Deploy base folder - artifactFolder: bin/Release/net9.0/publish + artifactFolder: bin/Release/net10.0/win-x86/publish # The resource id of the cloud resource to be deployed to. # This key will be generated by arm/deploy action automatically. # You can replace it with your existing Azure Resource id # or add it to your environment variable file. resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} - workingDirectory: ../MeetingTranscription - + workingDirectory: ../meetings-transcription diff --git a/samples/meetings-transcription/csharp/MeetingTranscription.sln b/samples/meetings-transcription/csharp/MeetingTranscription.sln deleted file mode 100644 index a7d71c566e..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32505.173 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeetingTranscription", "MeetingTranscription\MeetingTranscription.csproj", "{3EDF2E79-29C1-4DAC-B76B-97A32A4F79C2}" -EndProject -Project("{A9E3F50B-275E-4AF7-ADCE-8BE12D41E305}") = "M365Agent", "M365Agent\M365Agent.ttkproj", "{766DB75A-580E-4D88-9BF9-CDBFC5FDE3DB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E7F8FF0D-402A-4D45-8F7A-A7B810662C8B}" - ProjectSection(SolutionItems) = preProject - MeetingTranscription.slnLaunch.user = MeetingTranscription.slnLaunch.user - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3EDF2E79-29C1-4DAC-B76B-97A32A4F79C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3EDF2E79-29C1-4DAC-B76B-97A32A4F79C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3EDF2E79-29C1-4DAC-B76B-97A32A4F79C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3EDF2E79-29C1-4DAC-B76B-97A32A4F79C2}.Release|Any CPU.Build.0 = Release|Any CPU - {766DB75A-580E-4D88-9BF9-CDBFC5FDE3DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {766DB75A-580E-4D88-9BF9-CDBFC5FDE3DB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {766DB75A-580E-4D88-9BF9-CDBFC5FDE3DB}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {766DB75A-580E-4D88-9BF9-CDBFC5FDE3DB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {766DB75A-580E-4D88-9BF9-CDBFC5FDE3DB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {33A214ED-5D5A-4382-B077-F057A7993B98} - EndGlobalSection -EndGlobal diff --git a/samples/meetings-transcription/csharp/MeetingTranscription.slnLaunch.user b/samples/meetings-transcription/csharp/MeetingTranscription.slnLaunch.user deleted file mode 100644 index d90b6ce72d..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription.slnLaunch.user +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "Name": "Microsoft Teams (browser)", - "Projects": [ - { - "Path": "MeetingTranscription\\MeetingTranscription.csproj", - "Action": "Start", - "DebugTarget": "Start Project" - }, - { - "Path": "M365Agent\\M365Agent.ttkproj", - "Action": "StartWithoutDebugging", - "DebugTarget": "Microsoft Teams (Browser)" - } - ] - }, - { - "Name": "Microsoft Teams (browser) (skip update app)", - "Projects": [ - { - "Path": "MeetingTranscription\\MeetingTranscription.csproj", - "Action": "Start", - "DebugTarget": "Start Project" - }, - { - "Path": "M365Agent\\M365Agent.ttkproj", - "Action": "StartWithoutDebugging", - "DebugTarget": "Microsoft Teams (browser) (skip update app)" - } - ] - } -] \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/AdapterWithErrorHandler.cs b/samples/meetings-transcription/csharp/MeetingTranscription/AdapterWithErrorHandler.cs deleted file mode 100644 index a94332edab..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/AdapterWithErrorHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Builder.TraceExtensions; -using Microsoft.Bot.Connector.Authentication; -using Microsoft.Extensions.Logging; - -namespace MeetingTranscription -{ - public class AdapterWithErrorHandler : CloudAdapter - { - public AdapterWithErrorHandler(BotFrameworkAuthentication botFrameworkAuthentication, ILogger logger) - : base(botFrameworkAuthentication, logger) - { - OnTurnError = async (turnContext, exception) => - { - // Log any leaked exception from the application. - logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); - - // Uncomment below commented line for local debugging. - // await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}"); - - // Send a trace activity, which will be displayed in the Bot Framework Emulator - await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); - }; - } - } -} diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Bots/TranscriptionBot.cs b/samples/meetings-transcription/csharp/MeetingTranscription/Bots/TranscriptionBot.cs deleted file mode 100644 index 1fadf758ba..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Bots/TranscriptionBot.cs +++ /dev/null @@ -1,173 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using MeetingTranscription.Helpers; -using MeetingTranscription.Models.Configuration; -using MeetingTranscription.Services; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Teams; -using Microsoft.Bot.Schema; -using Microsoft.Bot.Schema.Teams; -using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MeetingTranscription.Bots -{ - public class TranscriptionBot : TeamsActivityHandler - { - /// - /// Helper instance to make graph calls. - /// - private readonly GraphHelper graphHelper; - - /// - /// Stores the Azure configuration values. - /// - private readonly IOptions azureSettings; - - /// - /// Store details of meeting transcript. - /// - private readonly ConcurrentDictionary transcriptsDictionary; - - /// - /// Instance of card factory to create adaptive cards. - /// - private readonly ICardFactory cardFactory; - - /// - /// Creates bot instance. - /// - /// Stores the Azure configuration values. - /// Store details of meeting transcript. - /// Instance of card factory to create adaptive cards. - public TranscriptionBot(IOptions azureSettings, ConcurrentDictionary transcriptsDictionary, ICardFactory cardFactory) - { - this.transcriptsDictionary = transcriptsDictionary; - this.azureSettings = azureSettings; - graphHelper = new GraphHelper(azureSettings); - this.cardFactory = cardFactory; - } - - /// - /// Activity handler for on message activity. - /// - /// A strongly-typed context object for this turn. - /// A cancellation token that can be used by other objects - /// or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. - protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - var replyText = $"Echo: {turnContext.Activity.Text}"; - await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken); - } - - /// - /// Activity handler for meeting end event. - /// - /// The details of the meeting. - /// A strongly-typed context object for this turn. - /// A cancellation token that can be used by other objects - /// or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. - protected override async Task OnTeamsMeetingEndAsync(MeetingEndEventDetails meeting, ITurnContext turnContext, CancellationToken cancellationToken) - { - try - { - var meetingInfo = await TeamsInfo.GetMeetingInfoAsync(turnContext); - Console.WriteLine($"Meeting Ended: {meetingInfo.Details.MsGraphResourceId}"); - - // NEW: Get meeting organizer information when meeting ends - var organizerId = await graphHelper.GetMeetingOrganizerFromTeamsContextAsync(turnContext); - if (!string.IsNullOrEmpty(organizerId)) - { - Console.WriteLine($"Meeting organizer identified: {organizerId}"); - } - - // NEW: Use Teams context to find organizer and get transcripts - var result = await graphHelper.GetMeetingTranscriptionsAsync(meetingInfo.Details.MsGraphResourceId, organizerId); - - if (!string.IsNullOrEmpty(result)) - { - transcriptsDictionary.AddOrUpdate(meetingInfo.Details.MsGraphResourceId, result, (key, newValue) => result); - - var attachment = this.cardFactory.CreateAdaptiveCardAttachement(new { MeetingId = meetingInfo.Details.MsGraphResourceId }); - await turnContext.SendActivityAsync(MessageFactory.Attachment(attachment), cancellationToken); - - Console.WriteLine($"Successfully retrieved and cached meeting transcript for {meetingInfo.Details.MsGraphResourceId}"); - } - else - { - var attachment = this.cardFactory.CreateNotFoundCardAttachement(); - await turnContext.SendActivityAsync(MessageFactory.Attachment(attachment), cancellationToken); - - Console.WriteLine($"No transcript found for meeting {meetingInfo.Details.MsGraphResourceId}"); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error in OnTeamsMeetingEndAsync: {ex.Message}"); - - // Send error card to user - var errorAttachment = this.cardFactory.CreateNotFoundCardAttachement(); - await turnContext.SendActivityAsync(MessageFactory.Attachment(errorAttachment), cancellationToken); - } - } - - /// - /// Activity handler for Task module fetch event. - /// - /// A strongly-typed context object for this turn. - /// The task module invoke request value payload. - /// A cancellation token that can be used by other objects - /// or threads to receive notice of cancellation. - /// A Task Module Response for the request. - protected override async Task OnTeamsTaskModuleFetchAsync(ITurnContext turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken) - { - try - { - var meetingId = JObject.FromObject(taskModuleRequest.Data)["meetingId"]; - - return new TaskModuleResponse - { - Task = new TaskModuleContinueResponse - { - Type = "continue", - Value = new TaskModuleTaskInfo() - { - Url = $"{this.azureSettings.Value.AppBaseUrl}/home?meetingId={meetingId}", - Height = 600, - Width = 600, - Title = "Meeting Transcript", - }, - } - }; - } - catch (Exception ex) - { - Console.WriteLine($"Error in OnTeamsTaskModuleFetchAsync: {ex.Message}"); - - return new TaskModuleResponse - { - Task = new TaskModuleContinueResponse - { - Type = "continue", - Value = new TaskModuleTaskInfo() - { - Url = this.azureSettings.Value.AppBaseUrl + "/home", - Height = 350, - Width = 350, - Title = "Meeting Transcript", - }, - } - }; - } - } - } -} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Controllers/BotController.cs b/samples/meetings-transcription/csharp/MeetingTranscription/Controllers/BotController.cs deleted file mode 100644 index 0d5f98bb51..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Controllers/BotController.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using System.Threading.Tasks; - -namespace MeetingTranscription.Controllers -{ - // BotController handles incoming HTTP POST requests and delegates them to the CloudAdapter. - // It acts as an interface between incoming requests and the bot's processing logic for SingleTenant setup. - [Route("api/messages")] - [ApiController] - public class BotController : ControllerBase - { - private readonly CloudAdapter adapter; // Updated to CloudAdapter for SingleTenant authentication - private readonly IBot _bot; - - public BotController(CloudAdapter adapter, IBot bot) - { - this.adapter = adapter; - _bot = bot; - } - - [HttpPost] - [HttpGet] - public async Task PostAsync() - { - // Delegate the processing of the HTTP POST to the adapter. - // The adapter will invoke the bot. - await adapter.ProcessAsync(Request, Response, _bot); - } - } -} diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Controllers/HomeController.cs b/samples/meetings-transcription/csharp/MeetingTranscription/Controllers/HomeController.cs deleted file mode 100644 index e91f95586d..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Controllers/HomeController.cs +++ /dev/null @@ -1,64 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using MeetingTranscription.Helpers; -using MeetingTranscription.Models.Configuration; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using System.Collections.Concurrent; -using System.Threading.Tasks; - -namespace MeetingTranscription.Controllers -{ - public class HomeController : Controller - { - private readonly ConcurrentDictionary transcriptsDictionary; - - /// - /// Stores the Azure configuration values. - /// - private readonly IOptions azureSettings; - - /// - /// Stores the Azure configuration values. - /// - private readonly GraphHelper graphHelper; - - public HomeController (ConcurrentDictionary transcriptsDictionary, IOptions azureSettings) - { - this.transcriptsDictionary = transcriptsDictionary; - this.azureSettings = azureSettings; - graphHelper = new(azureSettings); - } - - /// - /// Returns view to be displayed in Task Module. - /// - /// Id of the meeting. - /// - public async Task Index([FromQuery] string meetingId) - { - ViewBag.Transcripts = "Transcript not found."; - - if (!string.IsNullOrEmpty(meetingId)) - { - var isFound = transcriptsDictionary.TryGetValue(meetingId, out string transcripts); - if (isFound) - { - ViewBag.Transcripts = $"Format: {transcripts}"; - } - else - { - var result = await this.graphHelper.GetMeetingTranscriptionsAsync(meetingId); - if (!string.IsNullOrEmpty(meetingId)) - { - transcriptsDictionary.AddOrUpdate(meetingId, result, (key, newValue) => result); - ViewBag.Transcripts = $"Format: {result}"; - } - } - } - return View(); - } - } -} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Helpers/GraphHelper.cs b/samples/meetings-transcription/csharp/MeetingTranscription/Helpers/GraphHelper.cs deleted file mode 100644 index 48cf309fdb..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Helpers/GraphHelper.cs +++ /dev/null @@ -1,162 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -namespace MeetingTranscription.Helpers -{ - using Microsoft.Extensions.Options; - using Microsoft.Identity.Client; - using MeetingTranscription.Models.Configuration; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http.Headers; - using System.Threading.Tasks; - using System.Net.Http; - using MeetingTranscription.Models; - using Newtonsoft.Json.Linq; - using Microsoft.Bot.Builder; - using Microsoft.Bot.Builder.Teams; - using Microsoft.Bot.Schema.Teams; - using Microsoft.Bot.Schema; - - public class GraphHelper - { - /// - /// Stores the Azure configuration values. - /// - private readonly IOptions azureSettings; - - /// - /// Cache for user discovery to avoid repeated API calls. - /// - private static readonly Dictionary UserCache = new Dictionary(); - - public GraphHelper(IOptions azureSettings) - { - this.azureSettings = azureSettings; - } - - /// - /// Gets application token. - /// - /// Application token. - public async Task GetToken() - { - IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(this.azureSettings.Value.MicrosoftAppId) - .WithClientSecret(this.azureSettings.Value.MicrosoftAppPassword) - .WithAuthority($"https://login.microsoftonline.com/{this.azureSettings.Value.MicrosoftAppTenantId}") - .WithRedirectUri("https://daemon") - .Build(); - - // TeamsAppInstallation.ReadWriteForChat.All Chat.Create User.Read.All TeamsAppInstallation.ReadWriteForChat.All - string[] scopes = new string[] { "https://graph.microsoft.com/.default" }; - var result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); - - return result.AccessToken; - } - - /// - /// Get meeting transcripts using a specific user ID. - /// - /// Meeting ID. - /// User ID who has access to the meeting. - /// Meeting transcripts. - public async Task GetMeetingTranscriptionsAsync(string meetingId, string userId=null) - { - try - { - string accessToken = await GetToken(); - - Console.WriteLine($"Attempting to get transcripts for meeting {meetingId} using user {userId}"); - - var getAllTranscriptsEndpoint = $"{this.azureSettings.Value.GraphApiEndpoint}/users/{userId}/onlineMeetings/{meetingId}/transcripts"; - var getAllTranscriptReq = new HttpRequestMessage(HttpMethod.Get, getAllTranscriptsEndpoint); - getAllTranscriptReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - - var client = new HttpClient(); - var response = await client.SendAsync(getAllTranscriptReq); - - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(); - Console.WriteLine($"Failed to get transcripts: {response.StatusCode} - {errorContent}"); - return string.Empty; - } - - var content = await response.Content.ReadAsStringAsync(); - var transcripts = (JObject.Parse(content)["value"]).ToObject>(); - - if (transcripts != null && transcripts.Count > 0) - { - var transcriptId = transcripts.FirstOrDefault()?.Id; - var getTranscriptEndpoint = $"{getAllTranscriptsEndpoint}/{transcriptId}/content?$format=text/vtt"; - - var getTranscriptReq = new HttpRequestMessage(HttpMethod.Get, getTranscriptEndpoint); - getTranscriptReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - var transcriptResponse = await client.SendAsync(getTranscriptReq); - - if (transcriptResponse.IsSuccessStatusCode) - { - var transcriptContent = await transcriptResponse.Content.ReadAsStringAsync(); - Console.WriteLine($"Successfully retrieved transcript content ({transcriptContent.Length} characters)"); - return transcriptContent; - } - else - { - Console.WriteLine($"Failed to get transcript content: {transcriptResponse.StatusCode}"); - return string.Empty; - } - } - else - { - Console.WriteLine("No transcripts found for this meeting."); - return string.Empty; - } - } - catch (Exception ex) - { - Console.WriteLine($"Error getting transcripts: {ex.Message}"); - throw; - } - } - - /// - /// TEAMS-SPECIFIC: Get meeting organizer using Teams context and member information. - /// This is the most reliable approach for Teams bots. - /// - /// Teams turn context from the bot. - /// Meeting organizer user ID. - public async Task GetMeetingOrganizerFromTeamsContextAsync(ITurnContext turnContext) - { - try - { - Console.WriteLine("Getting meeting organizer from Teams context..."); - - // Get meeting information from Teams context - var meetingInfo = await TeamsInfo.GetMeetingInfoAsync(turnContext); - if (meetingInfo?.Organizer != null) - { - var organizerAadId = meetingInfo.Organizer.AadObjectId; - Console.WriteLine($"Found meeting organizer from meeting info: {organizerAadId}"); - return organizerAadId; - } - - // Last fallback: use the current user from turn context - if (turnContext.Activity?.From?.AadObjectId != null) - { - Console.WriteLine($"Using current user as fallback organizer: {turnContext.Activity.From.AadObjectId}"); - return turnContext.Activity.From.AadObjectId; - } - - Console.WriteLine("No meeting organizer found from Teams context"); - return string.Empty; - } - catch (Exception ex) - { - Console.WriteLine($"Error getting meeting organizer from Teams context: {ex.Message}"); - return string.Empty; - } - } - } -} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/MeetingTranscription.csproj b/samples/meetings-transcription/csharp/MeetingTranscription/MeetingTranscription.csproj deleted file mode 100644 index b5dbe00fd1..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/MeetingTranscription.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - net6.0 - latest - - - - - - - - - - - - - - Always - - - - diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Program.cs b/samples/meetings-transcription/csharp/MeetingTranscription/Program.cs deleted file mode 100644 index 79a6c82244..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Program.cs +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using Microsoft.AspNetCore.Builder; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Connector.Authentication; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using MeetingTranscription; -using MeetingTranscription.Bots; -using System.Collections.Concurrent; -using MeetingTranscription.Models.Configuration; -using MeetingTranscription.Services; - -var builder = WebApplication.CreateBuilder(args); - -// Adds application configuration settings to specified IServiceCollection. -builder.Services.AddOptions() -.Configure((botOptions, configuration) => -{ - botOptions.MicrosoftAppId = configuration.GetValue("MicrosoftAppId"); - botOptions.MicrosoftAppPassword = configuration.GetValue("MicrosoftAppPassword"); - botOptions.MicrosoftAppTenantId = configuration.GetValue("MicrosoftAppTenantId"); - botOptions.AppBaseUrl = configuration.GetValue("AppBaseUrl"); - botOptions.UserId = configuration.GetValue("UserId"); - botOptions.GraphApiEndpoint = configuration.GetValue("GraphApiEndpoint"); -}); - -builder.Services.AddHttpClient().AddControllers().AddNewtonsoftJson(); - -// Creates Singleton Card Factory. -builder.Services.AddSingleton(); - -// Create a global hashset for our save task details -builder.Services.AddSingleton>(); - -// Create the Bot Framework Authentication to be used with the Bot Adapter. -builder.Services.AddSingleton(); - -// Create the Bot Adapter with error handling enabled. -builder.Services.AddSingleton(); - -builder.Services.AddSingleton(); - -// Create the bot as a transient. In this case the ASP Controller is expecting an IBot. -IServiceCollection serviceCollection = builder.Services.AddTransient(); -builder.Services.AddMvc().AddSessionStateTempDataProvider(); - -var app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.UseDeveloperExceptionPage(); -} - -app.UseDefaultFiles() - .UseStaticFiles() - .UseWebSockets() - .UseRouting() - .UseAuthorization() - .UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - }); - -app.Run(); \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Properties/launchSettings.json b/samples/meetings-transcription/csharp/MeetingTranscription/Properties/launchSettings.json deleted file mode 100644 index ff9d8fe153..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Properties/launchSettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "profiles": { - "Start Project": { - "commandName": "Project", - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:7130;http://localhost:5130", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "hotReloadProfile": "aspnetcore" - } - } -} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/appsettings.Development.json b/samples/meetings-transcription/csharp/MeetingTranscription/appsettings.Development.json deleted file mode 100644 index 7d7d7f5ddd..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/appsettings.json b/samples/meetings-transcription/csharp/MeetingTranscription/appsettings.json deleted file mode 100644 index d6efcd0576..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/appsettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "MicrosoftAppId": "<>", - "MicrosoftAppPassword": "<>", - "MicrosoftAppTenantId": "<>", - "AppBaseUrl": "<>", - "GraphApiEndpoint": "https://graph.microsoft.com/beta", - "MicrosoftAppType": "SingleTenant" -} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/wwwroot/default.htm b/samples/meetings-transcription/csharp/MeetingTranscription/wwwroot/default.htm deleted file mode 100644 index f194923919..0000000000 --- a/samples/meetings-transcription/csharp/MeetingTranscription/wwwroot/default.htm +++ /dev/null @@ -1,420 +0,0 @@ - - - - - - - Meeting Transcription - - - - - -
-
-
-
MeetingTranscription Bot
-
-
-
-
-
Your bot is ready!
-
You can test your bot in the Bot Framework Emulator
- by connecting to http://localhost:3978/api/messages.
- -
Visit Azure - Bot Service to register your bot and add it to
- various channels. The bot's endpoint URL typically looks - like this:
-
https://your_bots_hostname/api/messages
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/README.md b/samples/meetings-transcription/csharp/README.md index 33953cdc8f..0b13436641 100644 --- a/samples/meetings-transcription/csharp/README.md +++ b/samples/meetings-transcription/csharp/README.md @@ -25,7 +25,7 @@ The Meeting Transcript Bot uses Microsoft Graph API to fetch and present meeting ## Interaction with app -![MeetingsTranscriptionGif](MeetingTranscription/Images/MeetingsTranscriptionGif.gif) +![MeetingsTranscriptionGif](meetings-transcription/Images/MeetingsTranscriptionGif.gif) ## Try it yourself - experience the App in your Microsoft Teams client Please find below demo manifest which is deployed on Microsoft Azure and you can try it yourself by uploading the app manifest (.zip file link below) to your teams and/or as a personal app. (Sideloading must be enabled for your tenant, [see steps here](https://docs.microsoft.com/microsoftteams/platform/concepts/build-and-test/prepare-your-o365-tenant#enable-custom-teams-apps-and-turn-on-custom-app-uploading)). @@ -64,70 +64,76 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool 1. Register a new application in the [Microsoft Entra ID – App Registrations](https://go.microsoft.com/fwlink/?linkid=2083908) portal. - - Register one Azure AD application in your tenant's directory: for the bot and tab app authentication. + - Register one Azure AD application in your tenant's directory: for the bot and tab app authentication. - - Log in to the Azure portal from your subscription, and go to the "App registrations" blade [here](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps). Ensure that you use a tenant where admin consent for API permissions can be provided. + - Log in to the Azure portal from your subscription, and go to the "App registrations" blade [here](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps). Ensure that you use a tenant where admin consent for API permissions can be provided. - - Click on "New registration", and create an Azure AD application. + - Click on "New registration", and create an Azure AD application. - - **Name:** The name of your Teams app - if you are following the template for a default deployment, we recommend "App catalog lifecycle". + - **Name:** The name of your Teams app - if you are following the template for a default deployment, we recommend "App catalog lifecycle". - - **Supported account types:** Select "Accounts in any organizational directory" + - **Supported account types:** Select "Accounts in any organizational directory" - - Leave the "Redirect URL" field blank. + - Leave the "Redirect URL" field blank. - - Click on the "Register" button. + - Click on the "Register" button. - - When the app is registered, you'll be taken to the app's "Overview" page. Copy the **Application (client) ID**; we will need it later. Verify that the "Supported account types" is set to **Multiple organizations**. + - When the app is registered, you'll be taken to the app's "Overview" page. Copy the **Application (client) ID**; we will need it later. Verify that the "Supported account types" is set to **Multiple organizations**. - - Navigate to **API Permissions**, and make sure to add the follow permissions: + - Navigate to **API Permissions**, and make sure to add the follow permissions: * Select Add a permission * Select Microsoft Graph -> Delegated permissions. * `User.Read` (enabled by default) * Click on Add permissions. Please make sure to grant the admin consent for the required permissions. - - On the side rail in the Manage section, navigate to the "Certificates & secrets" section. In the Client secrets section, click on "+ New client secret". Add a description for the secret and select Expires as "Never". Click "Add". + - On the side rail in the Manage section, navigate to the "Certificates & secrets" section. In the Client secrets section, click on "+ New client secret". Add a description for the secret and select Expires as "Never". Click "Add". - - Once the client secret is created, copy its **Value**, please take a note of the secret as it will be required later. + - Once the client secret is created, copy its **Value**, please take a note of the secret as it will be required later. - - At this point you have 3 unique values: - - Application (client) ID which will be later used during Azure bot creation - - Client secret for the bot which will be later used during Azure bot creation - - Directory (tenant) ID + - At this point you have 3 unique values: + - Application (client) ID which will be later used during Azure bot creation + - Client secret for the bot which will be later used during Azure bot creation + - Directory (tenant) ID We recommend that you copy these values into a text file, using an application like Notepad. We will need these values later. - - Under left menu, navigate to **API Permissions**, and make sure to add the following permissions of Microsoft Graph API > Application permissions: + - Under left menu, navigate to **API Permissions**, and make sure to add the following permissions of Microsoft Graph API > Application permissions: + - OnlineMeetings.Read.All + - OnlineMeetingTranscript.Read.All + - CallRecordings.Read.All + + - CallRecords.Read.All + Click on Add Permissions to commit your changes. - - If you are logged in as the Global Administrator, click on the Grant admin consent for %tenant-name% button to grant admin consent else, inform your admin to do the same through the portal or follow the steps provided here to create a link and send it to your admin for consent. + - If you are logged in as the Global Administrator, click on the Grant admin consent for %tenant-name% button to grant admin consent else, inform your admin to do the same through the portal or follow the steps provided here to create a link and send it to your admin for consent. - - Global Administrator can grant consent using following link: [https://login.microsoftonline.com/common/adminconsent?client_id=](https://login.microsoftonline.com/common/adminconsent?client_id=)<%appId%> + - Global Administrator can grant consent using following link: [https://login.microsoftonline.com/common/adminconsent?client_id=](https://login.microsoftonline.com/common/adminconsent?client_id=)<%appId%> 2. Setup for Bot -- Register a bot with Azure Bot Service, following the instructions [here](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration?view=azure-bot-service-3.0) +- Register a bot with Azure Bot Service, following the instructions [here](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/) -- Ensure that you've [enabled the Teams Channel](https://docs.microsoft.com/en-us/azure/bot-service/channel-connect-teams?view=azure-bot-service-4.0) +- Ensure that you've [enabled the Teams Channel](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/) - While registering the bot, use `https:///api/messages` as the messaging endpoint. > NOTE: When you create your bot you will create an App ID and App password - make sure you keep these for later. 3. Setup NGROK -1) Run ngrok - point to port 3978 +1) Run ngrok - point to port 5130 ```bash - ngrok http 3978 --host-header="localhost:3978" + ngrok http 5130 --host-header="localhost:5130" ``` Alternatively, you can also use the `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: ```bash - devtunnel host -p 3978 --allow-anonymous + devtunnel host -p 5130 --allow-anonymous ``` - If you are using Ngrok, once started you should see URL `https://41ed-abcd-e125.ngrok-free.app`. Copy it, this is your baseUrl that will used as endpoint for Azure bot and webhook. @@ -139,9 +145,10 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool git clone https://github.com/OfficeDev/Microsoft-Teams-Samples.git ``` -- Update the `appsettings.json` configuration for the bot to use the `MicrosoftAppId` and `MicrosoftAppPassword` and `MicrosoftAppTenantId` and `AppBaseUrl` and `UserId` (Note that the MicrosoftAppId is the AppId created in step 1 , the MicrosoftAppPassword is referred to as the "client secret" in step 1 and you can always create a new client secret anytime., MicrosoftAppTenantId is reffered to as Directory tenant Id in step 1, AppBaseUrl is the URL that you get in step 3 after running the tunnel, UserId of the user used while granting the policy in step 5). +- Update the `appsettings.Development.json` configuration for the bot to use the `ClientId` and `ClientSecret` and `TenantId` and `AppBaseUrl` and `UserId` (Note that the MicrosoftAppId is the AppId created in step 1 , the MicrosoftAppPassword is referred to as the "client secret" in step 1 and you can always create a new client secret anytime., MicrosoftAppTenantId is reffered to as Directory tenant Id in step 1, AppBaseUrl is the URL that you get in step 3 after running the tunnel, UserId of the user used while granting the policy in step 5). - Run the bot from a terminal or from Visual Studio: + A) From a terminal, navigate to `MeetingTranscription` ```bash @@ -152,9 +159,13 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool B) Or from Visual Studio - Launch Visual Studio + - File -> Open -> Project/Solution + - Navigate to `samples/meetings-transcription/csharp` folder + - Select `MeetingTranscription.csproj` file + - Press `F5` to run the project **NOTE: If you are not getting option to start transcript. Make sure it is enabled from [Teams Admin center](https://admin.teams.microsoft.com). Under `Meetings -> Meeting Policies -> Applied policy(Default is Global)-> Recording & Transcription -> Transcription`** @@ -166,8 +177,8 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool - Follow this link- [Configure application access policy](https://docs.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy) -- **Note**: Copy the User Id you used to granting the policy. You need while configuring the appsettings.json file. -![Policy](MeetingTranscription/Images/Policy.png) +- **Note**: Copy the User Id you used to granting the policy. You need while configuring the appsettings.Development.json file. +![Policy](meetings-transcription/Images/Policy.png) # RSC Enable Configuration @@ -215,7 +226,7 @@ Add the following permissions inside the **authorization** section of your `mani 6. Setup Manifest for Teams - __*This step is specific to Teams.*__ - - **Edit** the `manifest.json` contained in the ./appPackage folder to replace your Microsoft App Id (that was created when you registered your app registration earlier) *everywhere* you see the place holder string `{{Microsoft-App-Id}}` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`) + - **Edit** the `manifest.json` contained in the ./appPackage folder to replace your Microsoft App Id (that was created when you registered your app registration earlier) *everywhere* you see the place holder string `{{TEAMS_APP_ID}} and {{BOT_ID}}` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`) - **Edit** the `manifest.json` for `validDomains` and replace `{{domain-name}}` with base Url of your domain. E.g. if you are using ngrok it would be `https://1234.ngrok-free.app` then your domain-name will be `1234.ngrok-free.app` and if you are using dev tunnels then your domain will be like: `12345.devtunnels.ms`. - **Zip** up the contents of the `appPackage` folder to create a `manifest.zip` (Make sure that zip file does not contains any subfolder otherwise you will get error while uploading your .zip package) @@ -225,28 +236,28 @@ Add the following permissions inside the **authorization** section of your `mani - Go to your project directory, the ./appPackage folder, select the zip folder, and choose Open. - Select Add in the pop-up dialog box. Your app is uploaded to Teams. -**Note**: If you are facing any issue in your app, please uncomment [this](https://github.com/OfficeDev/Microsoft-Teams-Samples/blob/main/samples/meetings-transcription/csharp/MeetingTranscription/AdapterWithErrorHandler.cs#L23) line and put your debugger for local debug. + ## Running the sample. 1. Schedule the meeting and add Meeting Transcript Bot from `Apps` section in that particular scheduled meeting. -![Add Bot](MeetingTranscription/Images/1.AddMeetingTranscriptBot.PNG) +![Add Bot](meetings-transcription/Images/1.AddMeetingTranscriptBot.PNG) -![AddMeetingGroup](MeetingTranscription/Images/2.AddMeetingGroup.png) +![AddMeetingGroup](meetings-transcription/Images/2.AddMeetingGroup.png) -![JoinMeeting](MeetingTranscription/Images/3.JoinMeeting.png) +![JoinMeeting](meetings-transcription/Images/3.JoinMeeting.png) 2. Once meeting started, start the Transcript for the meeting. -![Start Transcript](MeetingTranscription/Images/4.StartTranscript.png) +![Start Transcript](meetings-transcription/Images/4.StartTranscript.png) 3. Once the transcription has started, you can see the live transcription it the meeting UI. -![Leave Meeting](MeetingTranscription/Images/5.LeaveMeeting.png) +![Leave Meeting](meetings-transcription/Images/5.LeaveMeeting.png) 4. Once the Meeting ended, Meeting Transcript Bot will sent a card having a button to open task module. -![Meeting Transcript Card](MeetingTranscription/Images/6.MeetingTranscriptCard.png) +![Meeting Transcript Card](meetings-transcription/Images/6.MeetingTranscriptCard.png) 5. After clicking on `View Transcript` button, you will see the recorded Transcript in the opened Task Module. -![Transcript Task Module](MeetingTranscription/Images/7.TranscriptTaskModule.png) +![Transcript Task Module](meetings-transcription/Images/7.TranscriptTaskModule.png) ## Interacting with the bot. - After uploading the manifest add the bot into meeting. @@ -261,13 +272,13 @@ To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](htt - [List Meeting Attendance Reports](https://docs.microsoft.com/en-us/graph/api/meetingattendancereport-list?view=graph-rest-1.0&tabs=http) - [List Attendance Records](https://docs.microsoft.com/en-us/graph/api/attendancerecord-list?view=graph-rest-1.0&tabs=http) - [Configure application access policy](https://docs.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy) -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Teams SDK Documentation](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/) +- [Bot Basics](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/) - [Azure Portal](https://portal.azure.com) -- [Add Authentication to Your Bot Via Azure Bot Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication?view=azure-bot-service-4.0&tabs=csharp) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Add Authentication to Your Bot Via Azure Bot Service](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/) +- [Activity processing](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/) +- [Azure Bot Service Introduction](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/) +- [Azure Bot Service Documentation](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-ai-library/) - [.NET Core CLI tools](https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x) - [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) - [Azure Portal](https://portal.azure.com) diff --git a/samples/meetings-transcription/csharp/meetings-transcription.slnx b/samples/meetings-transcription/csharp/meetings-transcription.slnx new file mode 100644 index 0000000000..27a21a6d57 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/.gitignore b/samples/meetings-transcription/csharp/meetings-transcription/.gitignore similarity index 74% rename from samples/meetings-transcription/csharp/MeetingTranscription/.gitignore rename to samples/meetings-transcription/csharp/meetings-transcription/.gitignore index 7466c01f9c..034334ff54 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/.gitignore +++ b/samples/meetings-transcription/csharp/meetings-transcription/.gitignore @@ -3,7 +3,6 @@ build appPackage/build env/.env.*.user env/.env.local -appsettings.Development.json .deployment # User-specific files @@ -22,4 +21,8 @@ bld/ [Ll]og/ # Notification local store -.notification.localstore.json \ No newline at end of file +.notification.localstore.json +.notification.playgroundstore.json + +# devTools +devTools/ \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Config.cs b/samples/meetings-transcription/csharp/meetings-transcription/Config.cs new file mode 100644 index 0000000000..781cc424b0 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Config.cs @@ -0,0 +1,26 @@ +namespace meetings_transcription +{ + public class ConfigOptions + { + public TeamsConfigOptions Teams { get; set; } + public AzureConfigOptions Azure { get; set; } + } + + public class TeamsConfigOptions + { + public string BotType { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string TenantId { get; set; } + } + + public class AzureConfigOptions + { + public string MicrosoftAppId { get; set; } + public string MicrosoftAppPassword { get; set; } + public string MicrosoftAppTenantId { get; set; } + public string AppBaseUrl { get; set; } + public string UserId { get; set; } + public string GraphApiEndpoint { get; set; } + } +} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Controllers/Controller.cs b/samples/meetings-transcription/csharp/meetings-transcription/Controllers/Controller.cs new file mode 100644 index 0000000000..530af58da8 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Controllers/Controller.cs @@ -0,0 +1,78 @@ +using Microsoft.Teams.Api.Activities; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Activities; +using Microsoft.Teams.Apps.Annotations; +using Microsoft.Extensions.Options; +using meetings_transcription.Models.Configuration; +using meetings_transcription.Helpers; +using meetings_transcription.Services; +using meetings_transcription.Models; +using System.Collections.Concurrent; +using Newtonsoft.Json; + +namespace meetings_transcription.Controllers +{ + [TeamsController] + public class Controller + { + /// + /// Helper instance to make graph calls. + /// + private readonly GraphHelper graphHelper; + + /// + /// Stores the Azure configuration values. + /// + private readonly IOptions azureSettings; + + /// + /// Store details of meeting transcript. + /// + private readonly ConcurrentDictionary transcriptsDictionary; + + /// + /// Instance of card factory to create adaptive cards. + /// + private readonly ICardFactory cardFactory; + + /// + /// Creates bot instance. + /// + /// Stores the Azure configuration values. + /// Store details of meeting transcript. + /// Instance of card factory to create adaptive cards. + public Controller(IOptions azureSettings, ConcurrentDictionary transcriptsDictionary, ICardFactory cardFactory) + { + this.transcriptsDictionary = transcriptsDictionary; + this.azureSettings = azureSettings; + this.graphHelper = new GraphHelper(azureSettings); + this.cardFactory = cardFactory; + } + + /// + /// Activity handler for on message activity. + /// + [Message] + public async Task OnMessage([Context] MessageActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + var replyText = $"Echo: {activity.Text}"; + await client.Send(replyText); + } + + /// + /// Activity handler for conversation members added. + /// + [Conversation.MembersAdded] + public async Task OnMembersAdded(IContext context) + { + var welcomeText = "How can I help you today?"; + foreach (var member in context.Activity.MembersAdded) + { + if (member.Id != context.Activity.Recipient.Id) + { + await context.Send(welcomeText); + } + } + } + } +} diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Controllers/HomeController.cs b/samples/meetings-transcription/csharp/meetings-transcription/Controllers/HomeController.cs new file mode 100644 index 0000000000..81506c37f9 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Controllers/HomeController.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +using meetings_transcription.Helpers; +using meetings_transcription.Models.Configuration; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace meetings_transcription.Controllers +{ + public class HomeController : Microsoft.AspNetCore.Mvc.Controller + { + private readonly ConcurrentDictionary transcriptsDictionary; + + /// + /// Stores the Azure configuration values. + /// + private readonly IOptions azureSettings; + + /// + /// Stores the Azure configuration values. + /// + private readonly GraphHelper graphHelper; + + public HomeController (ConcurrentDictionary transcriptsDictionary, IOptions azureSettings) + { + this.transcriptsDictionary = transcriptsDictionary; + this.azureSettings = azureSettings; + graphHelper = new(azureSettings); + } + + /// + /// Returns view to be displayed in Task Module. + /// + /// Id of the meeting. + /// + public async Task Index([FromQuery] string meetingId) + { + ViewBag.Transcripts = "Transcript not found."; + + if (!string.IsNullOrEmpty(meetingId)) + { + // Try to find in cache using the ENCODED meeting ID (key used by middleware) + var isFound = transcriptsDictionary.TryGetValue(meetingId, out string transcripts); + if (isFound) + { + ViewBag.Transcripts = transcripts; + } + else + { + // Fallback: Try to fetch from Graph API if not in cache + // Use the configured UserId or AzureAdUserId + var userId = this.azureSettings.Value.AzureAdUserId ?? this.azureSettings.Value.UserId; + + // If userId is a UPN, need to resolve it first + if (!string.IsNullOrEmpty(userId) && userId.Contains("@")) + { + userId = await this.graphHelper.GetUserIdFromUpnAsync(userId); + } + + if (!string.IsNullOrEmpty(userId)) + { + // Use the ENCODED meeting ID when calling Graph API + var result = await this.graphHelper.GetMeetingTranscriptionsAsync(meetingId, userId); + if (!string.IsNullOrEmpty(result)) + { + // Store with ENCODED meeting ID as key (to match middleware) + result = result.Replace(" result); + ViewBag.Transcripts = result; + } + } + } + } + return View(); + } + } +} diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Helpers/GraphHelper.cs b/samples/meetings-transcription/csharp/meetings-transcription/Helpers/GraphHelper.cs new file mode 100644 index 0000000000..f36e32bc72 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Helpers/GraphHelper.cs @@ -0,0 +1,298 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace meetings_transcription.Helpers +{ + using Microsoft.Extensions.Options; + using Microsoft.Identity.Client; + using meetings_transcription.Models.Configuration; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http.Headers; + using System.Threading.Tasks; + using System.Net.Http; + using meetings_transcription.Models; + using Newtonsoft.Json.Linq; + + public class GraphHelper + { + private readonly IOptions azureSettings; + + public GraphHelper(IOptions azureSettings) + { + this.azureSettings = azureSettings; + } + + public async Task GetToken() + { + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(this.azureSettings.Value.MicrosoftAppId) + .WithClientSecret(this.azureSettings.Value.MicrosoftAppPassword) + .WithAuthority($"https://login.microsoftonline.com/{this.azureSettings.Value.MicrosoftAppTenantId}") + .WithRedirectUri("https://daemon") + .Build(); + + string[] scopes = new string[] { "https://graph.microsoft.com/.default" }; + var result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); + + return result.AccessToken; + } + + public async Task GetMeetingTranscriptionsAsync(string encodedMeetingId, string userId = null, DateTime? meetingEndTime = null) + { + try + { + string accessToken = await GetToken(); + var client = new HttpClient(); + + // Decode the base64 meeting ID to get the thread ID + var decodedBytes = Convert.FromBase64String(encodedMeetingId); + var decodedId = System.Text.Encoding.UTF8.GetString(decodedBytes); + + // Extract the thread ID (format: 19:meeting_@thread.v2) + var threadMatch = System.Text.RegularExpressions.Regex.Match(decodedId, @"19:meeting_[^@]+@thread\.v2"); + if (!threadMatch.Success) + { + return string.Empty; + } + + string threadId = threadMatch.Value; + + // Query call records to find the matching call + var callRecordsEndpoint = "https://graph.microsoft.com/v1.0/communications/callRecords"; + var callRecordsReq = new HttpRequestMessage(HttpMethod.Get, callRecordsEndpoint); + callRecordsReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + var callRecordsResponse = await client.SendAsync(callRecordsReq); + if (!callRecordsResponse.IsSuccessStatusCode) + { + return string.Empty; + } + + var callRecordsContent = await callRecordsResponse.Content.ReadAsStringAsync(); + var callRecords = JObject.Parse(callRecordsContent)["value"]; + + if (callRecords == null || !callRecords.Any()) + { + return string.Empty; + } + + // Search through call records to find the one matching our thread ID + foreach (var record in callRecords) + { + string callId = record["id"]?.ToString(); + if (string.IsNullOrEmpty(callId)) continue; + + // Get full call record details + var callDetailsEndpoint = $"https://graph.microsoft.com/v1.0/communications/callRecords/{callId}"; + var callDetailsReq = new HttpRequestMessage(HttpMethod.Get, callDetailsEndpoint); + callDetailsReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + var callDetailsResponse = await client.SendAsync(callDetailsReq); + if (!callDetailsResponse.IsSuccessStatusCode) continue; + + var callDetailsContent = await callDetailsResponse.Content.ReadAsStringAsync(); + var callDetails = JObject.Parse(callDetailsContent); + + // Check if the joinWebUrl contains our thread ID + string joinUrl = callDetails["joinWebUrl"]?.ToString(); + if (string.IsNullOrEmpty(joinUrl)) continue; + + string decodedUrl = Uri.UnescapeDataString(joinUrl); + string threadIdWithoutSuffix = threadId.Replace("@thread.v2", ""); + + if (decodedUrl.Contains(threadIdWithoutSuffix)) + { + // Get organizer ID + string organizerId = callDetails["organizer"]?["user"]?["id"]?.ToString(); + if (string.IsNullOrEmpty(organizerId)) + { + continue; + } + + // Query organizer's meetings with joinWebUrl filter + string encodedJoinUrl = Uri.EscapeDataString(joinUrl); + var organizerMeetingsEndpoint = $"{this.azureSettings.Value.GraphApiEndpoint}/users/{organizerId}/onlineMeetings?$filter=JoinWebUrl eq '{encodedJoinUrl}'"; + var organizerMeetingsReq = new HttpRequestMessage(HttpMethod.Get, organizerMeetingsEndpoint); + organizerMeetingsReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + var meetingsResponse = await client.SendAsync(organizerMeetingsReq); + if (!meetingsResponse.IsSuccessStatusCode) + { + continue; + } + + var meetingsContent = await meetingsResponse.Content.ReadAsStringAsync(); + var meetings = JObject.Parse(meetingsContent)["value"]; + + if (meetings == null || !meetings.Any()) + { + continue; + } + + // Find the meeting that matches the joinWebUrl exactly + var matchedMeeting = meetings.FirstOrDefault(m => m["joinWebUrl"]?.ToString() == joinUrl); + if (matchedMeeting == null) + { + continue; + } + + string graphMeetingId = matchedMeeting["id"]?.ToString(); + + // Get transcripts using the correct meeting ID + var transcriptsEndpoint = $"{this.azureSettings.Value.GraphApiEndpoint}/users/{organizerId}/onlineMeetings/{graphMeetingId}/transcripts"; + var transcriptsReq = new HttpRequestMessage(HttpMethod.Get, transcriptsEndpoint); + transcriptsReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + var transcriptsResponse = await client.SendAsync(transcriptsReq); + if (!transcriptsResponse.IsSuccessStatusCode) + { + return string.Empty; + } + + var transcriptsContent = await transcriptsResponse.Content.ReadAsStringAsync(); + var transcriptsArray = JObject.Parse(transcriptsContent)["value"] as JArray; + + var transcripts = transcriptsArray?.ToObject>(); + + if (transcripts != null && transcripts.Count > 0) + { + // CRITICAL: Filter by meeting end time if provided + Transcripts targetTranscript = null; + + if (meetingEndTime.HasValue) + { + // Tolerance: only consider transcripts within 5 minutes of meeting end time + const double toleranceMinutes = 5.0; + + // Find transcripts within tolerance + var matchingTranscripts = transcripts + .Where(t => t.EndDateTime.HasValue && + Math.Abs((t.EndDateTime.Value - meetingEndTime.Value).TotalMinutes) <= toleranceMinutes) + .ToList(); + + if (matchingTranscripts.Any()) + { + // Select the closest match + targetTranscript = matchingTranscripts + .OrderBy(t => Math.Abs((t.EndDateTime.Value - meetingEndTime.Value).TotalSeconds)) + .FirstOrDefault(); + } + else + { + // Return empty to trigger retry - DO NOT use fallback + return string.Empty; + } + } + else + { + // No end time provided - use most recent + targetTranscript = transcripts.OrderByDescending(t => t.CreatedDateTime).FirstOrDefault(); + } + + // If we have a target transcript, fetch its content + if (targetTranscript != null) + { + var transcriptId = targetTranscript.Id; + var getTranscriptEndpoint = $"{transcriptsEndpoint}/{transcriptId}/content?$format=text/vtt"; + + var getTranscriptReq = new HttpRequestMessage(HttpMethod.Get, getTranscriptEndpoint); + getTranscriptReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + var transcriptResponse = await client.SendAsync(getTranscriptReq); + + if (transcriptResponse.IsSuccessStatusCode) + { + var transcriptContent = await transcriptResponse.Content.ReadAsStringAsync(); + return transcriptContent; + } + } + } + + break; + } + } + + return string.Empty; + } + catch (Exception) + { + return string.Empty; + } + } + + public string GetMeetingOrganizerFromUserId(string userId) + { + try + { + if (!string.IsNullOrEmpty(userId)) + { + return userId; + } + + return string.Empty; + } + catch (Exception) + { + return string.Empty; + } + } + + public string DecodeMeetingId(string encodedMeetingId) + { + try + { + var decodedBytes = Convert.FromBase64String(encodedMeetingId); + var decodedString = System.Text.Encoding.UTF8.GetString(decodedBytes); + + var cleanedId = decodedString; + + if (cleanedId.Contains("#")) + { + var parts = cleanedId.Split('#'); + if (parts.Length >= 2) + { + cleanedId = parts[1]; + } + } + + return cleanedId; + } + catch (Exception) + { + return encodedMeetingId; + } + } + + public async Task GetUserIdFromUpnAsync(string userPrincipalName) + { + try + { + string accessToken = await GetToken(); + + var getUserEndpoint = $"{this.azureSettings.Value.GraphApiEndpoint}/users/{userPrincipalName}?$select=id"; + var getUserReq = new HttpRequestMessage(HttpMethod.Get, getUserEndpoint); + getUserReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + var client = new HttpClient(); + var response = await client.SendAsync(getUserReq); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + var user = JObject.Parse(content); + var userId = user["id"]?.ToString(); + return userId ?? string.Empty; + } + else + { + return string.Empty; + } + } + catch (Exception) + { + return string.Empty; + } + } + } +} diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Helpers/GraphHelper_New.cs b/samples/meetings-transcription/csharp/meetings-transcription/Helpers/GraphHelper_New.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/1.AddMeetingTranscriptBot.PNG b/samples/meetings-transcription/csharp/meetings-transcription/Images/1.AddMeetingTranscriptBot.PNG similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/1.AddMeetingTranscriptBot.PNG rename to samples/meetings-transcription/csharp/meetings-transcription/Images/1.AddMeetingTranscriptBot.PNG diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/2.AddMeetingGroup.png b/samples/meetings-transcription/csharp/meetings-transcription/Images/2.AddMeetingGroup.png similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/2.AddMeetingGroup.png rename to samples/meetings-transcription/csharp/meetings-transcription/Images/2.AddMeetingGroup.png diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/3.JoinMeeting.png b/samples/meetings-transcription/csharp/meetings-transcription/Images/3.JoinMeeting.png similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/3.JoinMeeting.png rename to samples/meetings-transcription/csharp/meetings-transcription/Images/3.JoinMeeting.png diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/4.StartTranscript.png b/samples/meetings-transcription/csharp/meetings-transcription/Images/4.StartTranscript.png similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/4.StartTranscript.png rename to samples/meetings-transcription/csharp/meetings-transcription/Images/4.StartTranscript.png diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/5.LeaveMeeting.png b/samples/meetings-transcription/csharp/meetings-transcription/Images/5.LeaveMeeting.png similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/5.LeaveMeeting.png rename to samples/meetings-transcription/csharp/meetings-transcription/Images/5.LeaveMeeting.png diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/6.MeetingTranscriptCard.png b/samples/meetings-transcription/csharp/meetings-transcription/Images/6.MeetingTranscriptCard.png similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/6.MeetingTranscriptCard.png rename to samples/meetings-transcription/csharp/meetings-transcription/Images/6.MeetingTranscriptCard.png diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/7.TranscriptTaskModule.png b/samples/meetings-transcription/csharp/meetings-transcription/Images/7.TranscriptTaskModule.png similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/7.TranscriptTaskModule.png rename to samples/meetings-transcription/csharp/meetings-transcription/Images/7.TranscriptTaskModule.png diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/MeetingsTranscriptionGif.gif b/samples/meetings-transcription/csharp/meetings-transcription/Images/MeetingsTranscriptionGif.gif similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/MeetingsTranscriptionGif.gif rename to samples/meetings-transcription/csharp/meetings-transcription/Images/MeetingsTranscriptionGif.gif diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Images/Policy.png b/samples/meetings-transcription/csharp/meetings-transcription/Images/Policy.png similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Images/Policy.png rename to samples/meetings-transcription/csharp/meetings-transcription/Images/Policy.png diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Middleware/MeetingEventInterceptor.cs b/samples/meetings-transcription/csharp/meetings-transcription/Middleware/MeetingEventInterceptor.cs new file mode 100644 index 0000000000..2a232e6ba1 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Middleware/MeetingEventInterceptor.cs @@ -0,0 +1,390 @@ +using System.Text; +using System.Text.Json; +using Newtonsoft.Json.Linq; +using meetings_transcription.Helpers; +using meetings_transcription.Services; +using meetings_transcription.Models; +using meetings_transcription.Models.Configuration; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; + +namespace meetings_transcription.Middleware +{ + /// + /// Middleware to intercept meeting events and handle them BEFORE Teams SDK deserialization. + /// This prevents JSON deserialization errors for meeting events with incomplete payloads. + /// + public class MeetingEventInterceptor + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public MeetingEventInterceptor(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync( + HttpContext context, + IOptions azureSettings, + ConcurrentDictionary transcriptsDictionary, + ICardFactory cardFactory) + { + // Only intercept POST requests to /api/messages + if (context.Request.Method == "POST" && context.Request.Path.StartsWithSegments("/api/messages")) + { + context.Request.EnableBuffering(); + + string body; + using (var reader = new StreamReader( + context.Request.Body, + encoding: Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + bufferSize: 1024, + leaveOpen: true)) + { + body = await reader.ReadToEndAsync(); + context.Request.Body.Position = 0; + } + + try + { + var jsonActivity = JObject.Parse(body); + var activityType = jsonActivity["type"]?.ToString(); + var activityName = jsonActivity["name"]?.ToString(); + + _logger.LogInformation($"Received activity: type={activityType}, name={activityName}"); + + // Intercept meeting events + if (activityType == "event") + { + if (activityName == "application/vnd.microsoft.meetingEnd") + { + _logger.LogInformation("Intercepting meeting end event"); + await HandleMeetingEndEvent(jsonActivity, context, azureSettings, transcriptsDictionary); + return; // Don't pass to next middleware + } + else if (activityName == "application/vnd.microsoft.meetingStart") + { + _logger.LogInformation("Intercepting meeting start event"); + // Return 200 OK to acknowledge + context.Response.StatusCode = 200; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync("{}"); + return; // Don't pass to next middleware + } + } + // Intercept task/fetch + else if (activityType == "invoke" && activityName == "task/fetch") + { + _logger.LogInformation("Intercepting task/fetch"); + await HandleTaskFetch(jsonActivity, context, azureSettings); + return; // Don't pass to next middleware + } + } + catch (JsonException ex) + { + _logger.LogWarning($"Failed to parse activity JSON: {ex.Message}"); + } + catch (Exception ex) + { + _logger.LogError($"Error in meeting event interceptor: {ex.Message}"); + } + } + + // Continue to next middleware + await _next(context); + } + + private async Task HandleMeetingEndEvent( + JObject jsonActivity, + HttpContext context, + IOptions azureSettings, + ConcurrentDictionary transcriptsDictionary) + { + try + { + // Extract meeting ID from channelData + var channelData = jsonActivity["channelData"]; + var meetingId = channelData?["meeting"]?["id"]?.ToString(); + + if (string.IsNullOrEmpty(meetingId)) + { + _logger.LogWarning("Meeting ID not found in channel data"); + context.Response.StatusCode = 200; + await context.Response.WriteAsync("{}"); + return; + } + + _logger.LogInformation($"Processing meeting end: encoded meetingId={meetingId}"); + + // Log full event payload for debugging + _logger.LogInformation($"?? Full event payload: {jsonActivity.ToString(Newtonsoft.Json.Formatting.None)}"); + + // Extract meeting end time for transcript filtering + DateTime? meetingEndTime = null; + var endTimeValue = jsonActivity["value"]?["EndTime"]?.ToString(); + if (!string.IsNullOrEmpty(endTimeValue) && DateTime.TryParse(endTimeValue, out var parsedEndTime)) + { + meetingEndTime = parsedEndTime; + _logger.LogInformation($"Meeting end time extracted: {meetingEndTime}"); + } + + // MANDATORY DELAY: Wait 20 seconds after meeting ends before attempting to fetch transcript + // This gives Microsoft's servers time to begin processing the transcript + _logger.LogInformation("Waiting 20 seconds after meeting end before fetching transcript..."); + await Task.Delay(TimeSpan.FromSeconds(20)); + _logger.LogInformation("Initial delay complete. Starting transcript fetch attempts."); + + // Get meeting transcription + var graphHelper = new GraphHelper(azureSettings); + + // Retry logic: Transcript generation takes time after meeting ends + // Add initial delay to give Microsoft servers time to process + string result = null; + int maxRetries = 6; + int[] delaySeconds = { 10, 20, 40, 70, 110, 150 }; // Start with 10s delay, then progressive increases + + for (int attempt = 0; attempt < maxRetries; attempt++) + { + _logger.LogInformation($"Waiting {delaySeconds[attempt]} seconds before attempt {attempt + 1}/{maxRetries}..."); + await Task.Delay(TimeSpan.FromSeconds(delaySeconds[attempt])); + + _logger.LogInformation($"Attempt {attempt + 1}/{maxRetries}: Fetching transcript using CallRecords API"); + result = await graphHelper.GetMeetingTranscriptionsAsync(meetingId, null, meetingEndTime); + + if (!string.IsNullOrEmpty(result)) + { + _logger.LogInformation($"? Transcript found on attempt {attempt + 1}"); + break; + } + + if (attempt < maxRetries - 1) + { + _logger.LogInformation($"Transcript not ready yet (attempt {attempt + 1}/{maxRetries}). Will retry in {delaySeconds[attempt + 1]} seconds."); + } + else + { + _logger.LogInformation($"Transcript not ready after {maxRetries} attempts."); + } + } + + if (!string.IsNullOrEmpty(result)) + { + // Clean up result + result = result.Replace(" result); + _logger.LogInformation($"? Transcript stored for meeting {meetingId} ({result.Length} characters)"); + + // Extract conversation details for posting card + var conversationId = jsonActivity["conversation"]?["id"]?.ToString(); + var serviceUrl = jsonActivity["serviceUrl"]?.ToString(); + var tenantId = channelData?["tenant"]?["id"]?.ToString(); + + if (!string.IsNullOrEmpty(conversationId) && !string.IsNullOrEmpty(serviceUrl)) + { + _logger.LogInformation($"?? Attempting to post Adaptive Card to conversation {conversationId}"); + await PostTranscriptCardToChat( + conversationId, + serviceUrl, + tenantId, + meetingId, + azureSettings.Value.MicrosoftAppId, + azureSettings.Value.MicrosoftAppPassword); + } + else + { + _logger.LogWarning($"?? Missing conversation details - cannot post card"); + _logger.LogInformation($"?? Users can view the transcript through the bot's task module interface"); + } + } + else + { + _logger.LogWarning($"?? No transcript found after {maxRetries} attempts"); + } + + // Acknowledge the event + context.Response.StatusCode = 200; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync("{}"); + } + catch (Exception ex) + { + _logger.LogError($"Error handling meeting end event: {ex.Message}"); + _logger.LogError($"Stack trace: {ex.StackTrace}"); + context.Response.StatusCode = 500; + await context.Response.WriteAsync($"{{\"error\":\"{ex.Message}\"}}"); + } + } + + private async Task PostTranscriptCardToChat( + string conversationId, + string serviceUrl, + string tenantId, + string meetingId, + string appId, + string appPassword) + { + try + { + using var httpClient = new HttpClient(); + + // Get OAuth token for Bot Framework API (use tenant-specific endpoint) + _logger.LogInformation("?? Requesting Bot Framework OAuth token..."); + _logger.LogInformation($"?? Using tenant ID: {tenantId}"); + var tokenResponse = await httpClient.PostAsync( + $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token", + new FormUrlEncodedContent(new[] + { + new KeyValuePair("grant_type", "client_credentials"), + new KeyValuePair("client_id", appId), + new KeyValuePair("client_secret", appPassword), + new KeyValuePair("scope", "https://api.botframework.com/.default") + })); + + if (!tokenResponse.IsSuccessStatusCode) + { + var errorContent = await tokenResponse.Content.ReadAsStringAsync(); + _logger.LogError($"? Failed to obtain Bot Framework token: {tokenResponse.StatusCode} - {errorContent}"); + return; + } + + var tokenJson = await tokenResponse.Content.ReadAsStringAsync(); + var token = JObject.Parse(tokenJson)["access_token"]?.ToString(); + + if (string.IsNullOrEmpty(token)) + { + _logger.LogError("? Token response did not contain access_token"); + return; + } + + _logger.LogInformation("? Bot Framework token obtained successfully"); + + // Create Adaptive Card payload + var cardPayload = new + { + type = "message", + from = new { id = appId }, + conversation = new { id = conversationId }, + channelData = new { tenant = new { id = tenantId } }, + attachments = new[] + { + new + { + contentType = "application/vnd.microsoft.card.adaptive", + content = new + { + type = "AdaptiveCard", + version = "1.5", + body = new[] + { + new + { + type = "TextBlock", + text = "Here is the last transcript details of the meeting.", + weight = "Bolder", + size = "Large" + } + }, + actions = new[] + { + new + { + type = "Action.Submit", + title = "View Transcript", + data = new + { + msteams = new { type = "task/fetch" }, + meetingId = meetingId + } + } + } + } + } + } + }; + + // Send message to conversation + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + + var conversationUrl = $"{serviceUrl.TrimEnd('/')}/v3/conversations/{conversationId}/activities"; + _logger.LogInformation($"?? Posting card to: {conversationUrl}"); + + var response = await httpClient.PostAsJsonAsync(conversationUrl, cardPayload); + + if (response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogInformation($"? Adaptive Card posted successfully to chat for meeting {meetingId}"); + _logger.LogInformation($"?? Response: {responseContent}"); + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"? Failed to post card: {response.StatusCode} - {errorContent}"); + } + } + catch (Exception ex) + { + _logger.LogError($"? Error posting card to chat: {ex.Message}"); + _logger.LogError($"Stack trace: {ex.StackTrace}"); + } + } + + private async Task HandleTaskFetch( + JObject jsonActivity, + HttpContext context, + IOptions azureSettings) + { + try + { + // Extract meeting ID from value.data.meetingId + var meetingId = jsonActivity["value"]?["data"]?["meetingId"]?.ToString(); + + var taskModuleUrl = !string.IsNullOrEmpty(meetingId) + ? $"{azureSettings.Value.AppBaseUrl}/home?meetingId={meetingId}" + : $"{azureSettings.Value.AppBaseUrl}/home"; + + var response = new + { + task = new + { + type = "continue", + value = new + { + title = "Meeting Transcript", + height = 600, + width = 600, + url = taskModuleUrl + } + } + }; + + context.Response.StatusCode = 200; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync(response); + } + catch (Exception ex) + { + _logger.LogError($"Error handling task/fetch: {ex.Message}"); + context.Response.StatusCode = 500; + await context.Response.WriteAsync($"{{\"error\":\"{ex.Message}\"}}"); + } + } + } + + /// + /// Extension method to register the middleware. + /// + public static class MeetingEventInterceptorExtensions + { + public static IApplicationBuilder UseMeetingEventInterceptor(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Models/Attachment.cs b/samples/meetings-transcription/csharp/meetings-transcription/Models/Attachment.cs new file mode 100644 index 0000000000..7d6c33ba9e --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Models/Attachment.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace meetings_transcription.Models +{ + /// + /// Represents an attachment for Teams messages. + /// + public class Attachment + { + /// + /// Gets or sets the content type of the attachment. + /// + public string ContentType { get; set; } + + /// + /// Gets or sets the content of the attachment. + /// + public object Content { get; set; } + } +} diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Models/Configuration/AzureSettings.cs b/samples/meetings-transcription/csharp/meetings-transcription/Models/Configuration/AzureSettings.cs similarity index 71% rename from samples/meetings-transcription/csharp/MeetingTranscription/Models/Configuration/AzureSettings.cs rename to samples/meetings-transcription/csharp/meetings-transcription/Models/Configuration/AzureSettings.cs index 1b7f835c38..03430f7b1b 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Models/Configuration/AzureSettings.cs +++ b/samples/meetings-transcription/csharp/meetings-transcription/Models/Configuration/AzureSettings.cs @@ -1,8 +1,8 @@ -// +// // Copyright (c) Microsoft. All rights reserved. // -namespace MeetingTranscription.Models.Configuration +namespace meetings_transcription.Models.Configuration { public class AzureSettings { @@ -27,10 +27,15 @@ public class AzureSettings public string AppBaseUrl { get; set; } /// - /// Id of User for which policy is granted. + /// Id of User for which policy is granted (UPN/email). /// public string UserId { get; set; } + /// + /// Azure AD User ID (GUID) - if provided, skips UPN resolution. + /// + public string AzureAdUserId { get; set; } + /// /// Graph API endpoint. /// diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Models/TaskModuleModels.cs b/samples/meetings-transcription/csharp/meetings-transcription/Models/TaskModuleModels.cs new file mode 100644 index 0000000000..f37b77de50 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Models/TaskModuleModels.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace meetings_transcription.Models +{ + /// + /// Task module response. + /// + public class TaskModuleResponse + { + /// + /// Gets or sets the task. + /// + public TaskModuleContinueResponse Task { get; set; } + } + + /// + /// Task module continue response. + /// + public class TaskModuleContinueResponse + { + /// + /// Gets or sets the type. + /// + public string Type { get; set; } + + /// + /// Gets or sets the value. + /// + public TaskModuleTaskInfo Value { get; set; } + } + + /// + /// Task module task info. + /// + public class TaskModuleTaskInfo + { + /// + /// Gets or sets the URL. + /// + public string Url { get; set; } + + /// + /// Gets or sets the height. + /// + public int Height { get; set; } + + /// + /// Gets or sets the width. + /// + public int Width { get; set; } + + /// + /// Gets or sets the title. + /// + public string Title { get; set; } + } +} diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Models/Transcripts.cs b/samples/meetings-transcription/csharp/meetings-transcription/Models/Transcripts.cs similarity index 61% rename from samples/meetings-transcription/csharp/MeetingTranscription/Models/Transcripts.cs rename to samples/meetings-transcription/csharp/meetings-transcription/Models/Transcripts.cs index 1901e951ae..45223ae334 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Models/Transcripts.cs +++ b/samples/meetings-transcription/csharp/meetings-transcription/Models/Transcripts.cs @@ -1,8 +1,8 @@ -// +// // Copyright (c) Microsoft. All rights reserved. // -namespace MeetingTranscription.Models +namespace meetings_transcription.Models { using System; @@ -17,5 +17,10 @@ public class Transcripts /// Created date of trancript. ///
public DateTime CreatedDateTime { get; set; } + + /// + /// End date and time of transcript. + /// + public DateTime? EndDateTime { get; set; } } } diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Program.cs b/samples/meetings-transcription/csharp/meetings-transcription/Program.cs new file mode 100644 index 0000000000..abdd7a1eff --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Program.cs @@ -0,0 +1,108 @@ +using meetings_transcription; +using meetings_transcription.Controllers; +using meetings_transcription.Models.Configuration; +using meetings_transcription.Services; +using meetings_transcription.Helpers; +using meetings_transcription.Middleware; +using Azure.Core; +using Azure.Identity; +using Microsoft.Teams.Api.Auth; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Extensions; +using Microsoft.Teams.Common.Http; +using Microsoft.Teams.Plugins.AspNetCore.Extensions; +using System.Collections.Concurrent; + +var builder = WebApplication.CreateBuilder(args); +var config = builder.Configuration.Get(); + +// Adds application configuration settings to specified IServiceCollection. +builder.Services.AddOptions() +.Configure((botOptions, configuration) => +{ + botOptions.MicrosoftAppId = configuration.GetValue("Azure:MicrosoftAppId"); + botOptions.MicrosoftAppPassword = configuration.GetValue("Azure:MicrosoftAppPassword"); + botOptions.MicrosoftAppTenantId = configuration.GetValue("Azure:MicrosoftAppTenantId"); + botOptions.AppBaseUrl = configuration.GetValue("Azure:AppBaseUrl"); + botOptions.UserId = configuration.GetValue("Azure:UserId"); + botOptions.GraphApiEndpoint = configuration.GetValue("Azure:GraphApiEndpoint"); +}); + +builder.Services.AddHttpClient().AddControllers().AddNewtonsoftJson(options => +{ + // Configure Newtonsoft.Json to handle missing properties gracefully + options.SerializerSettings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Ignore; + options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; +}); + +// Configure System.Text.Json for Teams SDK +builder.Services.Configure(options => +{ + options.SerializerOptions.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; + options.SerializerOptions.PropertyNameCaseInsensitive = true; +}); + +// Creates Singleton Card Factory. +builder.Services.AddSingleton(); + +// Create a global hashset for our save task details +builder.Services.AddSingleton>(); + +Func> createTokenFactory = async (string[] scopes, string? tenantId) => +{ + var clientId = config.Teams.ClientId; + + var managedIdentityCredential = new ManagedIdentityCredential(clientId); + var tokenRequestContext = new TokenRequestContext(scopes, tenantId: tenantId); + var accessToken = await managedIdentityCredential.GetTokenAsync(tokenRequestContext); + + return new TokenResponse + { + TokenType = "Bearer", + AccessToken = accessToken.Token, + }; +}; +var appBuilder = App.Builder(); + +if (config.Teams.BotType == "UserAssignedMsi") +{ + appBuilder.AddCredentials(new TokenCredentials( + config.Teams.ClientId ?? string.Empty, + async (tenantId, scopes) => + { + return await createTokenFactory(scopes, tenantId); + } + )); +} + +builder.Services.AddSingleton(); +builder.AddTeams(appBuilder); +builder.Services.AddMvc().AddSessionStateTempDataProvider(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} + +app.UseDefaultFiles() + .UseStaticFiles() + .UseWebSockets() + .UseRouting() + .UseAuthorization(); + +// Intercept meeting events BEFORE Teams SDK processes them +app.UseMeetingEventInterceptor(); + +app.UseTeams(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); +}); + +app.Run(); \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/meetings-transcription/Properties/launchSettings.json b/samples/meetings-transcription/csharp/meetings-transcription/Properties/launchSettings.json new file mode 100644 index 0000000000..3572a7a03f --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "profiles": { + // Debug project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Playground", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.playgroundstore.json", + "UPDATE_TEAMS_APP": "false" + }, + "hotReloadProfile": "aspnetcore" + }, + // Debug project within Teams + "Start Project": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + }, + } +} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Resources/NotFoundCard.json b/samples/meetings-transcription/csharp/meetings-transcription/Resources/NotFoundCard.json similarity index 99% rename from samples/meetings-transcription/csharp/MeetingTranscription/Resources/NotFoundCard.json rename to samples/meetings-transcription/csharp/meetings-transcription/Resources/NotFoundCard.json index 8c301442e8..c22a954f8b 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Resources/NotFoundCard.json +++ b/samples/meetings-transcription/csharp/meetings-transcription/Resources/NotFoundCard.json @@ -10,4 +10,4 @@ "size": "Large" } ] -} \ No newline at end of file +} diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Resources/TranscriptCard.json b/samples/meetings-transcription/csharp/meetings-transcription/Resources/TranscriptCard.json similarity index 100% rename from samples/meetings-transcription/csharp/MeetingTranscription/Resources/TranscriptCard.json rename to samples/meetings-transcription/csharp/meetings-transcription/Resources/TranscriptCard.json diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Services/CardFactory.cs b/samples/meetings-transcription/csharp/meetings-transcription/Services/CardFactory.cs similarity index 91% rename from samples/meetings-transcription/csharp/MeetingTranscription/Services/CardFactory.cs rename to samples/meetings-transcription/csharp/meetings-transcription/Services/CardFactory.cs index 821de62505..2d54c4aee7 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Services/CardFactory.cs +++ b/samples/meetings-transcription/csharp/meetings-transcription/Services/CardFactory.cs @@ -1,13 +1,13 @@ -// +// // Copyright (c) Microsoft. All rights reserved. // -namespace MeetingTranscription.Services +namespace meetings_transcription.Services { using System.IO; using AdaptiveCards; using AdaptiveCards.Templating; - using Microsoft.Bot.Schema; + using meetings_transcription.Models; using Newtonsoft.Json; /// diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Services/ICardFactory.cs b/samples/meetings-transcription/csharp/meetings-transcription/Services/ICardFactory.cs similarity index 85% rename from samples/meetings-transcription/csharp/MeetingTranscription/Services/ICardFactory.cs rename to samples/meetings-transcription/csharp/meetings-transcription/Services/ICardFactory.cs index e968fcca5d..f4626c52e0 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Services/ICardFactory.cs +++ b/samples/meetings-transcription/csharp/meetings-transcription/Services/ICardFactory.cs @@ -1,10 +1,10 @@ -// +// // Copyright (c) Microsoft. All rights reserved. // -namespace MeetingTranscription.Services +namespace meetings_transcription.Services { - using Microsoft.Bot.Schema; + using meetings_transcription.Models; /// /// Card Factory Service exposes methods to create Adaptive Card or Adaptive Cards as attachment. diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Views/Home/Index.cshtml b/samples/meetings-transcription/csharp/meetings-transcription/Views/Home/Index.cshtml similarity index 98% rename from samples/meetings-transcription/csharp/MeetingTranscription/Views/Home/Index.cshtml rename to samples/meetings-transcription/csharp/meetings-transcription/Views/Home/Index.cshtml index 6b3dd7a9a2..381d50fdb8 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Views/Home/Index.cshtml +++ b/samples/meetings-transcription/csharp/meetings-transcription/Views/Home/Index.cshtml @@ -1,4 +1,4 @@ -@{ +@{ Layout = "~/Views/Shared/_Layout.cshtml"; } @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Views/Shared/_Layout.cshtml b/samples/meetings-transcription/csharp/meetings-transcription/Views/Shared/_Layout.cshtml similarity index 96% rename from samples/meetings-transcription/csharp/MeetingTranscription/Views/Shared/_Layout.cshtml rename to samples/meetings-transcription/csharp/meetings-transcription/Views/Shared/_Layout.cshtml index 4e07874694..98d98f83ca 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Views/Shared/_Layout.cshtml +++ b/samples/meetings-transcription/csharp/meetings-transcription/Views/Shared/_Layout.cshtml @@ -1,4 +1,4 @@ - + diff --git a/samples/meetings-transcription/csharp/MeetingTranscription/Views/_ViewStart.cshtml b/samples/meetings-transcription/csharp/meetings-transcription/Views/_ViewStart.cshtml similarity index 88% rename from samples/meetings-transcription/csharp/MeetingTranscription/Views/_ViewStart.cshtml rename to samples/meetings-transcription/csharp/meetings-transcription/Views/_ViewStart.cshtml index 2de62418c0..be4d4b4b3b 100644 --- a/samples/meetings-transcription/csharp/MeetingTranscription/Views/_ViewStart.cshtml +++ b/samples/meetings-transcription/csharp/meetings-transcription/Views/_ViewStart.cshtml @@ -1,3 +1,3 @@ -@{ +@{ Layout = "~/Views/Shared/_Layout.cshtml"; } diff --git a/samples/meetings-transcription/csharp/meetings-transcription/appsettings.Development.json b/samples/meetings-transcription/csharp/meetings-transcription/appsettings.Development.json new file mode 100644 index 0000000000..d6228db644 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/appsettings.Development.json @@ -0,0 +1,28 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "", + "TenantId": "" + }, + "Azure": { + "MicrosoftAppId": "", + "MicrosoftAppPassword": "", + "MicrosoftAppTenantId": "", + "AppBaseUrl": "", + "GraphApiEndpoint": "", + "UserId": "", + "AzureAdUserId": "" + } +} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/meetings-transcription/appsettings.Playground.json b/samples/meetings-transcription/csharp/meetings-transcription/appsettings.Playground.json new file mode 100644 index 0000000000..0497106b3c --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/appsettings.Playground.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "TenantId": "", + "BotType": "" + } +} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/meetings-transcription/appsettings.json b/samples/meetings-transcription/csharp/meetings-transcription/appsettings.json new file mode 100644 index 0000000000..24087e40b0 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/appsettings.json @@ -0,0 +1,27 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "" + }, + "Azure": { + "MicrosoftAppId": "<>", + "MicrosoftAppPassword": "<>", + "MicrosoftAppTenantId": "<>", + "AppBaseUrl": "<>", + "GraphApiEndpoint": "https://graph.microsoft.com/beta", + "UserId": "<>", + "AzureAdUserId": "<>" + } +} \ No newline at end of file diff --git a/samples/meetings-transcription/csharp/meetings-transcription/meetings-transcription.csproj b/samples/meetings-transcription/csharp/meetings-transcription/meetings-transcription.csproj new file mode 100644 index 0000000000..d08938a8c9 --- /dev/null +++ b/samples/meetings-transcription/csharp/meetings-transcription/meetings-transcription.csproj @@ -0,0 +1,38 @@ + + + + net10.0 + enable + latest + + + + + + + + + + + + + + + + + + + + PreserveNewest + None + + + + PreserveNewest + None + + + Always + + + \ No newline at end of file