microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5514d9a4ac359b0ae5c20d4c49bb27c19a568c30

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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