// 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) }; } } }