Add Reactions and TargetedMessage support to Core#338
Add Reactions and TargetedMessage support to Core#338rido-min merged 7 commits intonext/core-api-clientsfrom
Conversation
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/.
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.
core/samples/TeamsBot/Program.cs
Outdated
| await context.SendActivityAsync( | ||
| TeamsActivity.CreateBuilder() | ||
| .WithText($"Hello {member.Name}!") | ||
| .WithRecipient(member, true) |
| /// <summary> | ||
| /// String constants for reaction types. | ||
| /// </summary> | ||
| public static class ReactionTypes |
There was a problem hiding this comment.
I'm not sure if we should keep this "incomplete by definition" list
core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs
Outdated
Show resolved
Hide resolved
| SetConversation(activity.Conversation); | ||
| SetFrom(activity.Recipient); | ||
| SetRecipient(activity.From); | ||
| //SetRecipient(activity.From); |
There was a problem hiding this comment.
I want to callout this important change. AFAIK Recipient was never required, so now making it explicit and allow to set the TM flag.
There was a problem hiding this comment.
is this supposed to be commented out? if so better to remove it?
There was a problem hiding this comment.
I keep this commented while we decide what to do with the Recipient
| 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); |
There was a problem hiding this comment.
note this deletion, if we decide to keep Recipient in WithConversationReference we should restore these asserts
There was a problem hiding this comment.
Pull request overview
This pull request introduces support for message reactions and targeted messaging in the Teams Bot SDK. The changes add a new ReactionsApi for managing reactions on conversation activities, expand the available reaction types, and enhance message delivery to support targeted (private) messages visible only to specific recipients. The implementation includes both low-level client methods and high-level API abstractions.
Changes:
- Added
ReactionsApiwith methods to add and remove reactions on activities, integrated intoConversationsApi - Implemented targeted messaging support via
IsTargetedproperty onCoreActivity, with query parameter handling inSendActivityAsync,UpdateActivityAsync, andDeleteActivityAsync - Expanded
ReactionTypesconstants to include checkmark, hourglass, pushpin, and exclamation reactions, while removing the unusedplusOnereaction
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs | Added IsTargeted property marked with [JsonIgnore] for SDK-internal routing control |
| core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs | Added WithRecipient(recipient, isTargeted) overload and commented out recipient setting in WithConversationReference |
| core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs | Added query parameter handling for targeted activities and new AddReactionAsync/DeleteReactionAsync methods |
| core/src/Microsoft.Teams.Bot.Apps/Api/ReactionsApi.cs | New API class providing high-level methods for adding and removing reactions |
| core/src/Microsoft.Teams.Bot.Apps/Api/ConversationsApi.cs | Integrated ReactionsApi as a new property |
| core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivities/MessageReactionActivity.cs | Updated reaction type constants with emoji descriptions and added new types |
| core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs | Commented out Router service registration |
| core/samples/TeamsBot/Program.cs | Added sample code demonstrating reactions and targeted messages |
| core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs | Added tests for IsTargeted property behavior and removed recipient assertions |
| core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs | Added tests for WithRecipient with targeted parameter and removed recipient assertions |
| core/test/Microsoft.Teams.Bot.Core.UnitTests/ConversationClientTests.cs | Added tests for targeted activity query string handling |
| .gitignore | Added .claude/ directory to ignore list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| return _client.DeleteReactionAsync( | ||
| activity.Conversation.Id, | ||
| activity.Id, | ||
| reactionType, | ||
| activity.ServiceUrl, | ||
| activity.Recipient.GetAgenticIdentity(), |
There was a problem hiding this comment.
The ReactionsApi uses activity.Recipient.GetAgenticIdentity() for extracting the agentic identity, but this is inconsistent with other API classes in the codebase. MembersApi and ActivitiesApi both use activity.From.GetAgenticIdentity() when dealing with activity context. The bot's identity should come from the From field (which represents the bot), not the Recipient field (which represents the user/target of the activity). This will likely cause authentication issues when the bot attempts to delete reactions.
| return _client.DeleteReactionAsync( | |
| activity.Conversation.Id, | |
| activity.Id, | |
| reactionType, | |
| activity.ServiceUrl, | |
| activity.Recipient.GetAgenticIdentity(), | |
| ArgumentNullException.ThrowIfNull(activity.From); | |
| return _client.DeleteReactionAsync( | |
| activity.Conversation.Id, | |
| activity.Id, | |
| reactionType, | |
| activity.ServiceUrl, | |
| activity.From.GetAgenticIdentity(), |
| SetConversation(activity.Conversation); | ||
| SetFrom(activity.Recipient); | ||
| SetRecipient(activity.From); | ||
| //SetRecipient(activity.From); |
There was a problem hiding this comment.
The recipient is being commented out without explanation, breaking the behavior of WithConversationReference. This method is intended to swap the From and Recipient fields when creating a reply (the bot becomes From, and the original sender becomes Recipient). By commenting out the SetRecipient call, replies created using this method will not have a recipient, which could break targeting and routing. This change appears intentional given the PR's focus on targeted messaging, but needs clarification on why the recipient is being removed when creating conversation references.
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.Teams.Bot.Apps.Schema; | ||
| using Microsoft.Teams.Bot.Core; | ||
| using Microsoft.Teams.Bot.Core.Schema; | ||
|
|
||
| namespace Microsoft.Teams.Bot.Apps.Api; | ||
|
|
||
| using CustomHeaders = Dictionary<string, string>; | ||
|
|
||
| /// <summary> | ||
| /// Provides reaction operations for adding and removing reactions on activities in conversations. | ||
| /// </summary> | ||
| public class ReactionsApi | ||
| { | ||
| private readonly ConversationClient _client; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="ReactionsApi"/> class. | ||
| /// </summary> | ||
| /// <param name="conversationClient">The conversation client for reaction operations.</param> | ||
| internal ReactionsApi(ConversationClient conversationClient) | ||
| { | ||
| _client = conversationClient; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a reaction to an activity in a conversation. | ||
| /// </summary> | ||
| /// <param name="conversationId">The ID of the conversation.</param> | ||
| /// <param name="activityId">The ID of the activity to react to.</param> | ||
| /// <param name="reactionType">The type of reaction to add (e.g., "like", "heart", "laugh").</param> | ||
| /// <param name="serviceUrl">The service URL for the conversation.</param> | ||
| /// <param name="agenticIdentity">Optional agentic identity for authentication.</param> | ||
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation.</returns> | ||
| public Task AddAsync( | ||
| string conversationId, | ||
| string activityId, | ||
| string reactionType, | ||
| Uri serviceUrl, | ||
| AgenticIdentity? agenticIdentity = null, | ||
| CustomHeaders? customHeaders = null, | ||
| CancellationToken cancellationToken = default) | ||
| => _client.AddReactionAsync(conversationId, activityId, reactionType, serviceUrl, agenticIdentity, customHeaders, cancellationToken); | ||
|
|
||
| /// <summary> | ||
| /// Adds a reaction to an activity using activity context. | ||
| /// </summary> | ||
| /// <param name="activity">The activity to react to. Must contain valid Id, Conversation.Id, and ServiceUrl.</param> | ||
| /// <param name="reactionType">The type of reaction to add (e.g., "like", "heart", "laugh").</param> | ||
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation.</returns> | ||
| public Task AddAsync( | ||
| TeamsActivity activity, | ||
| string reactionType, | ||
| CustomHeaders? customHeaders = null, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(activity); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(activity.Id); | ||
| ArgumentNullException.ThrowIfNull(activity.Conversation); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id); | ||
| ArgumentNullException.ThrowIfNull(activity.ServiceUrl); | ||
|
|
||
| return _client.AddReactionAsync( | ||
| activity.Conversation.Id, | ||
| activity.Id, | ||
| reactionType, | ||
| activity.ServiceUrl, | ||
| activity.Recipient.GetAgenticIdentity(), | ||
| customHeaders, | ||
| cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Removes a reaction from an activity in a conversation. | ||
| /// </summary> | ||
| /// <param name="conversationId">The ID of the conversation.</param> | ||
| /// <param name="activityId">The ID of the activity to remove the reaction from.</param> | ||
| /// <param name="reactionType">The type of reaction to remove (e.g., "like", "heart", "laugh").</param> | ||
| /// <param name="serviceUrl">The service URL for the conversation.</param> | ||
| /// <param name="agenticIdentity">Optional agentic identity for authentication.</param> | ||
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation.</returns> | ||
| public Task DeleteAsync( | ||
| string conversationId, | ||
| string activityId, | ||
| string reactionType, | ||
| Uri serviceUrl, | ||
| AgenticIdentity? agenticIdentity = null, | ||
| CustomHeaders? customHeaders = null, | ||
| CancellationToken cancellationToken = default) | ||
| => _client.DeleteReactionAsync(conversationId, activityId, reactionType, serviceUrl, agenticIdentity, customHeaders, cancellationToken); | ||
|
|
||
| /// <summary> | ||
| /// Removes a reaction from an activity using activity context. | ||
| /// </summary> | ||
| /// <param name="activity">The activity to remove the reaction from. Must contain valid Id, Conversation.Id, and ServiceUrl.</param> | ||
| /// <param name="reactionType">The type of reaction to remove (e.g., "like", "heart", "laugh").</param> | ||
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation.</returns> | ||
| public Task DeleteAsync( | ||
| TeamsActivity activity, | ||
| string reactionType, | ||
| CustomHeaders? customHeaders = null, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(activity); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(activity.Id); | ||
| ArgumentNullException.ThrowIfNull(activity.Conversation); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id); | ||
| ArgumentNullException.ThrowIfNull(activity.ServiceUrl); | ||
|
|
||
| return _client.DeleteReactionAsync( | ||
| activity.Conversation.Id, | ||
| activity.Id, | ||
| reactionType, | ||
| activity.ServiceUrl, | ||
| activity.Recipient.GetAgenticIdentity(), | ||
| customHeaders, | ||
| cancellationToken); | ||
| } | ||
| } |
There was a problem hiding this comment.
The new ReactionsApi functionality lacks test coverage. Similar API classes like MembersApi and ActivitiesApi have comprehensive test coverage, but there are no tests for AddReactionAsync or DeleteReactionAsync methods in the ConversationClient, and no tests for the ReactionsApi class itself. This is a gap in test coverage for new functionality.
| activity.Id, | ||
| reactionType, | ||
| activity.ServiceUrl, | ||
| activity.Recipient.GetAgenticIdentity(), |
There was a problem hiding this comment.
The ReactionsApi uses activity.Recipient.GetAgenticIdentity() for extracting the agentic identity, but this is inconsistent with other API classes in the codebase. MembersApi and ActivitiesApi both use activity.From.GetAgenticIdentity() when dealing with activity context. The bot's identity should come from the From field (which represents the bot), not the Recipient field (which represents the user/target of the activity). This will likely cause authentication issues when the bot attempts to add or remove reactions.
| activity.Recipient.GetAgenticIdentity(), | |
| activity.From.GetAgenticIdentity(), |
core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs
Outdated
Show resolved
Hide resolved
| /// <summary> | ||
| /// Adds a reaction to an activity in a conversation. | ||
| /// </summary> | ||
| /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param> | ||
| /// <param name="activityId">The ID of the activity to react to. Cannot be null or whitespace.</param> | ||
| /// <param name="reactionType">The type of reaction to add (e.g., "like", "heart", "laugh"). Cannot be null or whitespace.</param> | ||
| /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param> | ||
| /// <param name="agenticIdentity">Optional agentic identity for authentication.</param> | ||
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation.</returns> | ||
| /// <exception cref="HttpRequestException">Thrown if the reaction could not be added successfully.</exception> | ||
| public async Task AddReactionAsync(string conversationId, string activityId, string reactionType, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) | ||
| { | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(activityId); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(reactionType); | ||
| ArgumentNullException.ThrowIfNull(serviceUrl); | ||
|
|
||
| string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}"; | ||
|
|
||
| logger.LogTrace("Adding reaction at {Url}", url); | ||
|
|
||
| await _botHttpClient.SendAsync( | ||
| HttpMethod.Put, | ||
| url, | ||
| body: null, | ||
| CreateRequestOptions(agenticIdentity, "adding reaction", customHeaders), | ||
| cancellationToken).ConfigureAwait(false); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Removes a reaction from an activity in a conversation. | ||
| /// </summary> | ||
| /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param> | ||
| /// <param name="activityId">The ID of the activity to remove the reaction from. Cannot be null or whitespace.</param> | ||
| /// <param name="reactionType">The type of reaction to remove (e.g., "like", "heart", "laugh"). Cannot be null or whitespace.</param> | ||
| /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param> | ||
| /// <param name="agenticIdentity">Optional agentic identity for authentication.</param> | ||
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation.</returns> | ||
| /// <exception cref="HttpRequestException">Thrown if the reaction could not be removed successfully.</exception> | ||
| public async Task DeleteReactionAsync(string conversationId, string activityId, string reactionType, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) | ||
| { | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(activityId); | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(reactionType); | ||
| ArgumentNullException.ThrowIfNull(serviceUrl); | ||
|
|
||
| string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}"; | ||
|
|
||
| logger.LogTrace("Deleting reaction at {Url}", url); | ||
|
|
||
| await _botHttpClient.SendAsync( | ||
| HttpMethod.Delete, | ||
| url, | ||
| body: null, | ||
| CreateRequestOptions(agenticIdentity, "deleting reaction", customHeaders), | ||
| cancellationToken).ConfigureAwait(false); | ||
| } |
There was a problem hiding this comment.
Test coverage is missing for the new AddReactionAsync and DeleteReactionAsync methods. While the targeted message functionality is well tested, the new reaction methods added to ConversationClient do not have corresponding unit tests to verify URL construction, parameter validation, or HTTP method usage.
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/TeamsBot/Program.cs
Outdated
| return new CoreInvokeResponse(200) | ||
| { | ||
| { | ||
| Type = "application/vnd.microsoft.activity.message", |
There was a problem hiding this comment.
Might need to do a slightly complicated merge with next/core to update invokes
| SetConversation(activity.Conversation); | ||
| SetFrom(activity.Recipient); | ||
| SetRecipient(activity.From); | ||
| //SetRecipient(activity.From); |
There was a problem hiding this comment.
is this supposed to be commented out? if so better to remove it?
| /// 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. | ||
| /// </summary> | ||
| [JsonIgnore] public bool IsTargeted { get; set; } |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…xtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Major refactor introducing strongly-typed activity and handler model, improved nullability, and extensibility across Teams Bot SDK core and app layers. Key changes: - All activity schema types (From, Recipient, Conversation, etc.) are now nullable; builders and conversion methods updated for null-safety. - Introduced strongly-typed activity classes and handlers for invoke, event, message extension, meeting, adaptive card, file consent, and task module scenarios. - New builder patterns for invoke and task module responses. - Router enforces unique/catch-all invoke handler rules. - Refactored BotApplication to use BotApplicationOptions for AppId, removed IConfiguration dependency, and improved logging. - Added KeyedBotAuthenticationHandler for multi-bot MSAL scenarios. - ConversationClient now supports ReplyToId and truncates agent channel IDs. - All schema/entities unified under a single namespace; improved property accessors. - Added comprehensive sample bots and updated test coverage for all major Teams bot scenarios. - General code cleanup, copyright headers, and updated suppressions. These changes modernize the SDK, improve developer ergonomics, and provide robust samples for all Teams bot scenarios.
- 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
This pull request introduces support for message reactions in the Teams bot SDK, along with improvements for targeted messaging. The changes add a new
ReactionsApifor handling reactions, expand the list of supported reaction types, and enhance message delivery to allow targeting specific recipients. The most important updates are grouped below:Message Reaction Support
ReactionsApiclass, enabling bots to add and remove reactions (like, heart, laugh, etc.) on conversation activities. This includes both direct and context-based methods for reaction management. (core/src/Microsoft.Teams.Bot.Apps/Api/ReactionsApi.cs)ReactionsApiintoConversationsApi, exposing it via theReactionsproperty and updating documentation to reflect its availability. (core/src/Microsoft.Teams.Bot.Apps/Api/ConversationsApi.cs) [1] [2] [3]AddReactionAsyncandDeleteReactionAsyncmethods inConversationClientto support adding/removing reactions through HTTP calls. (core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs)Expanded Reaction Types
ReactionTypesclass to include new reactions (checkmark, hourglass, pushpin, exclamation) and clarified existing reactions with emoji descriptions. Removed the unusedplusOnereaction. (core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivities/MessageReactionActivity.cs)Targeted Messaging Enhancements
IsTargetedproperty toCoreActivityand updated activity cloning to preserve this flag, enabling messages to be directed privately to specific recipients. (core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs) [1] [2]SendActivityAsync,UpdateActivityAsync, andDeleteActivityAsyncmethods inConversationClientto handle targeted activities by appending theisTargetedActivity=truequery parameter when appropriate. (core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs) [1] [2] [3] [4]CoreActivityBuilderto allow specifying targeted recipients via theWithRecipient(recipient, isTargeted)method. (core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs)Sample Usage
core/samples/TeamsBot/Program.cs) to demonstrate reaction usage (adding a "cake" reaction when "hello" is received) and sending targeted messages to conversation members.These changes collectively make it easier for bot developers to manage reactions and deliver targeted messages within Teams conversations.