microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
samples/migration-bot

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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