microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
next/core-compat-sso-middleware

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

598lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Text.Json;
5using Microsoft.Extensions.Logging;
6using Microsoft.Teams.Bot.Core.Http;
7using Microsoft.Teams.Bot.Core.Schema;
8
9namespace Microsoft.Teams.Bot.Core;
10
11using CustomHeaders = Dictionary<string, string>;
12
13/// <summary>
14/// Provides methods for sending activities to a conversation endpoint using HTTP requests.
15/// </summary>
16/// <param name="httpClient">The HTTP client instance used to send requests to the conversation service. Must not be null.</param>
17/// <param name="logger">The logger instance used for logging. Optional.</param>
18public class ConversationClient(HttpClient httpClient, ILogger<ConversationClient> logger = default!)
19{
20 private readonly BotHttpClient _botHttpClient = new(httpClient, logger);
21 private readonly JsonSerializerOptions _jsonSerializerOptions = new()
22 {
23 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
24 WriteIndented = false,
25 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
26 };
27
28 internal const string ConversationHttpClientName = "BotConversationClient";
29
30 internal BotHttpClient BotHttpClient => _botHttpClient;
31
32 /// <summary>
33 /// Gets the default custom headers that will be included in all requests.
34 /// </summary>
35 public CustomHeaders DefaultCustomHeaders { get; } = [];
36
37 /// <summary>
38 /// Sends the specified activity to the conversation endpoint asynchronously.
39 /// </summary>
40 /// <param name="activity">The activity to send. Cannot be null. The activity must contain valid conversation and service URL information.</param>
41 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
42 /// <param name="cancellationToken">A cancellation token that can be used to cancel the send operation.</param>
43 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the sent activity.</returns>
44 /// <exception cref="Exception">Thrown if the activity could not be sent successfully. The exception message includes the HTTP status code and
45 /// response content.</exception>
46 public virtual async Task<SendActivityResponse?> SendActivityAsync(CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
47 {
48 ArgumentNullException.ThrowIfNull(activity);
49 ArgumentNullException.ThrowIfNull(activity.Conversation);
50 ArgumentException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id);
51 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
52
53 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(activity.Conversation.Id)}/activities/";
54
55 if (activity.ChannelId == "agents")
56 {
57 logger.LogInformation("Truncating conversation ID for 'agents' channel to comply with length restrictions.");
58 string conversationId = activity.Conversation.Id;
59 string convId = conversationId.Length > 100 ? conversationId[..100] : conversationId;
60 url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(convId)}/activities/";
61 }
62
63 if (!string.IsNullOrEmpty(activity.ReplyToId))
64 {
65 url += activity.ReplyToId;
66 }
67
68 if (activity.Recipient?.IsTargeted == true)
69 {
70 url += url.Contains('?', StringComparison.Ordinal) ? "&isTargetedActivity=true" : "?isTargetedActivity=true";
71 }
72
73 logger.LogInformationGuarded("Sending activity to {Url}", url);
74
75 string body = activity.ToJson();
76
77 return await _botHttpClient.SendAsync<SendActivityResponse>(
78 HttpMethod.Post,
79 url,
80 body,
81 CreateRequestOptions(activity.From?.GetAgenticIdentity(), "sending activity", customHeaders),
82 cancellationToken).ConfigureAwait(false);
83 }
84
85 /// <summary>
86 /// Updates an existing activity in a conversation.
87 /// </summary>
88 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
89 /// <param name="activityId">The ID of the activity to update. Cannot be null or whitespace.</param>
90 /// <param name="activity">The updated activity data. Cannot be null.</param>
91 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
92 /// <param name="cancellationToken">A cancellation token that can be used to cancel the update operation.</param>
93 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity.</returns>
94 /// <exception cref="HttpRequestException">Thrown if the activity could not be updated successfully.</exception>
95 public virtual async Task<UpdateActivityResponse> UpdateActivityAsync(string conversationId, string activityId, CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
96 {
97 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
98 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
99 ArgumentNullException.ThrowIfNull(activity);
100 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
101
102 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}";
103
104 if (activity.Recipient?.IsTargeted == true)
105 {
106 url += "?isTargetedActivity=true";
107 }
108
109 string body = activity.ToJson();
110
111 logger.LogTraceGuarded("Updating activity at {Url}: {Activity}", url, body);
112
113 return (await _botHttpClient.SendAsync<UpdateActivityResponse>(
114 HttpMethod.Put,
115 url,
116 body,
117 CreateRequestOptions(activity.From?.GetAgenticIdentity(), "updating activity", customHeaders),
118 cancellationToken).ConfigureAwait(false))!;
119 }
120
121
122 /// <summary>
123 /// Updates an existing targeted activity in a conversation.
124 /// The activity body is sent with the targeted recipient to avoid "Cannot edit Recipient of Targeted Message" errors.
125 /// </summary>
126 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
127 /// <param name="activityId">The ID of the activity to update. Cannot be null or whitespace.</param>
128 /// <param name="activity">The updated activity data. Cannot be null. Must contain a valid ServiceUrl.</param>
129 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
130 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
131 /// <param name="cancellationToken">A cancellation token that can be used to cancel the update operation.</param>
132 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity.</returns>
133 /// <exception cref="HttpRequestException">Thrown if the activity could not be updated successfully.</exception>
134 public virtual async Task<UpdateActivityResponse> UpdateTargetedActivityAsync(string conversationId, string activityId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
135 {
136 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
137 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
138 ArgumentNullException.ThrowIfNull(activity);
139 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
140
141 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}?isTargetedActivity=true";
142
143 string body = activity.ToJson();
144
145 logger.LogTraceGuarded("Updating targeted activity at {Url}: {Activity}", url, body);
146
147 return (await _botHttpClient.SendAsync<UpdateActivityResponse>(
148 HttpMethod.Put,
149 url,
150 body,
151 CreateRequestOptions(agenticIdentity, "updating targeted activity", customHeaders),
152 cancellationToken).ConfigureAwait(false))!;
153 }
154
155 /// <summary>
156 /// Deletes an existing targeted activity from a conversation.
157 /// </summary>
158 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
159 /// <param name="activityId">The ID of the activity to delete. 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 delete operation.</param>
164 /// <returns>A task that represents the asynchronous operation.</returns>
165 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
166 public virtual Task DeleteTargetedActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
167 => DeleteActivityAsync(conversationId, activityId, serviceUrl, isTargeted: true, agenticIdentity, customHeaders, cancellationToken);
168
169 /// <summary>
170 /// Deletes an existing activity from a conversation.
171 /// </summary>
172 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
173 /// <param name="activityId">The ID of the activity to delete. Cannot be null or whitespace.</param>
174 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
175 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
176 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
177 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
178 /// <returns>A task that represents the asynchronous operation.</returns>
179 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
180 public virtual Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
181 => DeleteActivityAsync(conversationId, activityId, serviceUrl, isTargeted: false, agenticIdentity, customHeaders, cancellationToken);
182
183 /// <summary>
184 /// Deletes an existing activity from a conversation.
185 /// </summary>
186 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
187 /// <param name="activityId">The ID of the activity to delete. Cannot be null or whitespace.</param>
188 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
189 /// <param name="isTargeted">If true, deletes a targeted activity.</param>
190 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
191 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
192 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
193 /// <returns>A task that represents the asynchronous operation.</returns>
194 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
195 public async Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, bool isTargeted, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
196 {
197 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
198 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
199 ArgumentNullException.ThrowIfNull(serviceUrl);
200
201 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}";
202
203 if (isTargeted)
204 {
205 url += "?isTargetedActivity=true";
206 }
207
208 logger.LogTraceGuarded("Deleting activity at {Url}", url);
209
210 await _botHttpClient.SendAsync(
211 HttpMethod.Delete,
212 url,
213 body: null,
214 CreateRequestOptions(agenticIdentity, "deleting activity", customHeaders),
215 cancellationToken).ConfigureAwait(false);
216 }
217
218 /// <summary>
219 /// Deletes an existing activity from a conversation using activity context.
220 /// </summary>
221 /// <param name="activity">The activity to delete. Must contain valid Id, Conversation.Id, and ServiceUrl. Cannot be null.</param>
222 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
223 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
224 /// <returns>A task that represents the asynchronous operation.</returns>
225 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
226 public virtual async Task DeleteActivityAsync(CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
227 {
228 ArgumentNullException.ThrowIfNull(activity);
229 ArgumentException.ThrowIfNullOrWhiteSpace(activity.Id);
230 ArgumentNullException.ThrowIfNull(activity.Conversation);
231 ArgumentException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id);
232 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
233
234 await DeleteActivityAsync(
235 activity.Conversation.Id,
236 activity.Id,
237 activity.ServiceUrl,
238 activity.Recipient?.IsTargeted == true,
239 activity.From?.GetAgenticIdentity(),
240 customHeaders,
241 cancellationToken).ConfigureAwait(false);
242 }
243
244 /// <summary>
245 /// Gets the members of a conversation.
246 /// </summary>
247 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
248 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
249 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
250 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
251 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
252 /// <returns>A task that represents the asynchronous operation. The task result contains a list of conversation members.</returns>
253 /// <exception cref="HttpRequestException">Thrown if the members could not be retrieved successfully.</exception>
254 public virtual async Task<IList<ConversationAccount>> GetConversationMembersAsync(string conversationId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
255 {
256 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
257 ArgumentNullException.ThrowIfNull(serviceUrl);
258
259 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/members";
260
261 logger.LogTraceGuarded("Getting conversation members from {Url}", url);
262
263 return (await _botHttpClient.SendAsync<IList<ConversationAccount>>(
264 HttpMethod.Get,
265 url,
266 body: null,
267 CreateRequestOptions(agenticIdentity, "getting conversation members", customHeaders),
268 cancellationToken).ConfigureAwait(false))!;
269 }
270
271
272 /// <summary>
273 /// Gets a specific member of a conversation with strongly-typed result.
274 /// </summary>
275 /// <typeparam name="T">The type of conversation account to return. Must inherit from <see cref="ConversationAccount"/>.</typeparam>
276 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
277 /// <param name="userId">The ID of the user to retrieve. Cannot be null or whitespace.</param>
278 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
279 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
280 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
281 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
282 /// <returns>
283 /// A task that represents the asynchronous operation. The task result contains the conversation member
284 /// of type T with detailed information about the user.
285 /// </returns>
286 /// <exception cref="HttpRequestException">Thrown if the member could not be retrieved successfully.</exception>
287 public virtual async Task<T> GetConversationMemberAsync<T>(string conversationId, string userId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) where T : ConversationAccount
288 {
289 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
290 ArgumentNullException.ThrowIfNull(serviceUrl);
291 ArgumentException.ThrowIfNullOrWhiteSpace(userId);
292
293 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/members/{Uri.EscapeDataString(userId)}";
294
295 logger.LogTraceGuarded("Getting conversation member from {Url}", url);
296
297 return (await _botHttpClient.SendAsync<T>(
298 HttpMethod.Get,
299 url,
300 body: null,
301 CreateRequestOptions(agenticIdentity, "getting conversation member", customHeaders),
302 cancellationToken).ConfigureAwait(false))!;
303 }
304
305 /// <summary>
306 /// Gets the conversations in which the bot has participated.
307 /// </summary>
308 /// <param name="serviceUrl">The service URL for the bot. Cannot be null.</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 the conversations and an optional continuation token.</returns>
314 /// <exception cref="HttpRequestException">Thrown if the conversations could not be retrieved successfully.</exception>
315 public virtual async Task<GetConversationsResponse> GetConversationsAsync(Uri serviceUrl, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
316 {
317 ArgumentNullException.ThrowIfNull(serviceUrl);
318
319 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations";
320 if (!string.IsNullOrWhiteSpace(continuationToken))
321 {
322 url += $"?continuationToken={Uri.EscapeDataString(continuationToken)}";
323 }
324
325 logger.LogTraceGuarded("Getting conversations from {Url}", url);
326
327 return (await _botHttpClient.SendAsync<GetConversationsResponse>(
328 HttpMethod.Get,
329 url,
330 body: null,
331 CreateRequestOptions(agenticIdentity, "getting conversations", customHeaders),
332 cancellationToken).ConfigureAwait(false))!;
333 }
334
335 /// <summary>
336 /// Gets the members of a specific activity.
337 /// </summary>
338 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
339 /// <param name="activityId">The ID of the activity. Cannot be null or whitespace.</param>
340 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
341 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
342 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
343 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
344 /// <returns>A task that represents the asynchronous operation. The task result contains a list of members for the activity.</returns>
345 /// <exception cref="HttpRequestException">Thrown if the activity members could not be retrieved successfully.</exception>
346 public virtual async Task<IList<ConversationAccount>> GetActivityMembersAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
347 {
348 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
349 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
350 ArgumentNullException.ThrowIfNull(serviceUrl);
351
352 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}/members";
353
354 logger.LogTraceGuarded("Getting activity members from {Url}", url);
355
356 return (await _botHttpClient.SendAsync<IList<ConversationAccount>>(
357 HttpMethod.Get,
358 url,
359 body: null,
360 CreateRequestOptions(agenticIdentity, "getting activity members", customHeaders),
361 cancellationToken).ConfigureAwait(false))!;
362 }
363
364 /// <summary>
365 /// Creates a new conversation.
366 /// </summary>
367 /// <param name="parameters">The parameters for creating the conversation. Cannot be null.</param>
368 /// <param name="serviceUrl">The service URL for the bot. Cannot be null.</param>
369 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
370 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
371 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
372 /// <returns>A task that represents the asynchronous operation. The task result contains the conversation resource response with the conversation ID.</returns>
373 /// <exception cref="HttpRequestException">Thrown if the conversation could not be created successfully.</exception>
374 public virtual async Task<CreateConversationResponse> CreateConversationAsync(ConversationParameters parameters, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
375 {
376 ArgumentNullException.ThrowIfNull(parameters);
377 ArgumentNullException.ThrowIfNull(serviceUrl);
378
379 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations";
380
381 string paramsJson = JsonSerializer.Serialize(parameters, _jsonSerializerOptions);
382
383 logger.LogTraceGuarded("Creating conversation at {Url} with parameters: {Parameters}", url, paramsJson);
384
385 return (await _botHttpClient.SendAsync<CreateConversationResponse>(
386 HttpMethod.Post,
387 url,
388 paramsJson,
389 CreateRequestOptions(agenticIdentity, "creating conversation", customHeaders),
390 cancellationToken).ConfigureAwait(false))!;
391 }
392
393 /// <summary>
394 /// Gets the members of a conversation one page at a time.
395 /// </summary>
396 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
397 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
398 /// <param name="pageSize">Optional page size for the number of members to retrieve.</param>
399 /// <param name="continuationToken">Optional continuation token for pagination.</param>
400 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
401 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
402 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
403 /// <returns>A task that represents the asynchronous operation. The task result contains a page of members and an optional continuation token.</returns>
404 /// <exception cref="HttpRequestException">Thrown if the conversation members could not be retrieved successfully.</exception>
405 public virtual async Task<PagedMembersResult> GetConversationPagedMembersAsync(string conversationId, Uri serviceUrl, int? pageSize = null, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
406 {
407 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
408 ArgumentNullException.ThrowIfNull(serviceUrl);
409
410 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/pagedmembers";
411
412 List<string> queryParams = [];
413 if (pageSize.HasValue)
414 {
415 queryParams.Add($"pageSize={pageSize.Value}");
416 }
417 if (!string.IsNullOrWhiteSpace(continuationToken))
418 {
419 queryParams.Add($"continuationToken={Uri.EscapeDataString(continuationToken)}");
420 }
421 if (queryParams.Count > 0)
422 {
423 url += $"?{string.Join("&", queryParams)}";
424 }
425
426 logger.LogTraceGuarded("Getting paged conversation members from {Url}", url);
427
428 return (await _botHttpClient.SendAsync<PagedMembersResult>(
429 HttpMethod.Get,
430 url,
431 body: null,
432 CreateRequestOptions(agenticIdentity, "getting paged conversation members", customHeaders),
433 cancellationToken).ConfigureAwait(false))!;
434 }
435
436 /// <summary>
437 /// Deletes a member from a conversation.
438 /// </summary>
439 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
440 /// <param name="memberId">The ID of the member to delete. Cannot be null or whitespace.</param>
441 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
442 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
443 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
444 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
445 /// <returns>A task that represents the asynchronous operation.</returns>
446 /// <exception cref="HttpRequestException">Thrown if the member could not be deleted successfully.</exception>
447 /// <remarks>If the deleted member was the last member of the conversation, the conversation is also deleted.</remarks>
448 public virtual async Task DeleteConversationMemberAsync(string conversationId, string memberId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
449 {
450 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
451 ArgumentException.ThrowIfNullOrWhiteSpace(memberId);
452 ArgumentNullException.ThrowIfNull(serviceUrl);
453
454 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/members/{Uri.EscapeDataString(memberId)}";
455
456 logger.LogTraceGuarded("Deleting conversation member at {Url}", url);
457
458 await _botHttpClient.SendAsync(
459 HttpMethod.Delete,
460 url,
461 body: null,
462 CreateRequestOptions(agenticIdentity, "deleting conversation member", customHeaders),
463 cancellationToken).ConfigureAwait(false);
464 }
465
466 /// <summary>
467 /// Uploads and sends historic activities to the conversation.
468 /// </summary>
469 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
470 /// <param name="transcript">The transcript containing the historic activities. Cannot be null.</param>
471 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
472 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
473 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
474 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
475 /// <returns>A task that represents the asynchronous operation. The task result contains the response with a resource ID.</returns>
476 /// <exception cref="HttpRequestException">Thrown if the history could not be sent successfully.</exception>
477 /// <remarks>Activities in the transcript must have unique IDs and appropriate timestamps for proper rendering.</remarks>
478 public virtual async Task<SendConversationHistoryResponse> SendConversationHistoryAsync(string conversationId, Transcript transcript, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
479 {
480 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
481 ArgumentNullException.ThrowIfNull(transcript);
482 ArgumentNullException.ThrowIfNull(serviceUrl);
483
484 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/history";
485
486 string transcriptJson = JsonSerializer.Serialize(transcript, _jsonSerializerOptions);
487 logger.LogTraceGuarded("Sending conversation history to {Url}: {Transcript}", url, transcriptJson);
488
489 return (await _botHttpClient.SendAsync<SendConversationHistoryResponse>(
490 HttpMethod.Post,
491 url,
492 transcriptJson,
493 CreateRequestOptions(agenticIdentity, "sending conversation history", customHeaders),
494 cancellationToken).ConfigureAwait(false))!;
495 }
496
497 /// <summary>
498 /// Uploads an attachment to the channel's blob storage.
499 /// </summary>
500 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
501 /// <param name="attachmentData">The attachment data to upload. Cannot be null.</param>
502 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
503 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
504 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
505 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
506 /// <returns>A task that represents the asynchronous operation. The task result contains the response with an attachment ID.</returns>
507 /// <exception cref="HttpRequestException">Thrown if the attachment could not be uploaded successfully.</exception>
508 /// <remarks>This is useful for storing data in a compliant store when dealing with enterprises.</remarks>
509 public virtual async Task<UploadAttachmentResponse> UploadAttachmentAsync(string conversationId, AttachmentData attachmentData, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
510 {
511 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
512 ArgumentNullException.ThrowIfNull(attachmentData);
513 ArgumentNullException.ThrowIfNull(serviceUrl);
514
515 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/attachments";
516
517 string attachmentDataJson = JsonSerializer.Serialize(attachmentData, _jsonSerializerOptions);
518 logger.LogTraceGuarded("Uploading attachment to {Url}: {AttachmentData}", url, attachmentDataJson);
519
520 return (await _botHttpClient.SendAsync<UploadAttachmentResponse>(
521 HttpMethod.Post,
522 url,
523 attachmentDataJson,
524 CreateRequestOptions(agenticIdentity, "uploading attachment", customHeaders),
525 cancellationToken).ConfigureAwait(false))!;
526 }
527
528 /// <summary>
529 /// Adds a reaction to an activity in a conversation.
530 /// </summary>
531 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
532 /// <param name="activityId">The ID of the activity to react to. Cannot be null or whitespace.</param>
533 /// <param name="reactionType">The type of reaction to add (e.g., "like", "heart", "laugh"). Cannot be null or whitespace.</param>
534 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
535 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
536 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
537 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
538 /// <returns>A task that represents the asynchronous operation.</returns>
539 /// <exception cref="HttpRequestException">Thrown if the reaction could not be added successfully.</exception>
540 public async Task AddReactionAsync(string conversationId, string activityId, string reactionType, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
541 {
542 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
543 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
544 ArgumentException.ThrowIfNullOrWhiteSpace(reactionType);
545 ArgumentNullException.ThrowIfNull(serviceUrl);
546
547 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}/reactions/{Uri.EscapeDataString(reactionType)}";
548
549 logger.LogTraceGuarded("Adding reaction at {Url}", url);
550
551 await _botHttpClient.SendAsync(
552 HttpMethod.Put,
553 url,
554 body: null,
555 CreateRequestOptions(agenticIdentity, "adding reaction", customHeaders),
556 cancellationToken).ConfigureAwait(false);
557 }
558
559 /// <summary>
560 /// Removes a reaction from an activity in a conversation.
561 /// </summary>
562 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
563 /// <param name="activityId">The ID of the activity to remove the reaction from. Cannot be null or whitespace.</param>
564 /// <param name="reactionType">The type of reaction to remove (e.g., "like", "heart", "laugh"). Cannot be null or whitespace.</param>
565 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
566 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
567 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
568 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
569 /// <returns>A task that represents the asynchronous operation.</returns>
570 /// <exception cref="HttpRequestException">Thrown if the reaction could not be removed successfully.</exception>
571 public async Task DeleteReactionAsync(string conversationId, string activityId, string reactionType, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
572 {
573 ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
574 ArgumentException.ThrowIfNullOrWhiteSpace(activityId);
575 ArgumentException.ThrowIfNullOrWhiteSpace(reactionType);
576 ArgumentNullException.ThrowIfNull(serviceUrl);
577
578 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}/reactions/{Uri.EscapeDataString(reactionType)}";
579
580 logger.LogTraceGuarded("Deleting reaction at {Url}", url);
581
582 await _botHttpClient.SendAsync(
583 HttpMethod.Delete,
584 url,
585 body: null,
586 CreateRequestOptions(agenticIdentity, "deleting reaction", customHeaders),
587 cancellationToken).ConfigureAwait(false);
588 }
589
590 private BotRequestOptions CreateRequestOptions(AgenticIdentity? agenticIdentity, string operationDescription, CustomHeaders? customHeaders) =>
591 new()
592 {
593 AgenticIdentity = agenticIdentity,
594 OperationDescription = operationDescription,
595 DefaultHeaders = DefaultCustomHeaders,
596 CustomHeaders = customHeaders
597 };
598}
599