// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.Bot.Connector;
using Microsoft.Bot.Schema;
using Microsoft.Rest;
using Microsoft.Teams.Core;
using Microsoft.Teams.Core.Schema;
namespace Microsoft.Teams.Apps.BotBuilder
{
///
/// Provides a compatibility adapter that bridges the Teams Bot Core to the
/// Bot Framework's class.
///
///
/// This adapter enables legacy Bot Framework bots to use the new Teams Bot Core conversation management
/// without code changes. It converts between Bot Framework and Core activity formats, handles HTTP operation
/// responses, and manages custom header translations. All operations delegate to the underlying Core ConversationClient.
///
/// The underlying Teams Bot Core ConversationClient that performs the actual conversation operations.
internal sealed class CompatConversations(ConversationClient client) : IConversations
{
internal readonly ConversationClient _client = client;
///
/// Gets or sets the service URL for the bot service endpoint.
/// This URL is used for all conversation operations and must be set before making API calls.
///
internal string? ServiceUrl { get; set; }
///
/// Gets or sets the agentic identity extracted from the incoming activity.
/// Used for user-delegated token acquisition when the bot acts on behalf of an agentic app.
///
internal AgenticIdentity? AgenticIdentity { get; set; }
///
/// Creates a new conversation with the specified parameters.
///
/// The conversation parameters including members and activity. Cannot be null.
/// Optional custom HTTP headers to include in the request.
/// A cancellation token that can be used to cancel the asynchronous operation.
///
/// A task that represents the asynchronous operation. The task result contains an HTTP operation response with
/// a containing the conversation ID, activity ID, and service URL.
///
/// Thrown when is null or whitespace.
public async Task> CreateConversationWithHttpMessagesAsync(
Microsoft.Bot.Schema.ConversationParameters parameters,
Dictionary>? customHeaders = null,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
Microsoft.Teams.Core.ConversationParameters convoParams = parameters.FromCompatConversationParameters();
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
CreateConversationResponse res = await _client.CreateConversationAsync(
convoParams,
new Uri(ServiceUrl),
AgenticIdentity.FromAccount(convoParams.Activity?.From),
convertedHeaders,
cancellationToken).ConfigureAwait(false);
ConversationResourceResponse response = new()
{
ActivityId = res.ActivityId,
Id = res.Id,
ServiceUrl = res.ServiceUrl?.ToString(),
};
return new HttpOperationResponse
{
Body = response,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
///
/// Deletes an existing activity from a conversation.
///
/// The unique identifier of the conversation. Cannot be null or whitespace.
/// The unique identifier of the activity to delete. Cannot be null or whitespace.
/// Optional custom HTTP headers to include in the request.
/// A cancellation token that can be used to cancel the asynchronous operation.
/// A task that represents the asynchronous operation. The task result contains an HTTP operation response.
/// Thrown when is null or whitespace.
public async Task DeleteActivityWithHttpMessagesAsync(string conversationId, string activityId, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
await _client.DeleteActivityAsync(
conversationId,
activityId,
new Uri(ServiceUrl),
AgenticIdentity,
ConvertHeaders(customHeaders),
cancellationToken).ConfigureAwait(false);
return new HttpOperationResponse
{
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
public async Task DeleteConversationMemberWithHttpMessagesAsync(string conversationId, string memberId, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
await _client.DeleteConversationMemberAsync(
conversationId,
memberId,
new Uri(ServiceUrl),
AgenticIdentity,
ConvertHeaders(customHeaders),
cancellationToken).ConfigureAwait(false);
return new HttpOperationResponse { Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) };
}
public async Task>> GetActivityMembersWithHttpMessagesAsync(string conversationId, string activityId, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
IList members = await _client.GetActivityMembersAsync(
conversationId,
activityId,
new Uri(ServiceUrl!),
AgenticIdentity,
convertedHeaders,
cancellationToken).ConfigureAwait(false);
List channelAccounts = [.. members.Select(m => m.ToCompatChannelAccount())];
return new HttpOperationResponse>
{
Body = channelAccounts,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
///
/// Retrieves the list of members participating in a conversation.
///
/// The unique identifier of the conversation. Cannot be null or whitespace.
/// Optional custom HTTP headers to include in the request.
/// A cancellation token that can be used to cancel the asynchronous operation.
///
/// A task that represents the asynchronous operation. The task result contains an HTTP operation response with
/// a list of objects representing the conversation members.
///
/// Thrown when is null or whitespace.
public async Task>> GetConversationMembersWithHttpMessagesAsync(string conversationId, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
IList members = await _client.GetConversationMembersAsync(
conversationId,
new Uri(ServiceUrl),
AgenticIdentity,
convertedHeaders,
cancellationToken).ConfigureAwait(false);
List channelAccounts = [.. members.Select(m => m.ToCompatChannelAccount())];
return new HttpOperationResponse>
{
Body = channelAccounts,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
public async Task> GetConversationPagedMembersWithHttpMessagesAsync(string conversationId, int? pageSize = null, string? continuationToken = null, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
Microsoft.Teams.Core.PagedMembersResult pagedMembers = await _client.GetConversationPagedMembersAsync(
conversationId,
new Uri(ServiceUrl),
pageSize,
continuationToken,
AgenticIdentity,
convertedHeaders,
cancellationToken).ConfigureAwait(false);
Microsoft.Bot.Schema.PagedMembersResult result = new()
{
ContinuationToken = pagedMembers.ContinuationToken,
Members = pagedMembers.Members?.Select(m => m.ToCompatChannelAccount()).ToList()
};
return new HttpOperationResponse
{
Body = result,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
public async Task> GetConversationsWithHttpMessagesAsync(string? continuationToken = null, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
GetConversationsResponse conversations = await _client.GetConversationsAsync(
new Uri(ServiceUrl),
continuationToken,
AgenticIdentity,
convertedHeaders,
cancellationToken).ConfigureAwait(false);
ConversationsResult result = new()
{
ContinuationToken = conversations.ContinuationToken,
Conversations = conversations.Conversations?.Select(c => new Microsoft.Bot.Schema.ConversationMembers
{
Id = c.Id,
Members = c.Members?.Select(m => m.ToCompatChannelAccount()).ToList()
}).ToList()
};
return new HttpOperationResponse
{
Body = result,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
public async Task> ReplyToActivityWithHttpMessagesAsync(string conversationId, string activityId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
CoreActivity coreActivity = activity.FromBotFrameworkActivity();
// Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for sending activities
if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null)
{
coreActivity.ServiceUrl = new Uri(ServiceUrl);
}
coreActivity.ReplyToId = activityId;
coreActivity.Conversation = new Microsoft.Teams.Core.Schema.Conversation(conversationId);
SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false);
ResourceResponse resourceResponse = new()
{
Id = response?.Id ?? string.Empty
};
return new HttpOperationResponse
{
Body = resourceResponse,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
public async Task> SendConversationHistoryWithHttpMessagesAsync(string conversationId, Microsoft.Bot.Schema.Transcript transcript, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
Microsoft.Teams.Core.Transcript coreTranscript = new()
{
Activities = transcript.Activities?.Select(a => a.FromBotFrameworkActivity()).ToList()
};
SendConversationHistoryResponse response = await _client.SendConversationHistoryAsync(
conversationId,
coreTranscript,
new Uri(ServiceUrl),
AgenticIdentity,
convertedHeaders,
cancellationToken).ConfigureAwait(false);
ResourceResponse resourceResponse = new()
{
Id = response.Id
};
return new HttpOperationResponse
{
Body = resourceResponse,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
///
/// Sends an activity to an existing conversation.
///
/// The unique identifier of the conversation. Cannot be null or whitespace.
/// The activity to send. Cannot be null.
/// Optional custom HTTP headers to include in the request.
/// A cancellation token that can be used to cancel the asynchronous operation.
///
/// A task that represents the asynchronous operation. The task result contains an HTTP operation response with
/// a containing the ID of the sent activity.
///
public async Task> SendToConversationWithHttpMessagesAsync(string conversationId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
CoreActivity coreActivity = activity.FromBotFrameworkActivity();
// Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for sending activities
if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null)
{
coreActivity.ServiceUrl = new Uri(ServiceUrl);
}
coreActivity.Conversation = new Microsoft.Teams.Core.Schema.Conversation(conversationId);
SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false);
ResourceResponse resourceResponse = new()
{
Id = response?.Id ?? string.Empty
};
return new HttpOperationResponse
{
Body = resourceResponse,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
///
/// Updates an existing activity in a conversation.
///
/// The unique identifier of the conversation. Cannot be null or whitespace.
/// The unique identifier of the activity to update. Cannot be null or whitespace.
/// The updated activity content. Cannot be null.
/// Optional custom HTTP headers to include in the request.
/// A cancellation token that can be used to cancel the asynchronous operation.
///
/// A task that represents the asynchronous operation. The task result contains an HTTP operation response with
/// a containing the ID of the updated activity.
///
public async Task> UpdateActivityWithHttpMessagesAsync(string conversationId, string activityId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
CoreActivity coreActivity = activity.FromBotFrameworkActivity();
// Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for updating activities
if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null)
{
coreActivity.ServiceUrl = new Uri(ServiceUrl);
}
UpdateActivityResponse response = await _client.UpdateActivityAsync(conversationId, activityId, coreActivity, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false);
ResourceResponse resourceResponse = new()
{
Id = response.Id
};
return new HttpOperationResponse
{
Body = resourceResponse,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
public async Task> UploadAttachmentWithHttpMessagesAsync(string conversationId, Microsoft.Bot.Schema.AttachmentData attachmentUpload, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
Microsoft.Teams.Core.AttachmentData coreAttachmentData = new()
{
Type = attachmentUpload.Type,
Name = attachmentUpload.Name,
OriginalBase64 = attachmentUpload.OriginalBase64,
ThumbnailBase64 = attachmentUpload.ThumbnailBase64
};
UploadAttachmentResponse response = await _client.UploadAttachmentAsync(
conversationId,
coreAttachmentData,
new Uri(ServiceUrl),
AgenticIdentity,
convertedHeaders,
cancellationToken).ConfigureAwait(false);
ResourceResponse resourceResponse = new()
{
Id = response.Id
};
return new HttpOperationResponse
{
Body = resourceResponse,
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
private static Dictionary? ConvertHeaders(Dictionary>? customHeaders)
{
if (customHeaders == null)
{
return null;
}
Dictionary convertedHeaders = [];
foreach (KeyValuePair> kvp in customHeaders)
{
convertedHeaders[kvp.Key] = string.Join(",", kvp.Value);
}
return convertedHeaders;
}
public async Task> GetConversationMemberWithHttpMessagesAsync(string userId, string conversationId, Dictionary> customHeaders = null!, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
Dictionary? convertedHeaders = ConvertHeaders(customHeaders);
Microsoft.Teams.Core.Schema.ChannelAccount response = await _client.GetConversationMemberAsync(
conversationId, userId, new Uri(ServiceUrl), AgenticIdentity, convertedHeaders, cancellationToken).ConfigureAwait(false);
return new HttpOperationResponse
{
Body = response.ToCompatChannelAccount(),
Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
};
}
}
}