diff --git a/.azdo/publish.yaml b/.azdo/publish.yaml
new file mode 100644
index 00000000..4706f331
--- /dev/null
+++ b/.azdo/publish.yaml
@@ -0,0 +1,227 @@
+# =============================================================================
+# This pipeline publishes NuGet packages. Manually triggered only.
+# - "Internal": pushes unsigned packages to the internal TeamsSDKPreviews feed.
+# - "Public": signs (Authenticode + NuGet) and pushes to nuget.org (requires approval).
+# Version is determined by Nerdbank.GitVersioning (nbgv) from version.json.
+#
+# MIGRATED TO 1ES OFFICIAL PIPELINE TEMPLATE: This pipeline now extends from
+# 1ES.Official.PipelineTemplate to ensure compliance with M365 security requirements.
+# =============================================================================
+
+resources:
+ repositories:
+ - repository: 1esPipelines
+ type: git
+ name: 1ESPipelineTemplates/1ESPipelineTemplates
+ ref: refs/tags/release
+
+trigger: none
+
+pr: none
+
+parameters:
+- name: publishType
+ displayName: 'Publish Type'
+ type: string
+ default: 'Internal'
+ values:
+ - Internal
+ - Public
+
+variables:
+ buildConfiguration: 'Release'
+ folderPath: '$(Build.SourcesDirectory)'
+ ${{ if eq(parameters.publishType, 'Public') }}:
+ appRegistrationTenantId: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
+ authenticodeSignId: '2d5c4ab9-0b7e-4f60-bb92-70322df77b94'
+ nugetSignId: 'a94a770a-9a7b-4888-a3ea-24584b851e49'
+
+extends:
+ template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
+ parameters:
+ pool:
+ name: Azure-Pipelines-1ESPT-ExDShared
+ image: ubuntu-22.04
+ os: linux
+
+ sdl:
+ sourceAnalysisPool:
+ name: Azure-Pipelines-1ESPT-ExDShared
+ image: windows-2022
+ os: windows
+
+ # Required for M365PT tracking and drift management
+ customBuildTags:
+ - ES365AIMigrationTooling
+
+ stages:
+
+ - ${{ if eq(parameters.publishType, 'Internal') }}:
+ - stage: Build_Test_Pack_Push_Internal
+ displayName: 'Build, Test, Pack, and Push (Internal)'
+ jobs:
+ - job: BuildTestPackPush
+ displayName: 'Build, Test, Pack, and Push to Internal Feed'
+ steps:
+ - checkout: self
+ - task: UseDotNet@2
+ displayName: 'Use .NET 8'
+ inputs:
+ packageType: 'sdk'
+ version: '8.0.x'
+
+ - task: UseDotNet@2
+ displayName: 'Use .NET 10'
+ inputs:
+ packageType: 'sdk'
+ version: '10.0.x'
+
+ - pwsh: |
+ $nugetConfig = @"
+
+
+
+
+
+
+
+ "@
+ $nugetConfig | Out-File -FilePath "$(Build.SourcesDirectory)/nuget.config" -Encoding utf8
+ displayName: 'Create nuget.config'
+
+ - task: NuGetAuthenticate@1
+ displayName: 'Authenticate with NuGet feeds'
+
+ - script: dotnet restore
+ displayName: 'Restore'
+
+ - script: dotnet build --no-restore --configuration $(buildConfiguration)
+ displayName: 'Build'
+
+ - script: dotnet test --no-build --verbosity normal --logger trx --configuration $(buildConfiguration)
+ displayName: 'Test'
+
+ - task: PublishTestResults@2
+ displayName: 'Publish Test Results'
+ condition: succeededOrFailed()
+ inputs:
+ testResultsFormat: 'VSTest'
+ testResultsFiles: '**/*.trx'
+ mergeTestResults: true
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Pack'
+ inputs:
+ command: 'pack'
+ packagesToPack: 'Libraries/**/*.csproj'
+ nobuild: true
+ configuration: '$(buildConfiguration)'
+ outputDir: '$(Build.ArtifactStagingDirectory)'
+ buildProperties: 'SymbolPackageFormat=snupkg'
+
+ - task: 1ES.PublishNuget@1
+ displayName: 'Push NuGet Packages to Internal Feed'
+ inputs:
+ useDotNetTask: false
+ packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
+ packageParentPath: '$(Build.ArtifactStagingDirectory)'
+ publishVstsFeed: '$(System.TeamProject)/TeamsSDKPreviews'
+ nuGetFeedType: internal
+ allowPackageConflicts: false # Allow duplicate versions for preview testing
+ publishPackageMetadata: true
+ retryCountOnTaskFailure: 2
+
+ - task: 1ES.PublishPipelineArtifact@1
+ displayName: 'Publish NuGet Packages as Pipeline Artifact'
+ inputs:
+ targetPath: '$(Build.ArtifactStagingDirectory)'
+ artifactName: 'Packages'
+
+ - ${{ if eq(parameters.publishType, 'Public') }}:
+ - stage: Build_Test_Sign_Pack
+ displayName: 'Build, Test, Sign, and Pack (Public)'
+ jobs:
+ - job: BuildTestSignPack
+ displayName: 'Build, Test, Sign, and Pack'
+ steps:
+ - checkout: self
+
+ - task: UseDotNet@2
+ displayName: 'Use .NET 8'
+ inputs:
+ packageType: 'sdk'
+ version: '8.0.x'
+
+ - task: UseDotNet@2
+ displayName: 'Use .NET 10'
+ inputs:
+ packageType: 'sdk'
+ version: '10.0.x'
+
+ - pwsh: |
+ $nugetConfig = @"
+
+
+
+
+
+
+
+ "@
+ $nugetConfig | Out-File -FilePath "$(Build.SourcesDirectory)/nuget.config" -Encoding utf8
+ displayName: 'Create nuget.config'
+
+ - task: NuGetAuthenticate@1
+ displayName: 'Authenticate with NuGet feeds'
+
+ - script: dotnet restore
+ displayName: 'Restore'
+
+ - script: dotnet build --no-restore --configuration $(buildConfiguration)
+ displayName: 'Build'
+
+ - script: dotnet test --no-build --verbosity normal --logger trx --configuration $(buildConfiguration)
+ displayName: 'Test'
+
+ - task: PublishTestResults@2
+ displayName: 'Publish Test Results'
+ condition: succeededOrFailed()
+ inputs:
+ testResultsFormat: 'VSTest'
+ testResultsFiles: '**/*.trx'
+ mergeTestResults: true
+
+ - template: .azdo/templates/sign-and-pack.yaml@self
+
+ - task: 1ES.PublishPipelineArtifact@1
+ displayName: 'Publish NuGet Packages as Pipeline Artifact'
+ inputs:
+ targetPath: '$(Build.ArtifactStagingDirectory)'
+ artifactName: 'Packages'
+
+ - ${{ if eq(parameters.publishType, 'Public') }}:
+ - stage: PushToNuGet
+ displayName: 'Push Packages to nuget.org'
+ dependsOn: Build_Test_Sign_Pack
+ jobs:
+ - deployment: PushPackages
+ displayName: 'Manual Approval Required to Push Packages'
+ environment:
+ name: 'teams-net-publish'
+ strategy:
+ runOnce:
+ deploy:
+ steps:
+ - download: current
+ artifact: Packages
+
+ - task: 1ES.PublishNuget@1
+ displayName: 'Push Packages to nuget.org'
+ inputs:
+ useDotNetTask: false
+ packagesToPush: '$(Pipeline.Workspace)/Packages/*.nupkg'
+ packageParentPath: '$(Pipeline.Workspace)/Packages'
+ nuGetFeedType: external
+ publishFeedCredentials: 'Microsoft.Teams.*'
+ publishPackageMetadata: true
+ retryCountOnTaskFailure: 2
diff --git a/.azdo/publish.yml b/.azdo/publish.yml
deleted file mode 100644
index f33373a9..00000000
--- a/.azdo/publish.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-# =============================================================================
-# For public releases, this pipeline (teams.net) is triggered manually.
-# It builds, tests, signs (Authenticode + NuGet), and packs the code, then pushes to nuget.org (requires approval).
-# =============================================================================
-
-trigger: none
-
-pr: none
-
-pool:
- vmImage: 'ubuntu-22.04'
-
-variables:
- buildConfiguration: 'Release'
- folderPath: '$(Build.SourcesDirectory)'
- appRegistrationTenantId: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
- authenticodeSignId: '2d5c4ab9-0b7e-4f60-bb92-70322df77b94'
- nugetSignId: 'a94a770a-9a7b-4888-a3ea-24584b851e49'
-
-stages:
-- stage: Build_Test_Sign_Pack
- jobs:
- - job: BuildTestSignPack
- displayName: 'Build, Test, Sign, and Pack'
- steps:
- - checkout: self
-
- - task: UseDotNet@2
- displayName: 'Use .NET 8'
- inputs:
- packageType: 'sdk'
- version: '8.0.x'
-
- - task: UseDotNet@2
- displayName: 'Use .NET 10'
- inputs:
- packageType: 'sdk'
- version: '10.0.x'
-
- - script: dotnet restore
- displayName: 'Restore'
-
- - script: dotnet build --no-restore --configuration $(buildConfiguration)
- displayName: 'Build'
-
- - script: dotnet test --no-build --verbosity normal --logger trx --configuration $(buildConfiguration)
- displayName: 'Test'
-
- - task: PublishTestResults@2
- displayName: 'Publish Test Results'
- condition: succeededOrFailed()
- inputs:
- testResultsFormat: 'VSTest'
- testResultsFiles: '**/*.trx'
- mergeTestResults: true
-
- - template: templates/sign-and-pack.yaml
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish NuGet Packages as Pipeline Artifact'
- inputs:
- targetPath: '$(Build.ArtifactStagingDirectory)'
- artifact: 'Packages'
- publishLocation: 'pipeline'
-
-- stage: PushToNuGet
- displayName: 'Push NuGet Packages to nuget.org'
- dependsOn: Build_Test_Sign_Pack
- jobs:
- - deployment: PushPackages
- displayName: 'Manual Approval Required to Push Packages'
- environment:
- name: 'teams-net-publish'
- strategy:
- runOnce:
- deploy:
- steps:
- - download: current
- artifact: Packages
-
- - task: NuGetCommand@2
- displayName: 'Push NuGet Packages'
- inputs:
- command: push
- packagesToPush: '$(Pipeline.Workspace)/Packages/*.nupkg'
- nuGetFeedType: external
- publishFeedCredentials: 'Microsoft.Teams.*'
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 95247ceb..def52263 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,9 +9,7 @@
# local app settings files
appsettings.Local.json
appsettings.Development.json
-launchSettings.json
-
-.claude/
+launchsettings.json
# User-specific files
*.rsuser
diff --git a/Libraries/Microsoft.Teams.Api/Activities/Activity.cs b/Libraries/Microsoft.Teams.Api/Activities/Activity.cs
index 2d526cb1..6c7dc4a6 100644
--- a/Libraries/Microsoft.Teams.Api/Activities/Activity.cs
+++ b/Libraries/Microsoft.Teams.Api/Activities/Activity.cs
@@ -146,10 +146,6 @@ public partial class Activity : IActivity
[JsonPropertyOrder(130)]
public ChannelData? ChannelData { get; set; }
- [JsonIgnore]
- [Experimental("ExperimentalTeamsTargeted")]
- public bool IsTargeted { get; set; }
-
[JsonExtensionData]
public IDictionary Properties { get; set; } = new Dictionary();
@@ -227,14 +223,6 @@ public virtual Activity WithRelatesTo(ConversationReference value)
}
public virtual Activity WithRecipient(Account value)
- {
- #pragma warning disable ExperimentalTeamsTargeted
- return WithRecipient(value, false);
- #pragma warning restore ExperimentalTeamsTargeted
- }
-
- [Experimental("ExperimentalTeamsTargeted")]
- public virtual Activity WithRecipient(Account value, bool isTargeted)
{
Recipient = value;
#pragma warning disable ExperimentalTeamsTargeted
@@ -440,13 +428,6 @@ public Activity Merge(Activity from)
LocalTimestamp ??= from.LocalTimestamp;
AddEntity(from.Entities?.ToArray() ?? []);
- #pragma warning disable ExperimentalTeamsTargeted
- if (from.IsTargeted)
- {
- IsTargeted = true;
- }
- #pragma warning restore ExperimentalTeamsTargeted
-
if (from.ChannelData is not null)
{
WithData(from.ChannelData);
diff --git a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
index 7ae6d974..e198eb73 100644
--- a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
+++ b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
@@ -10,6 +10,7 @@ public class ClientCredentials : IHttpCredentials
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string? TenantId { get; set; }
+ public CloudEnvironment Cloud { get; set; } = CloudEnvironment.Public;
public ClientCredentials(string clientId, string clientSecret)
{
@@ -26,9 +27,9 @@ public ClientCredentials(string clientId, string clientSecret, string? tenantId)
public async Task Resolve(IHttpClient client, string[] scopes, CancellationToken cancellationToken = default)
{
- var tenantId = TenantId ?? "botframework.com";
+ var tenantId = TenantId ?? Cloud.LoginTenant;
var request = HttpRequest.Post(
- $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token"
+ $"{Cloud.LoginEndpoint}/{tenantId}/oauth2/v2.0/token"
);
request.Headers.Add("Content-Type", ["application/x-www-form-urlencoded"]);
diff --git a/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs b/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs
new file mode 100644
index 00000000..a251373f
--- /dev/null
+++ b/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs
@@ -0,0 +1,184 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Teams.Api.Auth;
+
+///
+/// Bundles all cloud-specific service endpoints for a given Azure environment.
+/// Use predefined instances (, , , )
+/// or construct a custom one.
+///
+public class CloudEnvironment
+{
+ ///
+ /// The Azure AD login endpoint (e.g. "https://login.microsoftonline.com").
+ ///
+ public string LoginEndpoint { get; }
+
+ ///
+ /// The default multi-tenant login tenant (e.g. "botframework.com").
+ ///
+ public string LoginTenant { get; }
+
+ ///
+ /// The Bot Framework OAuth scope (e.g. "https://api.botframework.com/.default").
+ ///
+ public string BotScope { get; }
+
+ ///
+ /// The Bot Framework token service base URL (e.g. "https://token.botframework.com").
+ ///
+ public string TokenServiceUrl { get; }
+
+ ///
+ /// The OpenID metadata URL for token validation (e.g. "https://login.botframework.com/v1/.well-known/openidconfiguration").
+ ///
+ public string OpenIdMetadataUrl { get; }
+
+ ///
+ /// The token issuer for Bot Framework tokens (e.g. "https://api.botframework.com").
+ ///
+ public string TokenIssuer { get; }
+
+ ///
+ /// The Microsoft Graph token scope (e.g. "https://graph.microsoft.com/.default").
+ ///
+ public string GraphScope { get; }
+
+ ///
+ /// Allowed service URL hostnames for this cloud environment.
+ ///
+ public IReadOnlyList AllowedServiceUrls { get; }
+
+ public CloudEnvironment(
+ string loginEndpoint,
+ string loginTenant,
+ string botScope,
+ string tokenServiceUrl,
+ string openIdMetadataUrl,
+ string tokenIssuer,
+ string graphScope,
+ string[]? allowedServiceUrls = null)
+ {
+ LoginEndpoint = loginEndpoint.TrimEnd('/');
+ LoginTenant = loginTenant;
+ BotScope = botScope;
+ TokenServiceUrl = tokenServiceUrl.TrimEnd('/');
+ OpenIdMetadataUrl = openIdMetadataUrl;
+ TokenIssuer = tokenIssuer;
+ GraphScope = graphScope;
+ AllowedServiceUrls = allowedServiceUrls is not null ? Array.AsReadOnly(allowedServiceUrls) : [];
+ }
+
+ ///
+ /// Microsoft public (commercial) cloud.
+ ///
+ public static readonly CloudEnvironment Public = new(
+ loginEndpoint: "https://login.microsoftonline.com",
+ loginTenant: "botframework.com",
+ botScope: "https://api.botframework.com/.default",
+ tokenServiceUrl: "https://token.botframework.com",
+ openIdMetadataUrl: "https://login.botframework.com/v1/.well-known/openidconfiguration",
+ tokenIssuer: "https://api.botframework.com",
+ graphScope: "https://graph.microsoft.com/.default",
+ allowedServiceUrls: ["smba.trafficmanager.net", "smba.onyx.prod.teams.trafficmanager.net", "smba.infra.gcc.teams.microsoft.com"]
+ );
+
+ ///
+ /// US Government Community Cloud High (GCCH).
+ ///
+ public static readonly CloudEnvironment USGov = new(
+ loginEndpoint: "https://login.microsoftonline.us",
+ loginTenant: "MicrosoftServices.onmicrosoft.us",
+ botScope: "https://api.botframework.us/.default",
+ tokenServiceUrl: "https://tokengcch.botframework.azure.us",
+ openIdMetadataUrl: "https://login.botframework.azure.us/v1/.well-known/openidconfiguration",
+ tokenIssuer: "https://api.botframework.us",
+ graphScope: "https://graph.microsoft.us/.default",
+ allowedServiceUrls: ["smba.infra.gov.teams.microsoft.us"]
+ );
+
+ ///
+ /// US Government Department of Defense (DoD).
+ ///
+ public static readonly CloudEnvironment USGovDoD = new(
+ loginEndpoint: "https://login.microsoftonline.us",
+ loginTenant: "MicrosoftServices.onmicrosoft.us",
+ botScope: "https://api.botframework.us/.default",
+ tokenServiceUrl: "https://apiDoD.botframework.azure.us",
+ openIdMetadataUrl: "https://login.botframework.azure.us/v1/.well-known/openidconfiguration",
+ tokenIssuer: "https://api.botframework.us",
+ graphScope: "https://dod-graph.microsoft.us/.default",
+ allowedServiceUrls: ["smba.infra.dod.teams.microsoft.us"]
+ );
+
+ ///
+ /// China cloud (21Vianet).
+ ///
+ public static readonly CloudEnvironment China = new(
+ loginEndpoint: "https://login.partner.microsoftonline.cn",
+ loginTenant: "microsoftservices.partner.onmschina.cn",
+ botScope: "https://api.botframework.azure.cn/.default",
+ tokenServiceUrl: "https://token.botframework.azure.cn",
+ openIdMetadataUrl: "https://login.botframework.azure.cn/v1/.well-known/openidconfiguration",
+ tokenIssuer: "https://api.botframework.azure.cn",
+ graphScope: "https://microsoftgraph.chinacloudapi.cn/.default",
+ allowedServiceUrls: ["frontend.botapi.msg.infra.teams.microsoftonline.cn"]
+ );
+
+ ///
+ /// Creates a new by applying non-null overrides on top of this instance.
+ /// Returns the same instance if all overrides are null (no allocation).
+ ///
+ public CloudEnvironment WithOverrides(
+ string? loginEndpoint = null,
+ string? loginTenant = null,
+ string? botScope = null,
+ string? tokenServiceUrl = null,
+ string? openIdMetadataUrl = null,
+ string? tokenIssuer = null,
+ string? graphScope = null,
+ string[]? allowedServiceUrls = null)
+ {
+ if (loginEndpoint is null && loginTenant is null && botScope is null &&
+ tokenServiceUrl is null && openIdMetadataUrl is null && tokenIssuer is null &&
+ graphScope is null && allowedServiceUrls is null)
+ {
+ return this;
+ }
+
+ return new CloudEnvironment(
+ loginEndpoint ?? LoginEndpoint,
+ loginTenant ?? LoginTenant,
+ botScope ?? BotScope,
+ tokenServiceUrl ?? TokenServiceUrl,
+ openIdMetadataUrl ?? OpenIdMetadataUrl,
+ tokenIssuer ?? TokenIssuer,
+ graphScope ?? GraphScope,
+ allowedServiceUrls ?? [.. AllowedServiceUrls]
+ );
+ }
+
+ ///
+ /// Resolves a cloud environment name (case-insensitive) to its corresponding instance.
+ /// Valid names: "Public", "USGov", "USGovDoD", "China".
+ ///
+ public static CloudEnvironment FromName(string name)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("Cloud environment name cannot be empty or whitespace.", nameof(name));
+ }
+
+ return name.ToLowerInvariant() switch
+ {
+ "public" => Public,
+ "usgov" => USGov,
+ "usgovdod" => USGovDoD,
+ "china" => China,
+ _ => throw new ArgumentException($"Unknown cloud environment: '{name}'. Valid values are: Public, USGov, USGovDoD, China.", nameof(name))
+ };
+ }
+}
diff --git a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
index b1588d2a..c61222ce 100644
--- a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
+++ b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
@@ -83,8 +83,12 @@ public ApiClient(ApiClient client, CancellationToken cancellationToken) : base(c
{
ServiceUrl = client.ServiceUrl;
Bots = new BotClient(_http, cancellationToken);
+ Bots.Token.ActiveBotScope = client.Bots.Token.ActiveBotScope;
+ Bots.Token.ActiveGraphScope = client.Bots.Token.ActiveGraphScope;
+ Bots.SignIn.TokenServiceUrl = client.Bots.SignIn.TokenServiceUrl;
Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken);
Users = new UserClient(_http, cancellationToken);
+ Users.Token.TokenServiceUrl = client.Users.Token.TokenServiceUrl;
Teams = new TeamClient(ServiceUrl, _http, cancellationToken);
Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken);
}
diff --git a/Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs b/Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs
index d1d8d792..716b0294 100644
--- a/Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs
+++ b/Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs
@@ -7,6 +7,8 @@ namespace Microsoft.Teams.Api.Clients;
public class BotSignInClient : Client
{
+ public string TokenServiceUrl { get; set; } = "https://token.botframework.com";
+
public BotSignInClient() : base()
{
@@ -32,7 +34,7 @@ public async Task GetUrlAsync(GetUrlRequest request, CancellationToken c
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
var req = HttpRequest.Get(
- $"https://token.botframework.com/api/botsignin/GetSignInUrl?{query}"
+ $"{TokenServiceUrl}/api/botsignin/GetSignInUrl?{query}"
);
var res = await _http.SendAsync(req, token);
@@ -44,7 +46,7 @@ public async Task GetUrlAsync(GetUrlRequest request, CancellationToken c
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
var req = HttpRequest.Get(
- $"https://token.botframework.com/api/botsignin/GetSignInResource?{query}"
+ $"{TokenServiceUrl}/api/botsignin/GetSignInResource?{query}"
);
var res = await _http.SendAsync(req, token);
diff --git a/Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs b/Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs
index a7deef3f..e4870df5 100644
--- a/Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs
+++ b/Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs
@@ -9,6 +9,8 @@ public class BotTokenClient : Client
{
public static readonly string BotScope = "https://api.botframework.com/.default";
public static readonly string GraphScope = "https://graph.microsoft.com/.default";
+ public string ActiveBotScope { get; set; } = BotScope;
+ public string ActiveGraphScope { get; set; } = GraphScope;
public BotTokenClient() : this(default)
{
@@ -38,12 +40,12 @@ public BotTokenClient(IHttpClientFactory factory, CancellationToken cancellation
public virtual async Task GetAsync(IHttpCredentials credentials, IHttpClient? http = null, CancellationToken cancellationToken = default)
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
- return await credentials.Resolve(http ?? _http, [BotScope], token);
+ return await credentials.Resolve(http ?? _http, [ActiveBotScope], token);
}
public async Task GetGraphAsync(IHttpCredentials credentials, IHttpClient? http = null, CancellationToken cancellationToken = default)
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
- return await credentials.Resolve(http ?? _http, [GraphScope], token);
+ return await credentials.Resolve(http ?? _http, [ActiveGraphScope], token);
}
}
\ No newline at end of file
diff --git a/Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs b/Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs
index 51808761..2a476dfc 100644
--- a/Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs
+++ b/Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs
@@ -10,6 +10,8 @@ namespace Microsoft.Teams.Api.Clients;
public class UserTokenClient : Client
{
+ public string TokenServiceUrl { get; set; } = "https://token.botframework.com";
+
private readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
@@ -39,7 +41,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
- var req = HttpRequest.Get($"https://token.botframework.com/api/usertoken/GetToken?{query}");
+ var req = HttpRequest.Get($"{TokenServiceUrl}/api/usertoken/GetToken?{query}");
var res = await _http.SendAsync(req, token);
return res.Body;
}
@@ -48,7 +50,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
- var req = HttpRequest.Post($"https://token.botframework.com/api/usertoken/GetAadTokens?{query}", body: request);
+ var req = HttpRequest.Post($"{TokenServiceUrl}/api/usertoken/GetAadTokens?{query}", body: request);
var res = await _http.SendAsync>(req, token);
return res.Body;
}
@@ -57,7 +59,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
- var req = HttpRequest.Get($"https://token.botframework.com/api/usertoken/GetTokenStatus?{query}");
+ var req = HttpRequest.Get($"{TokenServiceUrl}/api/usertoken/GetTokenStatus?{query}");
var res = await _http.SendAsync>(req, token);
return res.Body;
}
@@ -66,7 +68,7 @@ public async Task SignOutAsync(SignOutRequest request, CancellationToken cancell
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
- var req = HttpRequest.Delete($"https://token.botframework.com/api/usertoken/SignOut?{query}");
+ var req = HttpRequest.Delete($"{TokenServiceUrl}/api/usertoken/SignOut?{query}");
await _http.SendAsync(req, token);
}
@@ -84,7 +86,7 @@ public async Task SignOutAsync(SignOutRequest request, CancellationToken cancell
// This is required for the Bot Framework Token Service to process the request correctly.
var body = JsonSerializer.Serialize(request.GetBody(), _jsonSerializerOptions);
- var req = HttpRequest.Post($"https://token.botframework.com/api/usertoken/exchange?{query}", body);
+ var req = HttpRequest.Post($"{TokenServiceUrl}/api/usertoken/exchange?{query}", body);
req.Headers.Add("Content-Type", new List() { "application/json" });
var res = await _http.SendAsync(req, token);
diff --git a/Libraries/Microsoft.Teams.Api/Conversation.cs b/Libraries/Microsoft.Teams.Api/Conversation.cs
index 3cb0b152..a2cb8f53 100644
--- a/Libraries/Microsoft.Teams.Api/Conversation.cs
+++ b/Libraries/Microsoft.Teams.Api/Conversation.cs
@@ -64,6 +64,31 @@ public string ThreadId
}
}
+ ///
+ /// Construct a threaded conversation ID by appending ;messageid={messageId}
+ /// to the conversation ID. This is the format APX uses to route messages
+ /// to a specific thread in a channel.
+ ///
+ /// the conversation to thread into (e.g. 19:abc@thread.skype)
+ /// the thread root message ID (must be a non-zero numeric string)
+ /// the threaded conversation ID (e.g. 19:abc@thread.skype;messageid=123)
+ public static string ToThreadedConversationId(string conversationId, string messageId)
+ {
+ if (string.IsNullOrEmpty(conversationId))
+ {
+ throw new ArgumentException("conversationId must be a non-empty string", nameof(conversationId));
+ }
+
+ if (string.IsNullOrEmpty(messageId) || !ulong.TryParse(messageId, out var parsed) || parsed == 0)
+ {
+ throw new ArgumentException($"Invalid messageId \"{messageId}\": must be a non-zero numeric value", nameof(messageId));
+ }
+
+ // Strip any existing ;messageid= suffix (mirrors APX's NormalizeConversationId)
+ var baseId = conversationId.Split(';')[0];
+ return $"{baseId};messageid={messageId}";
+ }
+
public object Clone() => MemberwiseClone();
public Conversation Copy() => (Conversation)Clone();
}
diff --git a/Libraries/Microsoft.Teams.Apps/Activities/Activity.cs b/Libraries/Microsoft.Teams.Apps/Activities/Activity.cs
index 2e941671..ef110d9e 100644
--- a/Libraries/Microsoft.Teams.Apps/Activities/Activity.cs
+++ b/Libraries/Microsoft.Teams.Apps/Activities/Activity.cs
@@ -18,6 +18,7 @@ public class ActivityAttribute(string? name = null, Type? type = null) : Attribu
public static partial class AppActivityExtensions
{
+ [Obsolete("Use the handler with the cancellation token")]
public static App OnActivity(this App app, Func, Task> handler)
{
app.Router.Register(async (context) =>
@@ -40,6 +41,7 @@ public static App OnActivity(this App app, Func, Cancellatio
return app;
}
+ [Obsolete("Use the handler with the cancellation token")]
public static App OnActivity(this App app, Func, Task