// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Diagnostics; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Teams.Core.Diagnostics; using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; namespace Microsoft.Teams.Core; using CustomHeaders = Dictionary; /// /// Provides methods for sending activities to a conversation endpoint using HTTP requests. /// /// The HTTP client instance used to send requests to the conversation service. Must not be null. /// The logger instance used for logging. Optional. public class ConversationClient(HttpClient httpClient, ILogger logger = default!) { private readonly BotHttpClient _botHttpClient = new(httpClient, logger); private readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; internal const string ConversationHttpClientName = "BotConversationClient"; /// /// Gets the underlying used to issue authenticated requests to the conversation service. /// Exposed so consumers can reuse the same auth-bound HTTP pipeline for channel- or platform-specific endpoints /// not modeled directly on . /// public virtual BotHttpClient BotHttpClient => _botHttpClient; /// /// Sends the specified activity to the conversation endpoint asynchronously. /// /// The activity to send. Cannot be null. Must contain a valid ServiceUrl and Conversation with an Id. /// The recipient's IsTargeted property determines if this is a targeted activity, and AgenticIdentity is extracted from the recipient's properties. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the send operation. /// A task that represents the asynchronous operation. The task result contains the response with the ID of the sent activity. /// Thrown if the activity could not be sent successfully. The exception message includes the HTTP status code and /// response content. public virtual async Task SendActivityAsync(CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); string? conversationId = activity.Conversation?.Id; ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentNullException.ThrowIfNull(activity.ServiceUrl); #pragma warning disable ExperimentalTeamsTargeted bool isTargeted = activity.Recipient?.IsTargeted == true; #pragma warning restore ExperimentalTeamsTargeted AgenticIdentity? agenticIdentity = AgenticIdentity.FromAccount(activity.From); string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/"; if (activity.ChannelId == "agents") { logger.TruncatingConversationId(); string convId = "acf"; //conversationId.Length > 100 ? conversationId[..100] : conversationId; url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(convId)}/activities/"; } if (isTargeted) { url += url.Contains('?', StringComparison.Ordinal) ? "&isTargetedActivity=true" : "?isTargetedActivity=true"; } string body = activity.ToJson(); KeyValuePair opTag = new(Telemetry.Tags.Operation, Telemetry.Operations.SendActivity); using Activity? span = Telemetry.Source.StartActivity(Telemetry.Spans.ConversationClient, ActivityKind.Client); if (span is not null) { span.SetTag(Telemetry.Tags.Operation, Telemetry.Operations.SendActivity); span.SetTag(Telemetry.Tags.ServiceUrl, activity.ServiceUrl.ToString()); span.SetTag(Telemetry.Tags.ConversationId, conversationId); span.SetTag(Telemetry.Tags.ActivityType, activity.Type); } try { SendActivityResponse? response = await _botHttpClient.SendAsync( HttpMethod.Post, url, body, CreateRequestOptions(agenticIdentity, "sending activity", customHeaders), cancellationToken).ConfigureAwait(false); span?.SetTag(Telemetry.Tags.ActivityId, response?.Id); Telemetry.OutboundCalls.Add(1, opTag); return response; } catch (Exception ex) { span.RecordException(ex); Telemetry.OutboundErrors.Add(1, opTag); throw; } } /// /// Updates an existing activity in a conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to update. Cannot be null or whitespace. /// The updated activity data. Cannot be null. /// Whether this is a targeted activity visible only to a specific recipient. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the update operation. /// A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity. /// Thrown if the activity could not be updated successfully. public virtual async Task UpdateActivityAsync(string conversationId, string activityId, CoreActivity activity, bool isTargeted = false, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentException.ThrowIfNullOrWhiteSpace(activityId); ArgumentNullException.ThrowIfNull(activity); ArgumentNullException.ThrowIfNull(activity.ServiceUrl); string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}"; if (isTargeted) { url += "?isTargetedActivity=true"; } string body = activity.ToJson(); logger.UpdatingActivity(url, body); KeyValuePair opTag = new(Telemetry.Tags.Operation, Telemetry.Operations.UpdateActivity); using Activity? span = Telemetry.Source.StartActivity(Telemetry.Spans.ConversationClient, ActivityKind.Client); if (span is not null) { span.SetTag(Telemetry.Tags.Operation, Telemetry.Operations.UpdateActivity); span.SetTag(Telemetry.Tags.ServiceUrl, activity.ServiceUrl.ToString()); span.SetTag(Telemetry.Tags.ConversationId, conversationId); span.SetTag(Telemetry.Tags.ActivityId, activityId); span.SetTag(Telemetry.Tags.ActivityType, activity.Type); } try { UpdateActivityResponse response = (await _botHttpClient.SendAsync( HttpMethod.Put, url, body, CreateRequestOptions(agenticIdentity, "updating activity", customHeaders), cancellationToken).ConfigureAwait(false))!; Telemetry.OutboundCalls.Add(1, opTag); return response; } catch (Exception ex) { span.RecordException(ex); Telemetry.OutboundErrors.Add(1, opTag); throw; } } /// /// Updates an existing targeted activity in a conversation. /// The activity body is sent with the targeted recipient to avoid "Cannot edit Recipient of Targeted Message" errors. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to update. Cannot be null or whitespace. /// The updated activity data. Cannot be null. Must contain a valid ServiceUrl. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the update operation. /// A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity. /// Thrown if the activity could not be updated successfully. public virtual async Task UpdateTargetedActivityAsync(string conversationId, string activityId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentException.ThrowIfNullOrWhiteSpace(activityId); ArgumentNullException.ThrowIfNull(activity); ArgumentNullException.ThrowIfNull(activity.ServiceUrl); string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}?isTargetedActivity=true"; string body = activity.ToJson(); logger.UpdatingTargetedActivity(url, body); KeyValuePair opTag = new(Telemetry.Tags.Operation, Telemetry.Operations.UpdateActivity); using Activity? span = Telemetry.Source.StartActivity(Telemetry.Spans.ConversationClient, ActivityKind.Client); if (span is not null) { span.SetTag(Telemetry.Tags.Operation, Telemetry.Operations.UpdateActivity); span.SetTag(Telemetry.Tags.ServiceUrl, activity.ServiceUrl.ToString()); span.SetTag(Telemetry.Tags.ConversationId, conversationId); span.SetTag(Telemetry.Tags.ActivityId, activityId); span.SetTag(Telemetry.Tags.ActivityType, activity.Type); } try { UpdateActivityResponse response = (await _botHttpClient.SendAsync( HttpMethod.Put, url, body, CreateRequestOptions(agenticIdentity, "updating targeted activity", customHeaders), cancellationToken).ConfigureAwait(false))!; Telemetry.OutboundCalls.Add(1, opTag); return response; } catch (Exception ex) { span.RecordException(ex); Telemetry.OutboundErrors.Add(1, opTag); throw; } } /// /// Deletes an existing targeted activity from a conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to delete. Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the delete operation. /// A task that represents the asynchronous operation. /// Thrown if the activity could not be deleted successfully. public virtual Task DeleteTargetedActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) => DeleteActivityAsync(conversationId, activityId, serviceUrl, isTargeted: true, agenticIdentity, customHeaders, cancellationToken); /// /// Deletes an existing activity from a conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to delete. Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the delete operation. /// A task that represents the asynchronous operation. /// Thrown if the activity could not be deleted successfully. public virtual Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) => DeleteActivityAsync(conversationId, activityId, serviceUrl, isTargeted: false, agenticIdentity, customHeaders, cancellationToken); /// /// Deletes an existing activity from a conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to delete. Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// If true, deletes a targeted activity. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the delete operation. /// A task that represents the asynchronous operation. /// Thrown if the activity could not be deleted successfully. public virtual async Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, bool isTargeted, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentException.ThrowIfNullOrWhiteSpace(activityId); ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}"; if (isTargeted) { url += "?isTargetedActivity=true"; } KeyValuePair opTag = new(Telemetry.Tags.Operation, Telemetry.Operations.DeleteActivity); using Activity? span = Telemetry.Source.StartActivity(Telemetry.Spans.ConversationClient, ActivityKind.Client); if (span is not null) { span.SetTag(Telemetry.Tags.Operation, Telemetry.Operations.DeleteActivity); span.SetTag(Telemetry.Tags.ServiceUrl, serviceUrl.ToString()); span.SetTag(Telemetry.Tags.ConversationId, conversationId); span.SetTag(Telemetry.Tags.ActivityId, activityId); } try { await _botHttpClient.SendAsync( HttpMethod.Delete, url, body: null, CreateRequestOptions(agenticIdentity, "deleting activity", customHeaders), cancellationToken).ConfigureAwait(false); Telemetry.OutboundCalls.Add(1, opTag); } catch (Exception ex) { span.RecordException(ex); Telemetry.OutboundErrors.Add(1, opTag); throw; } } /// /// Deletes an existing activity from a conversation using activity context. /// /// The ID of the conversation. /// The activity to delete. Must contain valid Id and ServiceUrl. Cannot be null. /// Whether this is a targeted activity. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the delete operation. /// A task that represents the asynchronous operation. /// Thrown if the activity could not be deleted successfully. public virtual async Task DeleteActivityAsync(string conversationId, CoreActivity activity, bool isTargeted = false, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); ArgumentException.ThrowIfNullOrWhiteSpace(activity.Id); ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentNullException.ThrowIfNull(activity.ServiceUrl); await DeleteActivityAsync( conversationId, activity.Id, activity.ServiceUrl, isTargeted, agenticIdentity, customHeaders, cancellationToken).ConfigureAwait(false); } /// /// Gets the members of a conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains a list of conversation members. /// Thrown if the members could not be retrieved successfully. public virtual async Task> GetConversationMembersAsync(string conversationId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/members"; return (await _botHttpClient.SendAsync>( HttpMethod.Get, url, body: null, CreateRequestOptions(agenticIdentity, "getting conversation members", customHeaders), cancellationToken).ConfigureAwait(false))!; } /// /// Gets a specific member of a conversation with strongly-typed result. /// /// The type of conversation account to return. Must inherit from . /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the user to retrieve. Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// /// A task that represents the asynchronous operation. The task result contains the conversation member /// of type T with detailed information about the user. /// /// Thrown if the member could not be retrieved successfully. public virtual async Task GetConversationMemberAsync(string conversationId, string userId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) where T : ChannelAccount { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentNullException.ThrowIfNull(serviceUrl); ArgumentException.ThrowIfNullOrWhiteSpace(userId); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/members/{Uri.EscapeDataString(userId)}"; return (await _botHttpClient.SendAsync( HttpMethod.Get, url, body: null, CreateRequestOptions(agenticIdentity, "getting conversation member", customHeaders), cancellationToken).ConfigureAwait(false))!; } /// /// Gets the conversations in which the bot has participated. /// /// The service URL for the bot. Cannot be null. /// Optional continuation token for pagination. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains the conversations and an optional continuation token. /// Thrown if the conversations could not be retrieved successfully. public virtual async Task GetConversationsAsync(Uri serviceUrl, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations"; if (!string.IsNullOrWhiteSpace(continuationToken)) { url += $"?continuationToken={Uri.EscapeDataString(continuationToken)}"; } return (await _botHttpClient.SendAsync( HttpMethod.Get, url, body: null, CreateRequestOptions(agenticIdentity, "getting conversations", customHeaders), cancellationToken).ConfigureAwait(false))!; } /// /// Gets the members of a specific activity. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity. Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains a list of members for the activity. /// Thrown if the activity members could not be retrieved successfully. public virtual async Task> GetActivityMembersAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentException.ThrowIfNullOrWhiteSpace(activityId); ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}/members"; return (await _botHttpClient.SendAsync>( HttpMethod.Get, url, body: null, CreateRequestOptions(agenticIdentity, "getting activity members", customHeaders), cancellationToken).ConfigureAwait(false))!; } /// /// Creates a new conversation. /// /// The parameters for creating the conversation. Cannot be null. /// The service URL for the bot. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains the conversation resource response with the conversation ID. /// Thrown if the conversation could not be created successfully. public virtual async Task CreateConversationAsync(ConversationParameters parameters, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations"; string paramsJson = JsonSerializer.Serialize(parameters, _jsonSerializerOptions); logger.CreatingConversation(url, paramsJson); return (await _botHttpClient.SendAsync( HttpMethod.Post, url, paramsJson, CreateRequestOptions(agenticIdentity, "creating conversation", customHeaders), cancellationToken).ConfigureAwait(false))!; } /// /// Gets the members of a conversation one page at a time. /// /// The ID of the conversation. Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional page size for the number of members to retrieve. /// Optional continuation token for pagination. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains a page of members and an optional continuation token. /// Thrown if the conversation members could not be retrieved successfully. public virtual async Task GetConversationPagedMembersAsync(string conversationId, Uri serviceUrl, int? pageSize = null, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/pagedmembers"; List queryParams = []; if (pageSize.HasValue) { queryParams.Add($"pageSize={pageSize.Value}"); } if (!string.IsNullOrWhiteSpace(continuationToken)) { queryParams.Add($"continuationToken={Uri.EscapeDataString(continuationToken)}"); } if (queryParams.Count > 0) { url += $"?{string.Join("&", queryParams)}"; } return (await _botHttpClient.SendAsync( HttpMethod.Get, url, body: null, CreateRequestOptions(agenticIdentity, "getting paged conversation members", customHeaders), cancellationToken).ConfigureAwait(false))!; } /// /// Deletes a member from a conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the member to delete. Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. /// Thrown if the member could not be deleted successfully. /// If the deleted member was the last member of the conversation, the conversation is also deleted. public virtual async Task DeleteConversationMemberAsync(string conversationId, string memberId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentException.ThrowIfNullOrWhiteSpace(memberId); ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/members/{Uri.EscapeDataString(memberId)}"; await _botHttpClient.SendAsync( HttpMethod.Delete, url, body: null, CreateRequestOptions(agenticIdentity, "deleting conversation member", customHeaders), cancellationToken).ConfigureAwait(false); } /// /// Uploads and sends historic activities to the conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The transcript containing the historic activities. Cannot be null. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains the response with a resource ID. /// Thrown if the history could not be sent successfully. /// Activities in the transcript must have unique IDs and appropriate timestamps for proper rendering. public virtual async Task SendConversationHistoryAsync(string conversationId, Transcript transcript, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentNullException.ThrowIfNull(transcript); ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/history"; string transcriptJson = JsonSerializer.Serialize(transcript, _jsonSerializerOptions); logger.SendingConversationHistory(url, transcriptJson); return (await _botHttpClient.SendAsync( HttpMethod.Post, url, transcriptJson, CreateRequestOptions(agenticIdentity, "sending conversation history", customHeaders), cancellationToken).ConfigureAwait(false))!; } /// /// Uploads an attachment to the channel's blob storage. /// /// The ID of the conversation. Cannot be null or whitespace. /// The attachment data to upload. Cannot be null. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains the response with an attachment ID. /// Thrown if the attachment could not be uploaded successfully. /// This is useful for storing data in a compliant store when dealing with enterprises. public virtual async Task UploadAttachmentAsync(string conversationId, AttachmentData attachmentData, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentNullException.ThrowIfNull(attachmentData); ArgumentNullException.ThrowIfNull(serviceUrl); string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/attachments"; string attachmentDataJson = JsonSerializer.Serialize(attachmentData, _jsonSerializerOptions); logger.UploadingAttachment(url, attachmentDataJson); return (await _botHttpClient.SendAsync( HttpMethod.Post, url, attachmentDataJson, CreateRequestOptions(agenticIdentity, "uploading attachment", customHeaders), cancellationToken).ConfigureAwait(false))!; } /// /// Adds a reaction to an activity in a conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to react to. Cannot be null or whitespace. /// The type of reaction to add (e.g., "like", "heart", "laugh"). Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. /// Thrown if the reaction could not be added successfully. public virtual 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/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}/reactions/{Uri.EscapeDataString(reactionType)}"; await _botHttpClient.SendAsync( HttpMethod.Put, url, body: null, CreateRequestOptions(agenticIdentity, "adding reaction", customHeaders), cancellationToken).ConfigureAwait(false); } /// /// Removes a reaction from an activity in a conversation. /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to remove the reaction from. Cannot be null or whitespace. /// The type of reaction to remove (e.g., "like", "heart", "laugh"). Cannot be null or whitespace. /// The service URL for the conversation. Cannot be null. /// Optional agentic identity for authentication. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. /// Thrown if the reaction could not be removed successfully. public virtual 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/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}/reactions/{Uri.EscapeDataString(reactionType)}"; await _botHttpClient.SendAsync( HttpMethod.Delete, url, body: null, CreateRequestOptions(agenticIdentity, "deleting reaction", customHeaders), cancellationToken).ConfigureAwait(false); } private static BotRequestOptions CreateRequestOptions(AgenticIdentity? agenticIdentity, string operationDescription, CustomHeaders? customHeaders) => new() { AgenticIdentity = agenticIdentity, OperationDescription = operationDescription, CustomHeaders = customHeaders }; }