From 05225a2818489de175a4c02dc6f745b7381a0d09 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 17 Feb 2026 22:01:23 -0800 Subject: [PATCH 1/6] Add support for targeted (private) activities in SDK Introduces IsTargeted property to CoreActivity for private messages, updates ConversationClient to append isTargetedActivity query string for send/update/delete, and extends CoreActivityBuilder for targeted recipient support. Adds a "tm" command to send private messages to all members. Updates and adds tests for IsTargeted logic and ensures it is not serialized. Also updates .gitignore for .claude/. --- .gitignore | 2 + core/samples/TeamsBot/Program.cs | 16 ++ .../ConversationClient.cs | 36 +++- .../Schema/CoreActivity.cs | 7 + .../Schema/CoreActivityBuilder.cs | 14 +- .../ConversationClientTests.cs | 174 ++++++++++++++++++ .../CoreActivityBuilderTests.cs | 63 +++++++ .../Schema/CoreActivityTests.cs | 49 +++++ 8 files changed, 359 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 26d20f91..e89a149a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ # local app settings files appsettings.Local.json +.claude/ + # User-specific files *.rsuser *.suo diff --git a/core/samples/TeamsBot/Program.cs b/core/samples/TeamsBot/Program.cs index e8e6d476..2eada81b 100644 --- a/core/samples/TeamsBot/Program.cs +++ b/core/samples/TeamsBot/Program.cs @@ -22,6 +22,22 @@ await teamsApp.Api.Conversations.Reactions.AddAsync(context.Activity, "cake", cancellationToken: cancellationToken); }); +teamsApp.OnMessage("(?i)tm", async (context, cancellationToken) => +{ + var members = await teamsApp.Api.Conversations.Members.GetAllAsync(context.Activity, cancellationToken: cancellationToken); + foreach (var member in members) + { + await context.SendActivityAsync( + TeamsActivity.CreateBuilder() + .WithText($"Hello {member.Name}!") + .WithRecipient(member, true) + .Build(), cancellationToken) + ; + } + await context.SendActivityAsync($"Sent a private message to {members.Count} member(s) of the conversation!", cancellationToken); + +}); + // Markdown handler: matches "markdown" (case-insensitive) teamsApp.OnMessage("(?i)markdown", async (context, cancellationToken) => { diff --git a/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs b/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs index aa86f985..a901386b 100644 --- a/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs +++ b/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs @@ -51,6 +51,11 @@ public async Task SendActivityAsync(CoreActivity activity, url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{convId}/activities"; } + if (activity.IsTargeted) + { + url += url.Contains('?', StringComparison.Ordinal) ? "&isTargetedActivity=true" : "?isTargetedActivity=true"; + } + logger?.LogInformation("Sending activity to {Url}", url); string body = activity.ToJson(); @@ -83,6 +88,12 @@ public async Task UpdateActivityAsync(string conversatio ArgumentNullException.ThrowIfNull(activity.ServiceUrl); string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}"; + + if (activity.IsTargeted) + { + url += "?isTargetedActivity=true"; + } + string body = activity.ToJson(); logger.LogTrace("Updating activity at {Url}: {Activity}", url, body); @@ -107,7 +118,24 @@ public async Task UpdateActivityAsync(string conversatio /// A cancellation token that can be used to cancel the delete operation. /// A task that represents the asynchronous operation. /// Thrown if the activity could not be deleted successfully. - public async Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) + public Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) + { + return DeleteActivityAsync(conversationId, activityId, serviceUrl, isTargeted: false, agenticIdentity, customHeaders, cancellationToken); + } + + /// + /// Deletes an existing activity from a conversation. + /// + /// The ID of the conversation. Cannot be null or whitespace. + /// The ID of the activity to delete. Cannot be null or whitespace. + /// The service URL for the conversation. Cannot be null. + /// If true, deletes a targeted activity. + /// Optional agentic identity for authentication. + /// Optional custom headers to include in the request. + /// A cancellation token that can be used to cancel the delete operation. + /// A task that represents the asynchronous operation. + /// Thrown if the activity could not be deleted successfully. + public async Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, bool isTargeted, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentException.ThrowIfNullOrWhiteSpace(activityId); @@ -115,6 +143,11 @@ public async Task DeleteActivityAsync(string conversationId, string activityId, string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}"; + if (isTargeted) + { + url += "?isTargetedActivity=true"; + } + logger.LogTrace("Deleting activity at {Url}", url); await _botHttpClient.SendAsync( @@ -145,6 +178,7 @@ await DeleteActivityAsync( activity.Conversation.Id, activity.Id, activity.ServiceUrl, + activity.IsTargeted, activity.From.GetAgenticIdentity(), customHeaders, cancellationToken).ConfigureAwait(false); diff --git a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs index 1bd1a5b6..daca5df1 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs @@ -57,6 +57,12 @@ public class CoreActivity /// Gets or sets the account that should receive this activity. /// [JsonPropertyName("recipient")] public ConversationAccount Recipient { get; set; } = new(); + + /// + /// Indicates if this is a targeted message visible only to a specific recipient. + /// Used internally by the SDK for routing - not serialized to the service. + /// + [JsonIgnore] public bool IsTargeted { get; set; } /// /// Gets or sets the conversation in which this activity is taking place. /// @@ -150,6 +156,7 @@ protected CoreActivity(CoreActivity activity) Attachments = activity.Attachments; Properties = activity.Properties; Value = activity.Value; + IsTargeted = activity.IsTargeted; } /// diff --git a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs index d05e635e..624cc038 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs @@ -47,7 +47,7 @@ public TBuilder WithConversationReference(TActivity activity) WithChannelId(activity.ChannelId); SetConversation(activity.Conversation); SetFrom(activity.Recipient); - SetRecipient(activity.From); + //SetRecipient(activity.From); return (TBuilder)this; } @@ -140,8 +140,20 @@ public TBuilder WithFrom(ConversationAccount from) /// The recipient account. /// The builder instance for chaining. public TBuilder WithRecipient(ConversationAccount recipient) + { + return WithRecipient(recipient, false); + } + + /// + /// Sets the recipient account information and optionally marks this as a targeted message. + /// + /// The recipient account. + /// If true, marks this as a targeted message visible only to the specified recipient. + /// The builder instance for chaining. + public TBuilder WithRecipient(ConversationAccount recipient, bool isTargeted) { SetRecipient(recipient); + _activity.IsTargeted = isTargeted; return (TBuilder)this; } diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs index 212d4927..85c4c3c6 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System.Net; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Teams.Bot.Core.Schema; using Moq; using Moq.Protected; @@ -168,4 +170,176 @@ public async Task SendActivityAsync_ConstructsCorrectUrl() Assert.Equal("https://test.service.url/v3/conversations/conv123/activities/", capturedRequest.RequestUri?.ToString()); Assert.Equal(HttpMethod.Post, capturedRequest.Method); } + + [Fact] + public async Task SendActivityAsync_WithIsTargeted_AppendsQueryString() + { + HttpRequestMessage? capturedRequest = null; + Mock mockHttpMessageHandler = new(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"id\":\"activity123\"}") + }); + + HttpClient httpClient = new(mockHttpMessageHandler.Object); + ConversationClient conversationClient = new(httpClient); + + CoreActivity activity = new() + { + Type = ActivityType.Message, + Conversation = new Conversation { Id = "conv123" }, + ServiceUrl = new Uri("https://test.service.url/"), + IsTargeted = true + }; + + await conversationClient.SendActivityAsync(activity); + + Assert.NotNull(capturedRequest); + Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); + } + + [Fact] + public async Task SendActivityAsync_WithIsTargetedFalse_DoesNotAppendQueryString() + { + HttpRequestMessage? capturedRequest = null; + Mock mockHttpMessageHandler = new(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"id\":\"activity123\"}") + }); + + HttpClient httpClient = new(mockHttpMessageHandler.Object); + ConversationClient conversationClient = new(httpClient); + + CoreActivity activity = new() + { + Type = ActivityType.Message, + Conversation = new Conversation { Id = "conv123" }, + ServiceUrl = new Uri("https://test.service.url/"), + IsTargeted = false + }; + + await conversationClient.SendActivityAsync(activity); + + Assert.NotNull(capturedRequest); + Assert.DoesNotContain("isTargetedActivity", capturedRequest.RequestUri?.ToString()); + } + + [Fact] + public async Task UpdateActivityAsync_WithIsTargeted_AppendsQueryString() + { + HttpRequestMessage? capturedRequest = null; + Mock mockHttpMessageHandler = new(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"id\":\"activity123\"}") + }); + + HttpClient httpClient = new(mockHttpMessageHandler.Object); + ConversationClient conversationClient = new(httpClient, NullLogger.Instance); + + CoreActivity activity = new() + { + Type = ActivityType.Message, + ServiceUrl = new Uri("https://test.service.url/"), + IsTargeted = true + }; + + await conversationClient.UpdateActivityAsync("conv123", "activity123", activity); + + Assert.NotNull(capturedRequest); + Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); + Assert.Equal(HttpMethod.Put, capturedRequest.Method); + } + + [Fact] + public async Task DeleteActivityAsync_WithIsTargeted_AppendsQueryString() + { + HttpRequestMessage? capturedRequest = null; + Mock mockHttpMessageHandler = new(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }); + + HttpClient httpClient = new(mockHttpMessageHandler.Object); + ConversationClient conversationClient = new(httpClient, NullLogger.Instance); + + await conversationClient.DeleteActivityAsync( + "conv123", + "activity123", + new Uri("https://test.service.url/"), + isTargeted: true); + + Assert.NotNull(capturedRequest); + Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); + Assert.Equal(HttpMethod.Delete, capturedRequest.Method); + } + + [Fact] + public async Task DeleteActivityAsync_WithActivity_UsesIsTargetedProperty() + { + HttpRequestMessage? capturedRequest = null; + Mock mockHttpMessageHandler = new(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }); + + HttpClient httpClient = new(mockHttpMessageHandler.Object); + ConversationClient conversationClient = new(httpClient, NullLogger.Instance); + + CoreActivity activity = new() + { + Id = "activity123", + Type = ActivityType.Message, + Conversation = new Conversation { Id = "conv123" }, + ServiceUrl = new Uri("https://test.service.url/"), + IsTargeted = true + }; + + await conversationClient.DeleteActivityAsync(activity); + + Assert.NotNull(capturedRequest); + Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); + Assert.Equal(HttpMethod.Delete, capturedRequest.Method); + } } diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs index c3f62fd0..33cfc1d2 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs @@ -480,4 +480,67 @@ public void IntegrationTest_CreateComplexActivity() Assert.Equal("conv-001", activity.Conversation.Id); Assert.NotNull(activity.ChannelData); } + + [Fact] + public void WithRecipient_DefaultsToNotTargeted() + { + CoreActivity activity = new CoreActivityBuilder() + .WithRecipient(new ConversationAccount { Id = "user-123" }) + .Build(); + + Assert.False(activity.IsTargeted); + Assert.NotNull(activity.Recipient); + Assert.Equal("user-123", activity.Recipient.Id); + } + + [Fact] + public void WithRecipient_WithIsTargetedTrue_SetsIsTargeted() + { + CoreActivity activity = new CoreActivityBuilder() + .WithRecipient(new ConversationAccount { Id = "user-123" }, true) + .Build(); + + Assert.True(activity.IsTargeted); + Assert.NotNull(activity.Recipient); + Assert.Equal("user-123", activity.Recipient.Id); + } + + [Fact] + public void WithRecipient_WithIsTargetedFalse_DoesNotSetIsTargeted() + { + CoreActivity activity = new CoreActivityBuilder() + .WithRecipient(new ConversationAccount { Id = "user-123" }, false) + .Build(); + + Assert.False(activity.IsTargeted); + Assert.NotNull(activity.Recipient); + Assert.Equal("user-123", activity.Recipient.Id); + } + + [Fact] + public void WithRecipient_Targeted_MaintainsFluentChaining() + { + CoreActivityBuilder builder = new(); + + CoreActivityBuilder result = builder.WithRecipient(new ConversationAccount { Id = "user-123" }, true); + + Assert.Same(builder, result); + } + + [Fact] + public void WithRecipient_Targeted_CanChainWithOtherMethods() + { + CoreActivity activity = new CoreActivityBuilder() + .WithType(ActivityType.Message) + .WithRecipient(new ConversationAccount { Id = "user-123", Name = "Test User" }, true) + .WithChannelId("msteams") + .Build(); + + Assert.Equal(ActivityType.Message, activity.Type); + Assert.True(activity.IsTargeted); + Assert.NotNull(activity.Recipient); + Assert.Equal("user-123", activity.Recipient.Id); + Assert.Equal("Test User", activity.Recipient.Name); + Assert.Equal("msteams", activity.ChannelId); + } } diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs index 36835f3b..240f5cf2 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs @@ -346,4 +346,53 @@ public async Task DeserializeInvokeWithValueAsync() Assert.Equal("value1", act.Value["key1"]?.GetValue()); Assert.Equal(2, act.Value["key2"]?.GetValue()); } + + [Fact] + public void IsTargeted_DefaultsToFalse() + { + CoreActivity activity = new(); + + Assert.False(activity.IsTargeted); + } + + [Fact] + public void IsTargeted_CanBeSetToTrue() + { + CoreActivity activity = new() + { + IsTargeted = true + }; + + Assert.True(activity.IsTargeted); + } + + [Fact] + public void IsTargeted_IsNotSerializedToJson() + { + CoreActivity activity = new() + { + Type = ActivityType.Message, + IsTargeted = true + }; + + string json = activity.ToJson(); + + Assert.DoesNotContain("isTargeted", json, StringComparison.OrdinalIgnoreCase); + Assert.True(activity.IsTargeted); // Property still holds value + } + + [Fact] + public void IsTargeted_IsNotDeserializedFromJson() + { + string json = """ + { + "type": "message", + "isTargeted": true + } + """; + + CoreActivity activity = CoreActivity.FromJsonString(json); + + Assert.False(activity.IsTargeted); // Should default to false since JsonIgnore + } } From 81d6a09b846186e51abbc75aa19a53e428264323 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 17 Feb 2026 22:09:00 -0800 Subject: [PATCH 2/6] Remove Recipient assertions from activity unit tests Removed checks for Recipient Id and Name in several tests in CoreActivityBuilderTests.cs and CoreActivityTests.cs to streamline test coverage. Other test logic remains unchanged. --- .../CoreActivityBuilderTests.cs | 5 ----- .../Schema/CoreActivityTests.cs | 2 -- 2 files changed, 7 deletions(-) diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs index 33cfc1d2..0e9cfba7 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs @@ -338,8 +338,6 @@ public void WithConversationReference_AppliesConversationReference() Assert.Equal("conv-123", activity.Conversation.Id); Assert.Equal("bot-1", activity.From.Id); Assert.Equal("Bot", activity.From.Name); - Assert.Equal("user-1", activity.Recipient.Id); - Assert.Equal("User One", activity.Recipient.Name); } [Fact] @@ -360,8 +358,6 @@ public void WithConversationReference_SwapsFromAndRecipient() Assert.Equal("bot-id", replyActivity.From.Id); Assert.Equal("Bot", replyActivity.From.Name); - Assert.Equal("user-id", replyActivity.Recipient.Id); - Assert.Equal("User", replyActivity.Recipient.Name); } [Fact] @@ -424,7 +420,6 @@ public void WithConversationReference_ChainedWithOtherMethods_MaintainsFluentInt Assert.Equal(ActivityType.Message, activity.Type); Assert.Equal("bot-1", activity.From.Id); - Assert.Equal("user-1", activity.Recipient.Id); } [Fact] diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs index 240f5cf2..5a0d30c7 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs @@ -293,8 +293,6 @@ public void CreateReply() Assert.Equal("conversation1", reply.Conversation.Id); Assert.Equal("bot1", reply.From.Id); Assert.Equal("Bot One", reply.From.Name); - Assert.Equal("user1", reply.Recipient.Id); - Assert.Equal("User One", reply.Recipient.Name); } [Fact] From c9f923b9dc98c31c4fbb7a48291e471c256dcb3d Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 17 Feb 2026 22:59:24 -0800 Subject: [PATCH 3/6] Add proactive TeamsActivity message in EchoBot EchoBot now sends a "Hello TM !" message using TeamsBotApplication's ConversationClient after echoing the user's input. The TeamsActivity is built with correct conversation, recipient, sender, and service URL details. Minor formatting adjustment made in Program.cs with no logic changes. --- core/samples/CompatBot/EchoBot.cs | 13 ++++++++++++- core/samples/TeamsBot/Program.cs | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/samples/CompatBot/EchoBot.cs b/core/samples/CompatBot/EchoBot.cs index 6b8da45e..4f59a5de 100644 --- a/core/samples/CompatBot/EchoBot.cs +++ b/core/samples/CompatBot/EchoBot.cs @@ -9,9 +9,9 @@ using Microsoft.Teams.Bot.Core.Schema; using Microsoft.Bot.Schema; using Microsoft.Bot.Schema.Teams; +using Newtonsoft.Json.Linq; using Microsoft.Teams.Bot.Apps; using Microsoft.Teams.Bot.Apps.Schema; -using Newtonsoft.Json.Linq; namespace CompatBot; @@ -40,6 +40,17 @@ protected override async Task OnMessageActivityAsync(ITurnContext Date: Fri, 27 Feb 2026 07:04:14 -0800 Subject: [PATCH 4/6] Update core/samples/TeamsBot/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/samples/TeamsBot/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/samples/TeamsBot/Program.cs b/core/samples/TeamsBot/Program.cs index 9e6c3e80..f932b6d1 100644 --- a/core/samples/TeamsBot/Program.cs +++ b/core/samples/TeamsBot/Program.cs @@ -31,9 +31,9 @@ await context.SendActivityAsync( TeamsActivity.CreateBuilder() .WithText($"Hello {member.Name}!") .WithRecipient(member, true) - .Build(), cancellationToken) - ; + .Build(), cancellationToken); } + await context.SendActivityAsync($"Sent a private message to {members.Count} member(s) of the conversation!", cancellationToken); }); From 3b264c7e01d828a98eb2e522e6796e13ac5f7a7d Mon Sep 17 00:00:00 2001 From: Rido Date: Fri, 27 Feb 2026 07:05:41 -0800 Subject: [PATCH 5/6] Update core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../TeamsBotApplication.HostingExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs b/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs index 6b8e0527..99285853 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs @@ -47,7 +47,7 @@ public static IServiceCollection AddTeamsBotApplication(this IServiceCollection sp.GetService>()); }); - //services.AddSingleton(); + services.AddSingleton(); services.AddBotApplication(); return services; } From 9a559a37fce853187fa47af632f1c93b46344052 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 3 Mar 2026 21:20:05 -0800 Subject: [PATCH 6/6] Refactor handlers, improve null safety, add event support - Removed unused usings and improved null handling in EchoBot.cs - Refactored Program.cs: removed "tm" handler, updated "hello" handler, changed invoke response, and added event handler - Made recipient and From properties nullable for better null safety - Updated tests to support logging - Added GlobalSuppressions.cs to suppress SYSLIB1045 warning --- core/samples/CompatBot/EchoBot.cs | 4 +-- core/samples/TeamsBot/GlobalSuppressions.cs | 8 +++++ core/samples/TeamsBot/Program.cs | 32 ++++++------------- .../ConversationClient.cs | 2 +- .../Schema/CoreActivityBuilder.cs | 2 +- .../ConversationClientTests.cs | 2 ++ 6 files changed, 22 insertions(+), 28 deletions(-) create mode 100644 core/samples/TeamsBot/GlobalSuppressions.cs diff --git a/core/samples/CompatBot/EchoBot.cs b/core/samples/CompatBot/EchoBot.cs index 59ecb1f8..8b820d6f 100644 --- a/core/samples/CompatBot/EchoBot.cs +++ b/core/samples/CompatBot/EchoBot.cs @@ -3,10 +3,8 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Teams; -using Microsoft.Bot.Connector; using Microsoft.Bot.Schema; using Microsoft.Bot.Schema.Teams; -using Newtonsoft.Json.Linq; using Microsoft.Teams.Bot.Apps; using Microsoft.Teams.Bot.Apps.Schema; using Microsoft.Teams.Bot.Compat; @@ -43,7 +41,7 @@ protected override async Task OnMessageActivityAsync(ITurnContext { await context.SendActivityAsync("Hi there! 👋 You said hello!", cancellationToken); - - await teamsApp.Api.Conversations.Reactions.AddAsync(context.Activity, "cake", cancellationToken: cancellationToken); -}); - -teamsApp.OnMessage("(?i)tm", async (context, cancellationToken) => -{ - var members = await teamsApp.Api.Conversations.Members.GetAllAsync(context.Activity, cancellationToken: cancellationToken); - foreach (var member in members) - { - await context.SendActivityAsync( - TeamsActivity.CreateBuilder() - .WithText($"Hello {member.Name}!") - .WithRecipient(member, true) - .Build(), cancellationToken); - } - - await context.SendActivityAsync($"Sent a private message to {members.Count} member(s) of the conversation!", cancellationToken); - }); // Markdown handler: matches "markdown" (case-insensitive) @@ -159,11 +141,15 @@ [Visit Microsoft](https://www.microsoft.com) await context.SendActivityAsync(reply, cancellationToken); - return new CoreInvokeResponse(200) - { - Type = "application/vnd.microsoft.activity.message", - Body = "Invokes are great !!" - }; + return AdaptiveCardResponse.CreateMessageResponse("Invokes are great!!"); +}); + +// ==================== EVENT HANDLERS ==================== + +teamsApp.OnEvent(async (context, cancellationToken) => +{ + Console.WriteLine($"[Event] Name: {context.Activity.Name}"); + await context.SendActivityAsync($"Received event: `{context.Activity.Name}`", cancellationToken); }); // ==================== CONVERSATION UPDATE HANDLERS ==================== diff --git a/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs b/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs index 06b8e9e0..b9f28e41 100644 --- a/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs +++ b/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs @@ -184,7 +184,7 @@ await DeleteActivityAsync( activity.Id, activity.ServiceUrl, activity.IsTargeted, - activity.From.GetAgenticIdentity(), + activity.From?.GetAgenticIdentity(), customHeaders, cancellationToken).ConfigureAwait(false); } diff --git a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs index a9973e96..8674f4d0 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs @@ -166,7 +166,7 @@ public TBuilder WithRecipient(ConversationAccount? recipient) /// The recipient account. /// If true, marks this as a targeted message visible only to the specified recipient. /// The builder instance for chaining. - public TBuilder WithRecipient(ConversationAccount recipient, bool isTargeted) + public TBuilder WithRecipient(ConversationAccount? recipient, bool isTargeted) { SetRecipient(recipient); _activity.IsTargeted = isTargeted; diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs index 7a9f737b..53b30dcf 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System.Net; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Teams.Bot.Core.Schema; using Moq; using Moq.Protected;