microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
9dfef728feacaff7db1ea1762d5ccde4d6a434ff

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

435lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Net.Http.Json;
5using System.Net.Mime;
6using System.Text;
7using System.Text.Json;
8using Microsoft.Bot.Core.ClientSchema;
9using Microsoft.Bot.Core.Hosting;
10using Microsoft.Bot.Core.Schema;
11
12namespace Microsoft.Bot.Core;
13
14/// <summary>
15/// Provides methods for sending activities to a conversation endpoint using HTTP requests.
16/// </summary>
17/// <param name="httpClient">The HTTP client instance used to send requests to the conversation service. Must not be null.</param>
18public class ConversationClient(HttpClient httpClient)
19{
20 internal const string ConversationHttpClientName = "BotConversationClient";
21
22 /// <summary>
23 /// Gets the default custom headers that will be included in all requests.
24 /// </summary>
25 public Dictionary<string, string> DefaultCustomHeaders { get; } = new();
26
27 /// <summary>
28 /// Sends the specified activity to the conversation endpoint asynchronously.
29 /// </summary>
30 /// <param name="activity">The activity to send. Cannot be null. The activity must contain valid conversation and service URL information.</param>
31 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
32 /// <param name="cancellationToken">A cancellation token that can be used to cancel the send operation.</param>
33 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the sent activity.</returns>
34 /// <exception cref="Exception">Thrown if the activity could not be sent successfully. The exception message includes the HTTP status code and
35 /// response content.</exception>
36 public async Task<SendActivityResponse> SendActivityAsync(CoreActivity activity, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
37 {
38 ArgumentNullException.ThrowIfNull(activity);
39 ArgumentNullException.ThrowIfNull(activity.Conversation);
40 ArgumentNullException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id);
41 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
42
43 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{activity.Conversation.Id}/activities/";
44
45 return await SendHttpRequestAsync<SendActivityResponse>(
46 HttpMethod.Post,
47 url,
48 activity.ToJson(),
49 activity.From.GetAgenticIdentity(),
50 "sending activity",
51 customHeaders,
52 cancellationToken).ConfigureAwait(false);
53 }
54
55 /// <summary>
56 /// Updates an existing activity in a conversation.
57 /// </summary>
58 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
59 /// <param name="activityId">The ID of the activity to update. Cannot be null or whitespace.</param>
60 /// <param name="activity">The updated activity data. Cannot be null.</param>
61 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
62 /// <param name="cancellationToken">A cancellation token that can be used to cancel the update operation.</param>
63 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity.</returns>
64 /// <exception cref="HttpRequestException">Thrown if the activity could not be updated successfully.</exception>
65 public async Task<UpdateActivityResponse> UpdateActivityAsync(string conversationId, string activityId, CoreActivity activity, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
66 {
67 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
68 ArgumentNullException.ThrowIfNullOrWhiteSpace(activityId);
69 ArgumentNullException.ThrowIfNull(activity);
70 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
71
72 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}";
73
74 return await SendHttpRequestAsync<UpdateActivityResponse>(
75 HttpMethod.Put,
76 url,
77 activity.ToJson(),
78 activity.From.GetAgenticIdentity(),
79 "updating activity",
80 customHeaders,
81 cancellationToken).ConfigureAwait(false);
82 }
83
84
85 /// <summary>
86 /// Deletes an existing activity from a conversation.
87 /// </summary>
88 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
89 /// <param name="activityId">The ID of the activity to delete. Cannot be null or whitespace.</param>
90 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
91 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
92 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
93 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
94 /// <returns>A task that represents the asynchronous operation.</returns>
95 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
96 public async Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
97 {
98 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
99 ArgumentNullException.ThrowIfNullOrWhiteSpace(activityId);
100 ArgumentNullException.ThrowIfNull(serviceUrl);
101
102 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}";
103
104 await SendHttpRequestAsync<DeleteActivityResponse>(
105 HttpMethod.Delete,
106 url,
107 body: null,
108 agenticIdentity: agenticIdentity,
109 "deleting activity",
110 customHeaders,
111 cancellationToken).ConfigureAwait(false);
112 }
113
114 /// <summary>
115 /// Deletes an existing activity from a conversation using activity context.
116 /// </summary>
117 /// <param name="activity">The activity to delete. Must contain valid Id, Conversation.Id, and ServiceUrl. Cannot be null.</param>
118 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
119 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
120 /// <returns>A task that represents the asynchronous operation.</returns>
121 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
122 public async Task DeleteActivityAsync(CoreActivity activity, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
123 {
124 ArgumentNullException.ThrowIfNull(activity);
125 ArgumentNullException.ThrowIfNullOrWhiteSpace(activity.Id);
126 ArgumentNullException.ThrowIfNull(activity.Conversation);
127 ArgumentNullException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id);
128 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
129
130 await DeleteActivityAsync(
131 activity.Conversation.Id,
132 activity.Id,
133 activity.ServiceUrl,
134 activity.From.GetAgenticIdentity(),
135 customHeaders,
136 cancellationToken).ConfigureAwait(false);
137 }
138
139 /// <summary>
140 /// Gets the members of a conversation.
141 /// </summary>
142 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
143 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
144 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
145 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
146 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
147 /// <returns>A task that represents the asynchronous operation. The task result contains a list of conversation members.</returns>
148 /// <exception cref="HttpRequestException">Thrown if the members could not be retrieved successfully.</exception>
149 public async Task<IList<ConversationAccount>> GetConversationMembersAsync(string conversationId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
150 {
151 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
152 ArgumentNullException.ThrowIfNull(serviceUrl);
153
154 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/members";
155
156 return await SendHttpRequestAsync<IList<ConversationAccount>>(
157 HttpMethod.Get,
158 url,
159 body: null,
160 agenticIdentity,
161 "getting conversation members",
162 customHeaders,
163 cancellationToken).ConfigureAwait(false);
164 }
165
166 /// <summary>
167 /// Gets the conversations in which the bot has participated.
168 /// </summary>
169 /// <param name="serviceUrl">The service URL for the bot. Cannot be null.</param>
170 /// <param name="continuationToken">Optional continuation token for pagination.</param>
171 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
172 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
173 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
174 /// <returns>A task that represents the asynchronous operation. The task result contains the conversations and an optional continuation token.</returns>
175 /// <exception cref="HttpRequestException">Thrown if the conversations could not be retrieved successfully.</exception>
176 public async Task<GetConversationsResponse> GetConversationsAsync(Uri serviceUrl, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
177 {
178 ArgumentNullException.ThrowIfNull(serviceUrl);
179
180 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations";
181 if (!string.IsNullOrWhiteSpace(continuationToken))
182 {
183 url += $"?continuationToken={Uri.EscapeDataString(continuationToken)}";
184 }
185
186 return await SendHttpRequestAsync<GetConversationsResponse>(
187 HttpMethod.Get,
188 url,
189 body: null,
190 agenticIdentity,
191 "getting conversations",
192 customHeaders,
193 cancellationToken).ConfigureAwait(false);
194 }
195
196 /// <summary>
197 /// Gets the members of a specific activity.
198 /// </summary>
199 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
200 /// <param name="activityId">The ID of the activity. Cannot be null or whitespace.</param>
201 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
202 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
203 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
204 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
205 /// <returns>A task that represents the asynchronous operation. The task result contains a list of members for the activity.</returns>
206 /// <exception cref="HttpRequestException">Thrown if the activity members could not be retrieved successfully.</exception>
207 public async Task<IList<ConversationAccount>> GetActivityMembersAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
208 {
209 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
210 ArgumentNullException.ThrowIfNullOrWhiteSpace(activityId);
211 ArgumentNullException.ThrowIfNull(serviceUrl);
212
213 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}/members";
214
215 return await SendHttpRequestAsync<IList<ConversationAccount>>(
216 HttpMethod.Get,
217 url,
218 body: null,
219 agenticIdentity,
220 "getting activity members",
221 customHeaders,
222 cancellationToken).ConfigureAwait(false);
223 }
224
225 /// <summary>
226 /// Creates a new conversation.
227 /// </summary>
228 /// <param name="parameters">The parameters for creating the conversation. Cannot be null.</param>
229 /// <param name="serviceUrl">The service URL for the bot. Cannot be null.</param>
230 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
231 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
232 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
233 /// <returns>A task that represents the asynchronous operation. The task result contains the conversation resource response with the conversation ID.</returns>
234 /// <exception cref="HttpRequestException">Thrown if the conversation could not be created successfully.</exception>
235 public async Task<CreateConversationResponse> CreateConversationAsync(ConversationParameters parameters, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
236 {
237 ArgumentNullException.ThrowIfNull(parameters);
238 ArgumentNullException.ThrowIfNull(serviceUrl);
239
240 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations";
241
242 return await SendHttpRequestAsync<CreateConversationResponse>(
243 HttpMethod.Post,
244 url,
245 JsonSerializer.Serialize(parameters),
246 agenticIdentity,
247 "creating conversation",
248 customHeaders,
249 cancellationToken).ConfigureAwait(false);
250 }
251
252 /// <summary>
253 /// Gets the members of a conversation one page at a time.
254 /// </summary>
255 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
256 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
257 /// <param name="pageSize">Optional page size for the number of members to retrieve.</param>
258 /// <param name="continuationToken">Optional continuation token for pagination.</param>
259 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
260 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
261 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
262 /// <returns>A task that represents the asynchronous operation. The task result contains a page of members and an optional continuation token.</returns>
263 /// <exception cref="HttpRequestException">Thrown if the conversation members could not be retrieved successfully.</exception>
264 public async Task<PagedMembersResult> GetConversationPagedMembersAsync(string conversationId, Uri serviceUrl, int? pageSize = null, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
265 {
266 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
267 ArgumentNullException.ThrowIfNull(serviceUrl);
268
269 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/pagedmembers";
270
271 List<string> queryParams = new();
272 if (pageSize.HasValue)
273 {
274 queryParams.Add($"pageSize={pageSize.Value}");
275 }
276 if (!string.IsNullOrWhiteSpace(continuationToken))
277 {
278 queryParams.Add($"continuationToken={Uri.EscapeDataString(continuationToken)}");
279 }
280 if (queryParams.Count > 0)
281 {
282 url += $"?{string.Join("&", queryParams)}";
283 }
284
285 return await SendHttpRequestAsync<PagedMembersResult>(
286 HttpMethod.Get,
287 url,
288 body: null,
289 agenticIdentity,
290 "getting paged conversation members",
291 customHeaders,
292 cancellationToken).ConfigureAwait(false);
293 }
294
295 /// <summary>
296 /// Deletes a member from a conversation.
297 /// </summary>
298 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
299 /// <param name="memberId">The ID of the member to delete. Cannot be null or whitespace.</param>
300 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
301 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
302 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
303 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
304 /// <returns>A task that represents the asynchronous operation.</returns>
305 /// <exception cref="HttpRequestException">Thrown if the member could not be deleted successfully.</exception>
306 /// <remarks>If the deleted member was the last member of the conversation, the conversation is also deleted.</remarks>
307 public async Task DeleteConversationMemberAsync(string conversationId, string memberId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
308 {
309 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
310 ArgumentNullException.ThrowIfNullOrWhiteSpace(memberId);
311 ArgumentNullException.ThrowIfNull(serviceUrl);
312
313 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/members/{memberId}";
314
315 await SendHttpRequestAsync<object>(
316 HttpMethod.Delete,
317 url,
318 body: null,
319 agenticIdentity,
320 "deleting conversation member",
321 customHeaders,
322 cancellationToken).ConfigureAwait(false);
323 }
324
325 /// <summary>
326 /// Uploads and sends historic activities to the conversation.
327 /// </summary>
328 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
329 /// <param name="transcript">The transcript containing the historic activities. Cannot be null.</param>
330 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
331 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
332 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
333 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
334 /// <returns>A task that represents the asynchronous operation. The task result contains the response with a resource ID.</returns>
335 /// <exception cref="HttpRequestException">Thrown if the history could not be sent successfully.</exception>
336 /// <remarks>Activities in the transcript must have unique IDs and appropriate timestamps for proper rendering.</remarks>
337 public async Task<SendConversationHistoryResponse> SendConversationHistoryAsync(string conversationId, Transcript transcript, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
338 {
339 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
340 ArgumentNullException.ThrowIfNull(transcript);
341 ArgumentNullException.ThrowIfNull(serviceUrl);
342
343 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/history";
344
345 return await SendHttpRequestAsync<SendConversationHistoryResponse>(
346 HttpMethod.Post,
347 url,
348 JsonSerializer.Serialize(transcript),
349 agenticIdentity,
350 "sending conversation history",
351 customHeaders,
352 cancellationToken).ConfigureAwait(false);
353 }
354
355 /// <summary>
356 /// Uploads an attachment to the channel's blob storage.
357 /// </summary>
358 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
359 /// <param name="attachmentData">The attachment data to upload. Cannot be null.</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>A task that represents the asynchronous operation. The task result contains the response with an attachment ID.</returns>
365 /// <exception cref="HttpRequestException">Thrown if the attachment could not be uploaded successfully.</exception>
366 /// <remarks>This is useful for storing data in a compliant store when dealing with enterprises.</remarks>
367 public async Task<UploadAttachmentResponse> UploadAttachmentAsync(string conversationId, AttachmentData attachmentData, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? customHeaders = null, CancellationToken cancellationToken = default)
368 {
369 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
370 ArgumentNullException.ThrowIfNull(attachmentData);
371 ArgumentNullException.ThrowIfNull(serviceUrl);
372
373 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/attachments";
374
375 return await SendHttpRequestAsync<UploadAttachmentResponse>(
376 HttpMethod.Post,
377 url,
378 JsonSerializer.Serialize(attachmentData),
379 agenticIdentity,
380 "uploading attachment",
381 customHeaders,
382 cancellationToken).ConfigureAwait(false);
383 }
384
385 private async Task<T> SendHttpRequestAsync<T>(HttpMethod method, string url, string? body, AgenticIdentity? agenticIdentity, string operationDescription, Dictionary<string, string>? customHeaders, CancellationToken cancellationToken)
386 {
387 using HttpRequestMessage request = new(method, url);
388
389 if (body is not null)
390 {
391 request.Content = new StringContent(body, Encoding.UTF8, MediaTypeNames.Application.Json);
392 }
393
394 if (agenticIdentity is not null)
395 {
396 request.Options.Set(BotAuthenticationHandler.AgenticIdentityKey, agenticIdentity);
397 }
398
399 // Apply default custom headers
400 foreach (var header in DefaultCustomHeaders)
401 {
402 request.Headers.TryAddWithoutValidation(header.Key, header.Value);
403 }
404
405 // Apply method-level custom headers (these override default headers if same key)
406 if (customHeaders is not null)
407 {
408 foreach (var header in customHeaders)
409 {
410 request.Headers.Remove(header.Key);
411 request.Headers.TryAddWithoutValidation(header.Key, header.Value);
412 }
413 }
414
415 using HttpResponseMessage resp = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
416
417 if (resp.IsSuccessStatusCode)
418 {
419 string responseString = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
420 if (responseString.Length > 2) // to handle empty response
421 {
422 T? result = JsonSerializer.Deserialize<T>(responseString);
423 return result ?? throw new InvalidOperationException($"Failed to deserialize response for {operationDescription}");
424 }
425 // Empty response - return default value (e.g., for DELETE operations)
426 return default!;
427 }
428 else
429 {
430 string errResponseString = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
431 throw new HttpRequestException($"Error {operationDescription} {resp.StatusCode}. {errResponseString}");
432 }
433 }
434
435}
436