Skip to content

Add Reactions and TargetedMessage support to Core#338

Open
rido-min wants to merge 4 commits intonext/core-api-clientsfrom
next/core-api-clients-tm
Open

Add Reactions and TargetedMessage support to Core#338
rido-min wants to merge 4 commits intonext/core-api-clientsfrom
next/core-api-clients-tm

Conversation

@rido-min
Copy link
Member

This pull request introduces support for message reactions in the Teams bot SDK, along with improvements for targeted messaging. The changes add a new ReactionsApi for 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

  • Added a new ReactionsApi class, 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)
  • Integrated ReactionsApi into ConversationsApi, exposing it via the Reactions property and updating documentation to reflect its availability. (core/src/Microsoft.Teams.Bot.Apps/Api/ConversationsApi.cs) [1] [2] [3]
  • Implemented AddReactionAsync and DeleteReactionAsync methods in ConversationClient to support adding/removing reactions through HTTP calls. (core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs)

Expanded Reaction Types

  • Extended the ReactionTypes class to include new reactions (checkmark, hourglass, pushpin, exclamation) and clarified existing reactions with emoji descriptions. Removed the unused plusOne reaction. (core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivities/MessageReactionActivity.cs)

Targeted Messaging Enhancements

  • Added an IsTargeted property to CoreActivity and 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]
  • Modified SendActivityAsync, UpdateActivityAsync, and DeleteActivityAsync methods in ConversationClient to handle targeted activities by appending the isTargetedActivity=true query parameter when appropriate. (core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs) [1] [2] [3] [4]
  • Updated CoreActivityBuilder to allow specifying targeted recipients via the WithRecipient(recipient, isTargeted) method. (core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs)

Sample Usage

  • Enhanced the Teams bot sample (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.

Introduces ReactionsApi for programmatic message reactions in Teams bots, with AddAsync and DeleteAsync methods. ConversationClient now supports AddReactionAsync and DeleteReactionAsync. Expanded ReactionTypes with more documented types. Demonstrated usage in Program.cs by adding a "cake" reaction to "hello" messages.
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.
await context.SendActivityAsync(
TeamsActivity.CreateBuilder()
.WithText($"Hello {member.Name}!")
.WithRecipient(member, true)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targeted message

@@ -109,39 +109,54 @@ public class MessageReaction
public static class ReactionTypes
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should keep this "incomplete by definition" list

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

});

services.AddSingleton<Router>();
//services.AddSingleton<Router>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got a DI runtime error b/c the Router logger.. adding a note here to have test to verify

SetConversation(activity.Conversation);
SetFrom(activity.Recipient);
SetRecipient(activity.From);
//SetRecipient(activity.From);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to callout this important change. AFAIK Recipient was never required, so now making it explicit and allow to set the TM flag.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this supposed to be commented out? if so better to remove it?

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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note this deletion, if we decide to keep Recipient in WithConversationReference we should restore these asserts

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ReactionsApi with methods to add and remove reactions on activities, integrated into ConversationsApi
  • Implemented targeted messaging support via IsTargeted property on CoreActivity, with query parameter handling in SendActivityAsync, UpdateActivityAsync, and DeleteActivityAsync
  • Expanded ReactionTypes constants to include checkmark, hourglass, pushpin, and exclamation reactions, while removing the unused plusOne reaction

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.

Comment on lines +119 to +125

return _client.DeleteReactionAsync(
activity.Conversation.Id,
activity.Id,
reactionType,
activity.ServiceUrl,
activity.Recipient.GetAgenticIdentity(),
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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(),

Copilot uses AI. Check for mistakes.
SetConversation(activity.Conversation);
SetFrom(activity.Recipient);
SetRecipient(activity.From);
//SetRecipient(activity.From);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +129
// 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);
}
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +36
.Build(), cancellationToken)
;
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sample code has inconsistent formatting with a semicolon on a separate line (line 35). While this is valid C#, it's unconventional and should be moved to the end of the previous line for consistency with the rest of the codebase.

Suggested change
.Build(), cancellationToken)
;
}
.Build(), cancellationToken);
}

Copilot uses AI. Check for mistakes.
activity.Id,
reactionType,
activity.ServiceUrl,
activity.Recipient.GetAgenticIdentity(),
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
activity.Recipient.GetAgenticIdentity(),
activity.From.GetAgenticIdentity(),

Copilot uses AI. Check for mistakes.
});

services.AddSingleton<Router>();
//services.AddSingleton<Router>();
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Router service registration has been commented out without explanation. This is a critical infrastructure component used throughout the bot application (see TeamsBotApplication.cs which initializes Router in the constructor and uses it in ProcessAsync). Commenting this out breaks the dependency injection and will cause runtime failures when the bot tries to use the Router.

Suggested change
//services.AddSingleton<Router>();
services.AddSingleton<Router>();

Copilot uses AI. Check for mistakes.
Comment on lines +467 to +527
/// <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);
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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.
return new CoreInvokeResponse(200)
{
{
Type = "application/vnd.microsoft.activity.message",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot will JsonIgnore ignore when deserializing?

@rido-min ignoring means we would have to manually support some how setting this field in BF SDK activity.

Copy link
Contributor

Copilot AI commented Feb 25, 2026

@singhk97 I've opened a new pull request, #349, to work on those changes. Once the pull request is ready, I'll request review from you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants