microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/update-release-process

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

665lines · modecode

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