microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
next/core

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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