From 1cc2216ac24ba7019483f1f6ece0da73740cf964 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 2 Jul 2026 16:13:18 -0700 Subject: [PATCH] Use recipient agentic identity defaults Derive inbound bot app id from recipient botId and use recipient agentic identity for BotBuilder compat defaults. Preserve botId through channel account mapping and add focused tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ActivitySchemaMapper.cs | 10 +++++ .../TeamsApiClient.cs | 40 +++++++++---------- .../Http/BotRequestContext.cs | 2 +- .../Schema/ChannelAccount.cs | 6 +++ .../Schema/CoreActivity.cs | 2 + .../CompatActivityTests.cs | 23 +++++++++++ .../Http/BotRequestContextTests.cs | 4 +- 7 files changed, 64 insertions(+), 23 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps.BotBuilder/ActivitySchemaMapper.cs b/core/src/Microsoft.Teams.Apps.BotBuilder/ActivitySchemaMapper.cs index 779059fd..c29a4cb3 100644 --- a/core/src/Microsoft.Teams.Apps.BotBuilder/ActivitySchemaMapper.cs +++ b/core/src/Microsoft.Teams.Apps.BotBuilder/ActivitySchemaMapper.cs @@ -8,6 +8,7 @@ using Microsoft.Bot.Schema.Teams; using Microsoft.Teams.Core.Schema; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Teams.Apps.BotBuilder; @@ -69,6 +70,10 @@ public static Microsoft.Bot.Schema.ChannelAccount ToCompatChannelAccount(this Mi Name = account.Name }; + if (!string.IsNullOrEmpty(account.BotId)) + { + channelAccount.Properties.Add("botId", account.BotId); + } if (account.Properties.TryGetValue("aadObjectId", out object? aadObjectId)) { @@ -206,6 +211,11 @@ public static Microsoft.Teams.Core.Schema.ChannelAccount FromCompatChannelAccoun Microsoft.Teams.Core.Schema.ChannelAccount result = new() { Id = account.Id, Name = account.Name }; + if (account.Properties is not null && account.Properties.TryGetValue("botId", out JToken? botId)) + { + result.BotId = GetStringValue(botId); + } + if (!string.IsNullOrEmpty(account.AadObjectId)) { result.Properties["aadObjectId"] = account.AadObjectId; diff --git a/core/src/Microsoft.Teams.Apps.BotBuilder/TeamsApiClient.cs b/core/src/Microsoft.Teams.Apps.BotBuilder/TeamsApiClient.cs index cf570f6a..d1cfe2bd 100644 --- a/core/src/Microsoft.Teams.Apps.BotBuilder/TeamsApiClient.cs +++ b/core/src/Microsoft.Teams.Apps.BotBuilder/TeamsApiClient.cs @@ -45,10 +45,10 @@ private static string GetServiceUrl(ITurnContext turnContext) ?? throw new InvalidOperationException("ServiceUrl is required."); } - private static AgenticIdentity GetIdentity(ITurnContext turnContext) + private static AgenticIdentity? GetIdentity(ITurnContext turnContext) { CoreActivity coreActivity = turnContext.Activity.FromBotFrameworkActivity(); - return AgenticIdentity.FromAccount(coreActivity.From) ?? new AgenticIdentity(); + return AgenticIdentity.FromAccount(coreActivity.Recipient); } #endregion @@ -87,7 +87,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity identity = GetIdentity(turnContext); + AgenticIdentity? identity = GetIdentity(turnContext); Core.Schema.ChannelAccount result = await client.GetConversationMemberAsync( conversationId, userId, serviceUrl, BotRequestContext.FromAgenticIdentity(identity), null, cancellationToken).ConfigureAwait(false); @@ -121,7 +121,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity identity = GetIdentity(turnContext); + AgenticIdentity? identity = GetIdentity(turnContext); IList members = await client.GetConversationMembersAsync( conversationId, serviceUrl, BotRequestContext.FromAgenticIdentity(identity), null, cancellationToken).ConfigureAwait(false); @@ -158,7 +158,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity identity = GetIdentity(turnContext); + AgenticIdentity? identity = GetIdentity(turnContext); Core.PagedMembersResult pagedMembers = await client.GetConversationPagedMembersAsync( conversationId, serviceUrl, pageSize, continuationToken, BotRequestContext.FromAgenticIdentity(identity), null, cancellationToken).ConfigureAwait(false); @@ -192,7 +192,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity identity = GetIdentity(turnContext); + AgenticIdentity? identity = GetIdentity(turnContext); Core.Schema.ChannelAccount result = await client.GetConversationMemberAsync( t, userId, serviceUrl, BotRequestContext.FromAgenticIdentity(identity), null, cancellationToken).ConfigureAwait(false); @@ -220,7 +220,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity identity = GetIdentity(turnContext); + AgenticIdentity? identity = GetIdentity(turnContext); IList members = await client.GetConversationMembersAsync( t, serviceUrl, BotRequestContext.FromAgenticIdentity(identity), null, cancellationToken).ConfigureAwait(false); @@ -251,7 +251,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity identity = GetIdentity(turnContext); + AgenticIdentity? identity = GetIdentity(turnContext); Core.PagedMembersResult pagedMembers = await client.GetConversationPagedMembersAsync( t, serviceUrl, pageSize, continuationToken, BotRequestContext.FromAgenticIdentity(identity), null, cancellationToken).ConfigureAwait(false); @@ -280,7 +280,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ?? throw new InvalidOperationException("The meetingId can only be null if turnContext is within the scope of a MS Teams Meeting."); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); ConversationClient client = GetConversationClient(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v1/meetings/{Uri.EscapeDataString(meetingId)}"; @@ -319,7 +319,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v1/meetings/{Uri.EscapeDataString(meetingId)}/participants/{Uri.EscapeDataString(participantId)}?tenantId={Uri.EscapeDataString(tenantId)}"; @@ -353,7 +353,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v1/meetings/{Uri.EscapeDataString(meetingId)}/notification"; string body = JsonConvert.SerializeObject(notification); @@ -387,7 +387,7 @@ private static AgenticIdentity GetIdentity(ITurnContext turnContext) ?? throw new InvalidOperationException("This method is only valid within the scope of MS Teams Team."); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity identity = GetIdentity(turnContext); + AgenticIdentity? identity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/teams/{Uri.EscapeDataString(t)}"; @@ -419,7 +419,7 @@ public static async Task GetTeamChannelsAsync( ?? throw new InvalidOperationException("This method is only valid within the scope of MS Teams Team."); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity identity = GetIdentity(turnContext); + AgenticIdentity? identity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/teams/{Uri.EscapeDataString(t)}/conversations"; @@ -461,7 +461,7 @@ public static async Task SendMessageToListOfUsersAsync( ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/users/"; SendMessageToUsersRequest request = new() @@ -503,7 +503,7 @@ public static async Task SendMessageToListOfChannelsAsync( ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/channels/"; SendMessageToUsersRequest request = new() { @@ -545,7 +545,7 @@ public static async Task SendMessageToAllUsersInTeamAsync( ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); if (activity is not Activity teamActivity) throw new ArgumentException("Expected a Bot Framework Activity instance.", nameof(activity)); CoreActivity coreActivity = teamActivity.FromBotFrameworkActivity(); @@ -588,7 +588,7 @@ public static async Task SendMessageToAllUsersInTenantAsync( ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); if (activity is not Activity tenantActivity) throw new ArgumentException("Expected a Bot Framework Activity instance.", nameof(activity)); CoreActivity coreActivity = tenantActivity.FromBotFrameworkActivity(); @@ -684,7 +684,7 @@ await turnContext.Adapter.CreateConversationAsync( ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/{Uri.EscapeDataString(operationId)}"; @@ -715,7 +715,7 @@ await turnContext.Adapter.CreateConversationAsync( ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/failedentries/{Uri.EscapeDataString(operationId)}"; @@ -749,7 +749,7 @@ public static async Task CancelOperationAsync( ConversationClient client = GetConversationClient(turnContext); Uri serviceUrl = new(GetServiceUrl(turnContext)); - AgenticIdentity agenticIdentity = GetIdentity(turnContext); + AgenticIdentity? agenticIdentity = GetIdentity(turnContext); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/{Uri.EscapeDataString(operationId)}"; diff --git a/core/src/Microsoft.Teams.Core/Http/BotRequestContext.cs b/core/src/Microsoft.Teams.Core/Http/BotRequestContext.cs index d78685d5..c618bc05 100644 --- a/core/src/Microsoft.Teams.Core/Http/BotRequestContext.cs +++ b/core/src/Microsoft.Teams.Core/Http/BotRequestContext.cs @@ -53,7 +53,7 @@ public record BotRequestContext /// The inbound activity, or null. /// The context, or null when nothing could be derived. public static BotRequestContext? FromInboundActivity(CoreActivity? activity) - => Build(Schema.AgenticIdentity.FromAccount(activity?.Recipient), NormalizeAppId(activity?.Recipient?.Id)); + => Build(Schema.AgenticIdentity.FromAccount(activity?.Recipient), NormalizeAppId(activity?.Recipient?.BotId)); /// /// Builds context carrying only the supplied agentic identity. diff --git a/core/src/Microsoft.Teams.Core/Schema/ChannelAccount.cs b/core/src/Microsoft.Teams.Core/Schema/ChannelAccount.cs index 7e466b46..b31b67bf 100644 --- a/core/src/Microsoft.Teams.Core/Schema/ChannelAccount.cs +++ b/core/src/Microsoft.Teams.Core/Schema/ChannelAccount.cs @@ -18,6 +18,12 @@ public class ChannelAccount() [JsonPropertyName("id")] public string? Id { get; set; } + /// + /// Gets or sets the bot application ID associated with the account. + /// + [JsonPropertyName("botId")] + public string? BotId { get; set; } + /// /// Gets or sets the display name of the channel account. /// diff --git a/core/src/Microsoft.Teams.Core/Schema/CoreActivity.cs b/core/src/Microsoft.Teams.Core/Schema/CoreActivity.cs index 26526e96..5de01676 100644 --- a/core/src/Microsoft.Teams.Core/Schema/CoreActivity.cs +++ b/core/src/Microsoft.Teams.Core/Schema/CoreActivity.cs @@ -170,11 +170,13 @@ protected CoreActivity(CoreActivity activity) private static ChannelAccount CloneChannelAccount(ChannelAccount source) => new() { Id = source.Id, + BotId = source.BotId, Name = source.Name, IsTargeted = source.IsTargeted, AgenticAppId = source.AgenticAppId, AgenticUserId = source.AgenticUserId, AgenticAppBlueprintId = source.AgenticAppBlueprintId, + TenantId = source.TenantId, Properties = new ExtendedPropertiesDictionary(source.Properties) }; #pragma warning restore ExperimentalTeamsTargeted diff --git a/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatActivityTests.cs b/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatActivityTests.cs index 6073f077..3edb4e79 100644 --- a/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatActivityTests.cs +++ b/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatActivityTests.cs @@ -7,6 +7,7 @@ using Microsoft.Bot.Schema; using Microsoft.Teams.Core.Schema; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Teams.Apps.BotBuilder.UnitTests { @@ -311,6 +312,17 @@ public void FromCompatChannelAccount_MapsIdAndName() Assert.Equal("Alice", result.Name); } + [Fact] + public void FromCompatChannelAccount_MapsBotId() + { + Microsoft.Bot.Schema.ChannelAccount account = new() { Id = "bot-account-id" }; + account.Properties["botId"] = "28:bot-app-id"; + + Microsoft.Teams.Core.Schema.ChannelAccount result = account.FromCompatChannelAccount(); + + Assert.Equal("28:bot-app-id", result.BotId); + } + [Fact] public void FromCompatChannelAccount_MapsAadObjectIdToProperties() { @@ -350,6 +362,17 @@ public void FromCompatChannelAccount_ThrowsOnNull() Microsoft.Bot.Schema.ChannelAccount? account = null; Assert.Throws(() => account!.FromCompatChannelAccount()); } + + [Fact] + public void ToCompatChannelAccount_MapsBotId() + { + Microsoft.Teams.Core.Schema.ChannelAccount account = new() { Id = "bot-account-id", BotId = "28:bot-app-id" }; + + Microsoft.Bot.Schema.ChannelAccount result = account.ToCompatChannelAccount(); + + Assert.True(result.Properties.TryGetValue("botId", out JToken? botId)); + Assert.Equal("28:bot-app-id", botId?.ToString()); + } } public class FromCompatConversationParametersTests diff --git a/core/test/Microsoft.Teams.Core.UnitTests/Http/BotRequestContextTests.cs b/core/test/Microsoft.Teams.Core.UnitTests/Http/BotRequestContextTests.cs index 8b60c893..2792adc8 100644 --- a/core/test/Microsoft.Teams.Core.UnitTests/Http/BotRequestContextTests.cs +++ b/core/test/Microsoft.Teams.Core.UnitTests/Http/BotRequestContextTests.cs @@ -115,7 +115,7 @@ public void FromInboundActivity_TakesBotAppIdAndAgenticFromRecipient() { Type = ActivityType.Message, From = new ChannelAccount { Id = "user-id" }, - Recipient = new ChannelAccount { Id = "28:recipient-bot-id", AgenticUserId = "agentic-user" }, + Recipient = new ChannelAccount { Id = "recipient-account-id", BotId = "28:recipient-bot-id", AgenticUserId = "agentic-user" }, }; BotRequestContext? ctx = BotRequestContext.FromInboundActivity(activity); @@ -133,7 +133,7 @@ public void FromInboundActivity_IgnoresAgenticFieldsOnSender() { Type = ActivityType.Message, From = new ChannelAccount { Id = "user-id", AgenticUserId = "agentic-user" }, - Recipient = new ChannelAccount { Id = "28:recipient-bot-id" }, + Recipient = new ChannelAccount { Id = "recipient-account-id", BotId = "28:recipient-bot-id" }, }; BotRequestContext? ctx = BotRequestContext.FromInboundActivity(activity);