Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7a92aaf
Add hierarchical Teams API facade to TeamsBotApplication
rido-min Feb 17, 2026
ce1a868
Add TeamsApi REST endpoint mapping to README
rido-min Feb 17, 2026
0ce027b
Add API for adding/removing reactions to messages
rido-min Feb 18, 2026
31731d8
Merge branch 'next/core' into next/core-api-clients
rido-min Mar 4, 2026
b8e3cae
Expand Teams SDK: strong-typed invokes, events, refactoring
rido-min Mar 4, 2026
4a720ff
Add null checks and refactor config/service resolution
rido-min Mar 4, 2026
60f210e
fix slnx
rido-min Mar 4, 2026
97015ec
Add Reactions and TargetedMessage support to Core (#338)
rido-min Mar 4, 2026
c5e9025
Update tests for changes to Recipient property handling
rido-min Mar 4, 2026
27e0058
Refactor Conversation ctor, improve test coverage & reactions
rido-min Mar 4, 2026
4cf5f8f
Integrate xUnit logging into test project
rido-min Mar 4, 2026
83a0491
Add Teams message reaction logic and service URL overload
rido-min Mar 4, 2026
d0fa89a
Refactor test infra: logging, env vars, and code cleanup
rido-min Mar 4, 2026
d70704c
Add agentic app/user fields to CompatTeamsInfoTests
rido-min Mar 4, 2026
5e1187a
Refactor Teams bot hosting & middleware for ASP.NET Core
rido-min Mar 4, 2026
a4e0914
Update JSON serialization and logging in ConversationClient
rido-min Mar 4, 2026
3f4d38d
Add agentic identity support to Teams API integration tests
rido-min Mar 5, 2026
c7664c4
tm not working
rido-min Mar 5, 2026
74f3a26
Mark and serialize targeted messages in activity payloads
rido-min Mar 5, 2026
f03fc62
Include isTargeted in activity JSON serialization
rido-min Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
# local app settings files
appsettings.Local.json

.claude/

# User-specific files
*.rsuser
*.suo
Expand Down
57 changes: 55 additions & 2 deletions core/samples/CompatBot/EchoBot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Schema.Teams;
using Microsoft.Identity.Client;
using Microsoft.Teams.Bot.Apps;
using Microsoft.Teams.Bot.Apps.Schema;
using Microsoft.Teams.Bot.Compat;
Expand Down Expand Up @@ -36,15 +37,67 @@ protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivi
ConversationData conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData(), cancellationToken);

string replyText = $"Echo from BF Compat [{conversationData.MessageCount++}]: {turnContext.Activity.Text}";
await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken);

var act = MessageFactory.Text(replyText, replyText);
act.Properties.Add("isTargeted", true);
await turnContext.SendActivityAsync(act, cancellationToken);


await turnContext.SendActivityAsync(MessageFactory.Text($"Send a proactive message `/api/notify/{turnContext.Activity.Conversation.Id}`"), cancellationToken);

var activity = ((Activity)turnContext.Activity).FromCompatActivity();
TeamsActivity tm = TeamsActivity.CreateBuilder()
.WithConversation(new Conversation { Id = activity.Conversation?.Id! })
.WithText("Hello TM !")
.WithRecipient(activity.From, true)
.WithFrom(activity.Recipient)
//.WithServiceUrl(activity.ServiceUrl!)
.WithServiceUrl("https://pilot1.botapi.skype.com/amer/9a9b49fd-1dc5-4217-88b3-ecf855e91b0e/")
.Build();

//await teamsBotApp.ConversationClient.SendActivityAsync(tm, cancellationToken: cancellationToken);

var res = await turnContext.SendActivityAsync(MessageFactory.Text("I'm going to add and remove reactions to this message."), cancellationToken);

await Task.Delay(500, cancellationToken);
var ca = ((Activity)turnContext.Activity).FromCompatActivity();
await teamsBotApp.ConversationClient.AddReactionAsync(
turnContext.Activity.Conversation.Id,
res.Id,
"laugh",
//new Uri("https://pilot1.botapi.skype.com/amer/9a9b49fd-1dc5-4217-88b3-ecf855e91b0e/"),
ca.ServiceUrl!,
AgenticIdentity.FromProperties(ca.Recipient?.Properties),
null,
cancellationToken);

await Task.Delay(500, cancellationToken);
await teamsBotApp.ConversationClient.AddReactionAsync(
turnContext.Activity.Conversation.Id,
res.Id,
"sad",
ca.ServiceUrl!,
AgenticIdentity.FromProperties(ca.Recipient?.Properties),
null,
cancellationToken);

await Task.Delay(500, cancellationToken);
await teamsBotApp.ConversationClient.DeleteReactionAsync(
turnContext.Activity.Conversation.Id,
res.Id,
"laugh",
//new Uri("https://pilot1.botapi.skype.com/amer/9a9b49fd-1dc5-4217-88b3-ecf855e91b0e/"),
ca.ServiceUrl!,
AgenticIdentity.FromProperties(ca.Recipient?.Properties),
null,
cancellationToken);

// TeamsAPXClient provides Teams-specific operations like:
// - FetchTeamDetailsAsync, FetchChannelListAsync
// - FetchMeetingInfoAsync, FetchParticipantAsync, SendMeetingNotificationAsync
// - Batch messaging: SendMessageToListOfUsersAsync, SendMessageToAllUsersInTenantAsync, etc.

await SendUpdateDeleteActivityAsync(turnContext, teamsBotApp.ConversationClient, cancellationToken);
// await SendUpdateDeleteActivityAsync(turnContext, teamsBotApp.ConversationClient, cancellationToken);

Attachment attachment = new()
{
Expand Down
2 changes: 1 addition & 1 deletion core/samples/TeamsBot/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file is used by Code Analysis to maintain SuppressMessage
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
Expand Down
12 changes: 11 additions & 1 deletion core/samples/TeamsBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,19 @@ [Visit Microsoft](https://www.microsoft.com)
string replyText = $"You sent: `{context.Activity.Text}` in activity of type `{context.Activity.Type}`.";

MessageActivity reply = new(replyText);

reply.AddMention(context.Activity.From!);

await context.SendActivityAsync(reply, cancellationToken);
ArgumentNullException.ThrowIfNull(context.Activity.From);

TeamsActivity ta = TeamsActivity.CreateBuilder()
.WithType(TeamsActivityType.Message)
.WithText(replyText)
.WithRecipient(context.Activity.From, true)
.AddMention(context.Activity.From)
.Build();

await context.SendActivityAsync(ta, cancellationToken);

TeamsAttachment feedbackCard = TeamsAttachment.CreateBuilder()
.WithAdaptiveCard(Cards.FeedbackCardObj)
Expand Down
195 changes: 195 additions & 0 deletions core/src/Microsoft.Teams.Bot.Apps/Api/ActivitiesApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// 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 activity operations for sending, updating, and deleting activities in conversations.
/// </summary>
public class ActivitiesApi
{
private readonly ConversationClient _client;

/// <summary>
/// Initializes a new instance of the <see cref="ActivitiesApi"/> class.
/// </summary>
/// <param name="conversationClient">The conversation client for activity operations.</param>
internal ActivitiesApi(ConversationClient conversationClient)
{
_client = conversationClient;
}

/// <summary>
/// Sends an activity to a conversation.
/// </summary>
/// <param name="activity">The activity to send. Must contain valid conversation and service URL information.</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. The task result contains the response with the ID of the sent activity.</returns>
public Task<SendActivityResponse> SendAsync(
CoreActivity activity,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
=> _client.SendActivityAsync(activity, customHeaders, cancellationToken);

/// <summary>
/// Updates an existing activity in a conversation.
/// </summary>
/// <param name="conversationId">The ID of the conversation.</param>
/// <param name="activityId">The ID of the activity to update.</param>
/// <param name="activity">The updated activity data.</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. The task result contains the response with the ID of the updated activity.</returns>
public Task<UpdateActivityResponse> UpdateAsync(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wondering why we have disparity between send and update in extracting conv id/ activity id from activity itself? maybe we should have an overload for it similar to all other apis?

string conversationId,
string activityId,
CoreActivity activity,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
=> _client.UpdateActivityAsync(conversationId, activityId, activity, customHeaders, cancellationToken);

/// <summary>
/// Deletes an existing activity from a conversation.
/// </summary>
/// <param name="conversationId">The ID of the conversation.</param>
/// <param name="activityId">The ID of the activity to delete.</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,
Uri serviceUrl,
AgenticIdentity? agenticIdentity = null,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
=> _client.DeleteActivityAsync(conversationId, activityId, serviceUrl, agenticIdentity, customHeaders, cancellationToken);

/// <summary>
/// Deletes an existing activity from a conversation using activity context.
/// </summary>
/// <param name="activity">The activity to delete. Must contain valid Id, Conversation.Id, and ServiceUrl.</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(
CoreActivity activity,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
=> _client.DeleteActivityAsync(activity, customHeaders, cancellationToken);

/// <summary>
/// Deletes an existing activity from a conversation using Teams activity context.
/// </summary>
/// <param name="activity">The Teams activity to delete. Must contain valid Id, Conversation.Id, and ServiceUrl.</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(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need this overload for teamsactivity ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

good point, having the overload defeats the benefits of polymorphism

TeamsActivity activity,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
=> _client.DeleteActivityAsync(activity, customHeaders, cancellationToken);

/// <summary>
/// Uploads and sends historic activities to a conversation.
/// </summary>
/// <param name="conversationId">The ID of the conversation.</param>
/// <param name="transcript">The transcript containing the historic activities.</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. The task result contains the response with a resource ID.</returns>
public Task<SendConversationHistoryResponse> SendHistoryAsync(
string conversationId,
Transcript transcript,
Uri serviceUrl,
AgenticIdentity? agenticIdentity = null,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
=> _client.SendConversationHistoryAsync(conversationId, transcript, serviceUrl, agenticIdentity, customHeaders, cancellationToken);

/// <summary>
/// Uploads and sends historic activities to a conversation using activity context.
/// </summary>
/// <param name="activity">The activity providing conversation context. Must contain valid Conversation.Id and ServiceUrl.</param>
/// <param name="transcript">The transcript containing the historic activities.</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. The task result contains the response with a resource ID.</returns>
public Task<SendConversationHistoryResponse> SendHistoryAsync(
TeamsActivity activity,
Transcript transcript,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
ArgumentNullException.ThrowIfNull(activity.Conversation);
ArgumentNullException.ThrowIfNull(activity.Conversation.Id);
ArgumentNullException.ThrowIfNull(activity.ServiceUrl);

return _client.SendConversationHistoryAsync(
activity.Conversation.Id,
transcript,
activity.ServiceUrl,
activity.From?.GetAgenticIdentity(),
customHeaders,
cancellationToken);
}

/// <summary>
/// Gets the members of a specific activity.
/// </summary>
/// <param name="conversationId">The ID of the conversation.</param>
/// <param name="activityId">The ID of the activity.</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. The task result contains a list of members for the activity.</returns>
public Task<IList<ConversationAccount>> GetMembersAsync(
Copy link
Collaborator

Choose a reason for hiding this comment

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

what does "members of a specific activity" even mean? is it members of the conversation in which this activity was posted?

string conversationId,
string activityId,
Uri serviceUrl,
AgenticIdentity? agenticIdentity = null,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
=> _client.GetActivityMembersAsync(conversationId, activityId, serviceUrl, agenticIdentity, customHeaders, cancellationToken);

/// <summary>
/// Gets the members of a specific activity using activity context.
/// </summary>
/// <param name="activity">The activity to get members for. Must contain valid Id, Conversation.Id, and ServiceUrl.</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. The task result contains a list of members for the activity.</returns>
public Task<IList<ConversationAccount>> GetMembersAsync(
TeamsActivity activity,
CustomHeaders? customHeaders = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
ArgumentNullException.ThrowIfNull(activity.Id);
ArgumentNullException.ThrowIfNull(activity.Conversation);
ArgumentNullException.ThrowIfNull(activity.Conversation.Id);
ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
Comment on lines +181 to +185
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

These guards allow empty/whitespace IDs (activity.Id, activity.Conversation.Id) because they only check for null. Since these values are used as URL path segments, empty/whitespace will produce invalid endpoints. Consider using ArgumentException.ThrowIfNullOrWhiteSpace(...) for string IDs (matching patterns used elsewhere in the PR, e.g., ReactionsApi).

Copilot uses AI. Check for mistakes.

return _client.GetActivityMembersAsync(
activity.Conversation.Id,
activity.Id,
activity.ServiceUrl,
activity.From?.GetAgenticIdentity(),
customHeaders,
cancellationToken);
}
}
Loading