microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
c92c1919ec3b5a0b25a067ad4ad46da598a2fdca

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs

444lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Text.Json;
5using Microsoft.Extensions.Logging;
6using CoreAssemblyInfo;
7using Microsoft.Teams.Bot.Core.Http;
8using Microsoft.Teams.Bot.Core.Schema;
9
10namespace Microsoft.Teams.Bot.Core;
11
12using CustomHeaders = Dictionary<string, string>;
13
14/// <summary>
15/// Provides methods for sending activities to a conversation endpoint using HTTP requests.
16/// </summary>
17/// <param name="httpClient">The HTTP client instance used to send requests to the conversation service. Must not be null.</param>
18/// <param name="logger">The logger instance used for logging. Optional.</param>
19[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "<Pending>")]
20public class ConversationClient(HttpClient httpClient, ILogger<ConversationClient> logger = default!)
21{
22 private readonly BotHttpClient _botHttpClient = new(httpClient, logger);
23 internal const string ConversationHttpClientName = "BotConversationClient";
24
25 /// <summary>
26 /// Gets the default custom headers that will be included in all requests.
27 /// </summary>
28 public CustomHeaders DefaultCustomHeaders { get; } = new()
29 {
30 ["User-Agent"] = $"{ThisAssembly.AssemblyName}/{ThisAssembly.AssemblyInformationalVersion}"
31 };
32
33 /// <summary>
34 /// Sends the specified activity to the conversation endpoint asynchronously.
35 /// </summary>
36 /// <param name="activity">The activity to send. Cannot be null. The activity must contain valid conversation and service URL information.</param>
37 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
38 /// <param name="cancellationToken">A cancellation token that can be used to cancel the send operation.</param>
39 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the sent activity.</returns>
40 /// <exception cref="Exception">Thrown if the activity could not be sent successfully. The exception message includes the HTTP status code and
41 /// response content.</exception>
42 public async Task<SendActivityResponse> SendActivityAsync(CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
43 {
44 ArgumentNullException.ThrowIfNull(activity);
45 ArgumentNullException.ThrowIfNull(activity.Conversation);
46 ArgumentException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id);
47 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
48
49 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{activity.Conversation.Id}/activities/";
50
51 if (activity.ChannelId == "agents")
52 {
53 logger.LogInformation("Truncating conversation ID for 'agents' channel to comply with length restrictions.");
54 string conversationId = activity.Conversation.Id;
55 string convId = conversationId.Length > 325 ? conversationId[..325] : conversationId;
56 url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{convId}/activities";
57 }
58
59 string body = activity.ToJson();
60
61 logger?.LogTrace("Sending activity to {Url}: {Activity}", url, body);
62
63 return (await _botHttpClient.SendAsync<SendActivityResponse>(
64 HttpMethod.Post,
65 url,
66 body,
67 CreateRequestOptions(activity.From.GetAgenticIdentity(), "sending activity", customHeaders),
68 cancellationToken).ConfigureAwait(false))!;
69 }
70
71 /// <summary>
72 /// Updates an existing activity in a conversation.
73 /// </summary>
74 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
75 /// <param name="activityId">The ID of the activity to update. Cannot be null or whitespace.</param>
76 /// <param name="activity">The updated activity data. Cannot be null.</param>
77 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
78 /// <param name="cancellationToken">A cancellation token that can be used to cancel the update operation.</param>
79 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity.</returns>
80 /// <exception cref="HttpRequestException">Thrown if the activity could not be updated successfully.</exception>
81 public async Task<UpdateActivityResponse> UpdateActivityAsync(string conversationId, string activityId, CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
82 {
83 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
84 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
85 ArgumentNullException.ThrowIfNull(activity);
86 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
87
88 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}";
89 string body = activity.ToJson();
90
91 logger.LogTrace("Updating activity at {Url}: {Activity}", url, body);
92
93 return (await _botHttpClient.SendAsync<UpdateActivityResponse>(
94 HttpMethod.Put,
95 url,
96 body,
97 CreateRequestOptions(activity.From.GetAgenticIdentity(), "updating activity", customHeaders),
98 cancellationToken).ConfigureAwait(false))!;
99 }
100
101
102 /// <summary>
103 /// Deletes an existing activity from a conversation.
104 /// </summary>
105 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
106 /// <param name="activityId">The ID of the activity to delete. Cannot be null or whitespace.</param>
107 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
108 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
109 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
110 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
111 /// <returns>A task that represents the asynchronous operation.</returns>
112 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
113 public async Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
114 {
115 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
116 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
117 ArgumentNullException.ThrowIfNull(serviceUrl);
118
119 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}";
120
121 logger.LogTrace("Deleting activity at {Url}", url);
122
123 await _botHttpClient.SendAsync(
124 HttpMethod.Delete,
125 url,
126 body: null,
127 CreateRequestOptions(agenticIdentity, "deleting activity", customHeaders),
128 cancellationToken).ConfigureAwait(false);
129 }
130
131 /// <summary>
132 /// Deletes an existing activity from a conversation using activity context.
133 /// </summary>
134 /// <param name="activity">The activity to delete. Must contain valid Id, Conversation.Id, and ServiceUrl. Cannot be null.</param>
135 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
136 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
137 /// <returns>A task that represents the asynchronous operation.</returns>
138 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
139 public async Task DeleteActivityAsync(CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
140 {
141 ArgumentNullException.ThrowIfNull(activity);
142 ArgumentException.ThrowIfNullOrWhiteSpace(activity.Id);
143 ArgumentNullException.ThrowIfNull(activity.Conversation);
144 ArgumentException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id);
145 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
146
147 await DeleteActivityAsync(
148 activity.Conversation.Id,
149 activity.Id,
150 activity.ServiceUrl,
151 activity.From.GetAgenticIdentity(),
152 customHeaders,
153 cancellationToken).ConfigureAwait(false);
154 }
155
156 /// <summary>
157 /// Gets the members of a conversation.
158 /// </summary>
159 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
160 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
161 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
162 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
163 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
164 /// <returns>A task that represents the asynchronous operation. The task result contains a list of conversation members.</returns>
165 /// <exception cref="HttpRequestException">Thrown if the members could not be retrieved successfully.</exception>
166 public async Task<IList<ConversationAccount>> GetConversationMembersAsync(string conversationId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
167 {
168 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
169 ArgumentNullException.ThrowIfNull(serviceUrl);
170
171 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/members";
172
173 logger.LogTrace("Getting conversation members from {Url}", url);
174
175 return (await _botHttpClient.SendAsync<IList<ConversationAccount>>(
176 HttpMethod.Get,
177 url,
178 body: null,
179 CreateRequestOptions(agenticIdentity, "getting conversation members", customHeaders),
180 cancellationToken).ConfigureAwait(false))!;
181 }
182
183
184 /// <summary>
185 /// Gets a specific member of a conversation with strongly-typed result.
186 /// </summary>
187 /// <typeparam name="T">The type of conversation account to return. Must inherit from <see cref="ConversationAccount"/>.</typeparam>
188 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
189 /// <param name="userId">The ID of the user to retrieve. Cannot be null or whitespace.</param>
190 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
191 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
192 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
193 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
194 /// <returns>
195 /// A task that represents the asynchronous operation. The task result contains the conversation member
196 /// of type T with detailed information about the user.
197 /// </returns>
198 /// <exception cref="HttpRequestException">Thrown if the member could not be retrieved successfully.</exception>
199 public async Task<T> GetConversationMemberAsync<T>(string conversationId, string userId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) where T : ConversationAccount
200 {
201 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
202 ArgumentNullException.ThrowIfNull(serviceUrl);
203 ArgumentException.ThrowIfNullOrWhiteSpace(userId);
204
205 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/members/{userId}";
206
207 logger.LogTrace("Getting conversation members from {Url}", url);
208
209 return (await _botHttpClient.SendAsync<T>(
210 HttpMethod.Get,
211 url,
212 body: null,
213 CreateRequestOptions(agenticIdentity, "getting conversation member", customHeaders),
214 cancellationToken).ConfigureAwait(false))!;
215 }
216
217 /// <summary>
218 /// Gets the conversations in which the bot has participated.
219 /// </summary>
220 /// <param name="serviceUrl">The service URL for the bot. Cannot be null.</param>
221 /// <param name="continuationToken">Optional continuation token for pagination.</param>
222 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
223 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
224 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
225 /// <returns>A task that represents the asynchronous operation. The task result contains the conversations and an optional continuation token.</returns>
226 /// <exception cref="HttpRequestException">Thrown if the conversations could not be retrieved successfully.</exception>
227 public async Task<GetConversationsResponse> GetConversationsAsync(Uri serviceUrl, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
228 {
229 ArgumentNullException.ThrowIfNull(serviceUrl);
230
231 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations";
232 if (!string.IsNullOrWhiteSpace(continuationToken))
233 {
234 url += $"?continuationToken={Uri.EscapeDataString(continuationToken)}";
235 }
236
237 logger.LogTrace("Getting conversations from {Url}", url);
238
239 return (await _botHttpClient.SendAsync<GetConversationsResponse>(
240 HttpMethod.Get,
241 url,
242 body: null,
243 CreateRequestOptions(agenticIdentity, "getting conversations", customHeaders),
244 cancellationToken).ConfigureAwait(false))!;
245 }
246
247 /// <summary>
248 /// Gets the members of a specific activity.
249 /// </summary>
250 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
251 /// <param name="activityId">The ID of the activity. Cannot be null or whitespace.</param>
252 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
253 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
254 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
255 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
256 /// <returns>A task that represents the asynchronous operation. The task result contains a list of members for the activity.</returns>
257 /// <exception cref="HttpRequestException">Thrown if the activity members could not be retrieved successfully.</exception>
258 public async Task<IList<ConversationAccount>> GetActivityMembersAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
259 {
260 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
261 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
262 ArgumentNullException.ThrowIfNull(serviceUrl);
263
264 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}/members";
265
266 logger.LogTrace("Getting activity members from {Url}", url);
267
268 return (await _botHttpClient.SendAsync<IList<ConversationAccount>>(
269 HttpMethod.Get,
270 url,
271 body: null,
272 CreateRequestOptions(agenticIdentity, "getting activity members", customHeaders),
273 cancellationToken).ConfigureAwait(false))!;
274 }
275
276 /// <summary>
277 /// Creates a new conversation.
278 /// </summary>
279 /// <param name="parameters">The parameters for creating the conversation. Cannot be null.</param>
280 /// <param name="serviceUrl">The service URL for the bot. Cannot be null.</param>
281 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
282 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
283 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
284 /// <returns>A task that represents the asynchronous operation. The task result contains the conversation resource response with the conversation ID.</returns>
285 /// <exception cref="HttpRequestException">Thrown if the conversation could not be created successfully.</exception>
286 public async Task<CreateConversationResponse> CreateConversationAsync(ConversationParameters parameters, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
287 {
288 ArgumentNullException.ThrowIfNull(parameters);
289 ArgumentNullException.ThrowIfNull(serviceUrl);
290
291 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations";
292
293 logger.LogTrace("Creating conversation at {Url} with parameters: {Parameters}", url, JsonSerializer.Serialize(parameters));
294
295 return (await _botHttpClient.SendAsync<CreateConversationResponse>(
296 HttpMethod.Post,
297 url,
298 JsonSerializer.Serialize(parameters),
299 CreateRequestOptions(agenticIdentity, "creating conversation", customHeaders),
300 cancellationToken).ConfigureAwait(false))!;
301 }
302
303 /// <summary>
304 /// Gets the members of a conversation one page at a time.
305 /// </summary>
306 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
307 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
308 /// <param name="pageSize">Optional page size for the number of members to retrieve.</param>
309 /// <param name="continuationToken">Optional continuation token for pagination.</param>
310 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
311 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
312 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
313 /// <returns>A task that represents the asynchronous operation. The task result contains a page of members and an optional continuation token.</returns>
314 /// <exception cref="HttpRequestException">Thrown if the conversation members could not be retrieved successfully.</exception>
315 public async Task<PagedMembersResult> GetConversationPagedMembersAsync(string conversationId, Uri serviceUrl, int? pageSize = null, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
316 {
317 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
318 ArgumentNullException.ThrowIfNull(serviceUrl);
319
320 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/pagedmembers";
321
322 List<string> queryParams = [];
323 if (pageSize.HasValue)
324 {
325 queryParams.Add($"pageSize={pageSize.Value}");
326 }
327 if (!string.IsNullOrWhiteSpace(continuationToken))
328 {
329 queryParams.Add($"continuationToken={Uri.EscapeDataString(continuationToken)}");
330 }
331 if (queryParams.Count > 0)
332 {
333 url += $"?{string.Join("&", queryParams)}";
334 }
335
336 logger.LogTrace("Getting paged conversation members from {Url}", url);
337
338 return (await _botHttpClient.SendAsync<PagedMembersResult>(
339 HttpMethod.Get,
340 url,
341 body: null,
342 CreateRequestOptions(agenticIdentity, "getting paged conversation members", customHeaders),
343 cancellationToken).ConfigureAwait(false))!;
344 }
345
346 /// <summary>
347 /// Deletes a member from a conversation.
348 /// </summary>
349 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
350 /// <param name="memberId">The ID of the member to delete. Cannot be null or whitespace.</param>
351 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
352 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
353 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
354 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
355 /// <returns>A task that represents the asynchronous operation.</returns>
356 /// <exception cref="HttpRequestException">Thrown if the member could not be deleted successfully.</exception>
357 /// <remarks>If the deleted member was the last member of the conversation, the conversation is also deleted.</remarks>
358 public async Task DeleteConversationMemberAsync(string conversationId, string memberId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
359 {
360 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
361 ArgumentException.ThrowIfNullOrWhiteSpace(memberId);
362 ArgumentNullException.ThrowIfNull(serviceUrl);
363
364 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/members/{memberId}";
365
366 logger.LogTrace("Deleting conversation member at {Url}", url);
367
368 await _botHttpClient.SendAsync(
369 HttpMethod.Delete,
370 url,
371 body: null,
372 CreateRequestOptions(agenticIdentity, "deleting conversation member", customHeaders),
373 cancellationToken).ConfigureAwait(false);
374 }
375
376 /// <summary>
377 /// Uploads and sends historic activities to the conversation.
378 /// </summary>
379 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
380 /// <param name="transcript">The transcript containing the historic activities. Cannot be null.</param>
381 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
382 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
383 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
384 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
385 /// <returns>A task that represents the asynchronous operation. The task result contains the response with a resource ID.</returns>
386 /// <exception cref="HttpRequestException">Thrown if the history could not be sent successfully.</exception>
387 /// <remarks>Activities in the transcript must have unique IDs and appropriate timestamps for proper rendering.</remarks>
388 public async Task<SendConversationHistoryResponse> SendConversationHistoryAsync(string conversationId, Transcript transcript, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
389 {
390 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
391 ArgumentNullException.ThrowIfNull(transcript);
392 ArgumentNullException.ThrowIfNull(serviceUrl);
393
394 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/history";
395
396 logger.LogTrace("Sending conversation history to {Url}: {Transcript}", url, JsonSerializer.Serialize(transcript));
397
398 return (await _botHttpClient.SendAsync<SendConversationHistoryResponse>(
399 HttpMethod.Post,
400 url,
401 JsonSerializer.Serialize(transcript),
402 CreateRequestOptions(agenticIdentity, "sending conversation history", customHeaders),
403 cancellationToken).ConfigureAwait(false))!;
404 }
405
406 /// <summary>
407 /// Uploads an attachment to the channel's blob storage.
408 /// </summary>
409 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
410 /// <param name="attachmentData">The attachment data to upload. Cannot be null.</param>
411 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
412 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
413 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
414 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
415 /// <returns>A task that represents the asynchronous operation. The task result contains the response with an attachment ID.</returns>
416 /// <exception cref="HttpRequestException">Thrown if the attachment could not be uploaded successfully.</exception>
417 /// <remarks>This is useful for storing data in a compliant store when dealing with enterprises.</remarks>
418 public async Task<UploadAttachmentResponse> UploadAttachmentAsync(string conversationId, AttachmentData attachmentData, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
419 {
420 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
421 ArgumentNullException.ThrowIfNull(attachmentData);
422 ArgumentNullException.ThrowIfNull(serviceUrl);
423
424 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/attachments";
425
426 logger.LogTrace("Uploading attachment to {Url}: {AttachmentData}", url, JsonSerializer.Serialize(attachmentData));
427
428 return (await _botHttpClient.SendAsync<UploadAttachmentResponse>(
429 HttpMethod.Post,
430 url,
431 JsonSerializer.Serialize(attachmentData),
432 CreateRequestOptions(agenticIdentity, "uploading attachment", customHeaders),
433 cancellationToken).ConfigureAwait(false))!;
434 }
435
436 private BotRequestOptions CreateRequestOptions(AgenticIdentity? agenticIdentity, string operationDescription, CustomHeaders? customHeaders) =>
437 new()
438 {
439 AgenticIdentity = agenticIdentity,
440 OperationDescription = operationDescription,
441 DefaultHeaders = DefaultCustomHeaders,
442 CustomHeaders = customHeaders
443 };
444}
445