microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
kavin/agents-sdk-interop

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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