microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
samples/migration-bot

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Bot.Compat/CompatConversations.cs

438lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.Bot.Connector;
5using Microsoft.Bot.Schema;
6using Microsoft.Rest;
7using Microsoft.Teams.Bot.Core;
8using Microsoft.Teams.Bot.Core.Schema;
9
10// TODO: Figure out what to do with Agentic Identities. They're all "nulls" here right now.
11// The identity is dependent on the incoming payload or supplied in for proactive scenarios.
12namespace Microsoft.Teams.Bot.Compat
13{
14 /// <summary>
15 /// Provides a compatibility adapter that bridges the Teams Bot Core <see cref="ConversationClient"/> to the
16 /// Bot Framework's <see cref="IConversations"/> interface.
17 /// </summary>
18 /// <remarks>
19 /// This adapter enables legacy Bot Framework bots to use the new Teams Bot Core conversation management
20 /// without code changes. It converts between Bot Framework and Core activity formats, handles HTTP operation
21 /// responses, and manages custom header translations. All operations delegate to the underlying Core ConversationClient.
22 /// </remarks>
23 /// <param name="client">The underlying Teams Bot Core ConversationClient that performs the actual conversation operations.</param>
24 internal sealed class CompatConversations(ConversationClient client) : IConversations
25 {
26 internal readonly ConversationClient _client = client;
27
28 /// <summary>
29 /// Gets or sets the service URL for the bot service endpoint.
30 /// This URL is used for all conversation operations and must be set before making API calls.
31 /// </summary>
32 internal string? ServiceUrl { get; set; }
33
34 /// <summary>
35 /// Creates a new conversation with the specified parameters.
36 /// </summary>
37 /// <param name="parameters">The conversation parameters including members and activity. Cannot be null.</param>
38 /// <param name="customHeaders">Optional custom HTTP headers to include in the request.</param>
39 /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
40 /// <returns>
41 /// A task that represents the asynchronous operation. The task result contains an HTTP operation response with
42 /// a <see cref="ConversationResourceResponse"/> containing the conversation ID, activity ID, and service URL.
43 /// </returns>
44 /// <exception cref="ArgumentException">Thrown when <see cref="ServiceUrl"/> is null or whitespace.</exception>
45 public async Task<HttpOperationResponse<ConversationResourceResponse>> CreateConversationWithHttpMessagesAsync(
46 Microsoft.Bot.Schema.ConversationParameters parameters,
47 Dictionary<string, List<string>>? customHeaders = null,
48 CancellationToken cancellationToken = default)
49 {
50 ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
51
52 Microsoft.Teams.Bot.Core.ConversationParameters convoParams = parameters.FromCompatConversationParameters();
53 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
54
55 CreateConversationResponse res = await _client.CreateConversationAsync(
56 convoParams,
57 new Uri(ServiceUrl),
58 AgenticIdentity.FromProperties(convoParams.Activity?.From?.Properties),
59 convertedHeaders,
60 cancellationToken).ConfigureAwait(false);
61
62 ConversationResourceResponse response = new()
63 {
64 ActivityId = res.ActivityId,
65 Id = res.Id,
66 ServiceUrl = res.ServiceUrl?.ToString(),
67 };
68
69 return new HttpOperationResponse<ConversationResourceResponse>
70 {
71 Body = response,
72 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
73 };
74 }
75
76 /// <summary>
77 /// Deletes an existing activity from a conversation.
78 /// </summary>
79 /// <param name="conversationId">The unique identifier of the conversation. Cannot be null or whitespace.</param>
80 /// <param name="activityId">The unique identifier of the activity to delete. Cannot be null or whitespace.</param>
81 /// <param name="customHeaders">Optional custom HTTP headers to include in the request.</param>
82 /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
83 /// <returns>A task that represents the asynchronous operation. The task result contains an HTTP operation response.</returns>
84 /// <exception cref="ArgumentException">Thrown when <see cref="ServiceUrl"/> is null or whitespace.</exception>
85 public async Task<HttpOperationResponse> DeleteActivityWithHttpMessagesAsync(string conversationId, string activityId, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
86 {
87 ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
88
89 await _client.DeleteActivityAsync(
90 conversationId,
91 activityId,
92 new Uri(ServiceUrl),
93 null!,
94 ConvertHeaders(customHeaders),
95 cancellationToken).ConfigureAwait(false);
96 return new HttpOperationResponse
97 {
98 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
99 };
100 }
101
102 public async Task<HttpOperationResponse> DeleteConversationMemberWithHttpMessagesAsync(string conversationId, string memberId, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
103 {
104 ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
105
106 await _client.DeleteConversationMemberAsync(
107 conversationId,
108 memberId,
109 new Uri(ServiceUrl),
110 null!,
111 ConvertHeaders(customHeaders),
112 cancellationToken).ConfigureAwait(false);
113 return new HttpOperationResponse { Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) };
114 }
115
116 public async Task<HttpOperationResponse<IList<ChannelAccount>>> GetActivityMembersWithHttpMessagesAsync(string conversationId, string activityId, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
117 {
118 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
119
120 IList<Microsoft.Teams.Bot.Core.Schema.ConversationAccount> members = await _client.GetActivityMembersAsync(
121 conversationId,
122 activityId,
123 new Uri(ServiceUrl!),
124 null,
125 convertedHeaders,
126 cancellationToken).ConfigureAwait(false);
127
128 List<ChannelAccount> channelAccounts = [.. members.Select(m => m.ToCompatChannelAccount())];
129
130 return new HttpOperationResponse<IList<ChannelAccount>>
131 {
132 Body = channelAccounts,
133 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
134 };
135 }
136
137 /// <summary>
138 /// Retrieves the list of members participating in a conversation.
139 /// </summary>
140 /// <param name="conversationId">The unique identifier of the conversation. Cannot be null or whitespace.</param>
141 /// <param name="customHeaders">Optional custom HTTP headers to include in the request.</param>
142 /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
143 /// <returns>
144 /// A task that represents the asynchronous operation. The task result contains an HTTP operation response with
145 /// a list of <see cref="ChannelAccount"/> objects representing the conversation members.
146 /// </returns>
147 /// <exception cref="ArgumentException">Thrown when <see cref="ServiceUrl"/> is null or whitespace.</exception>
148 public async Task<HttpOperationResponse<IList<ChannelAccount>>> GetConversationMembersWithHttpMessagesAsync(string conversationId, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
149 {
150 ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
151
152 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
153
154 IList<Microsoft.Teams.Bot.Core.Schema.ConversationAccount> members = await _client.GetConversationMembersAsync(
155 conversationId,
156 new Uri(ServiceUrl),
157 null,
158 convertedHeaders,
159 cancellationToken).ConfigureAwait(false);
160
161 List<ChannelAccount> channelAccounts = [.. members.Select(m => m.ToCompatChannelAccount())];
162
163 return new HttpOperationResponse<IList<ChannelAccount>>
164 {
165 Body = channelAccounts,
166 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
167 };
168 }
169
170 public async Task<HttpOperationResponse<Microsoft.Bot.Schema.PagedMembersResult>> GetConversationPagedMembersWithHttpMessagesAsync(string conversationId, int? pageSize = null, string? continuationToken = null, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
171 {
172 ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
173
174 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
175
176 Microsoft.Teams.Bot.Core.PagedMembersResult pagedMembers = await _client.GetConversationPagedMembersAsync(
177 conversationId,
178 new Uri(ServiceUrl),
179 pageSize,
180 continuationToken,
181 null,
182 convertedHeaders,
183 cancellationToken).ConfigureAwait(false);
184
185 Microsoft.Bot.Schema.PagedMembersResult result = new()
186 {
187 ContinuationToken = pagedMembers.ContinuationToken,
188 Members = pagedMembers.Members?.Select(m => m.ToCompatChannelAccount()).ToList()
189 };
190
191 return new HttpOperationResponse<Microsoft.Bot.Schema.PagedMembersResult>
192 {
193 Body = result,
194 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
195 };
196 }
197
198 public async Task<HttpOperationResponse<ConversationsResult>> GetConversationsWithHttpMessagesAsync(string? continuationToken = null, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
199 {
200 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
201
202 GetConversationsResponse conversations = await _client.GetConversationsAsync(
203 new Uri(ServiceUrl!),
204 continuationToken,
205 null,
206 convertedHeaders,
207 cancellationToken).ConfigureAwait(false);
208
209 ConversationsResult result = new()
210 {
211 ContinuationToken = conversations.ContinuationToken,
212 Conversations = conversations.Conversations?.Select(c => new Microsoft.Bot.Schema.ConversationMembers
213 {
214 Id = c.Id,
215 Members = c.Members?.Select(m => m.ToCompatChannelAccount()).ToList()
216 }).ToList()
217 };
218
219 return new HttpOperationResponse<ConversationsResult>
220 {
221 Body = result,
222 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
223 };
224 }
225
226 public async Task<HttpOperationResponse<ResourceResponse>> ReplyToActivityWithHttpMessagesAsync(string conversationId, string activityId, Activity activity, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
227 {
228 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
229
230 CoreActivity coreActivity = activity.FromCompatActivity();
231
232 // Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for sending activities
233 if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null)
234 {
235 coreActivity.ServiceUrl = new Uri(ServiceUrl);
236 }
237
238 // ReplyToActivity is not available in ConversationClient, use SendActivityAsync with replyToId in Properties
239 coreActivity.Properties["replyToId"] = activityId;
240 if (coreActivity.Conversation == null)
241 {
242 coreActivity.Conversation = new Microsoft.Teams.Bot.Core.Schema.Conversation { Id = conversationId };
243 }
244 else
245 {
246 coreActivity.Conversation.Id = conversationId;
247 }
248
249 SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, convertedHeaders, cancellationToken).ConfigureAwait(false);
250
251 ResourceResponse resourceResponse = new()
252 {
253 Id = response?.Id ?? string.Empty
254 };
255
256 return new HttpOperationResponse<ResourceResponse>
257 {
258 Body = resourceResponse,
259 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
260 };
261 }
262
263 public async Task<HttpOperationResponse<ResourceResponse>> SendConversationHistoryWithHttpMessagesAsync(string conversationId, Microsoft.Bot.Schema.Transcript transcript, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
264 {
265 ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
266
267 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
268
269 Microsoft.Teams.Bot.Core.Transcript coreTranscript = new()
270 {
271 Activities = transcript.Activities?.Select(a => a.FromCompatActivity()).ToList()
272 };
273
274 SendConversationHistoryResponse response = await _client.SendConversationHistoryAsync(
275 conversationId,
276 coreTranscript,
277 new Uri(ServiceUrl),
278 null,
279 convertedHeaders,
280 cancellationToken).ConfigureAwait(false);
281
282 ResourceResponse resourceResponse = new()
283 {
284 Id = response.Id
285 };
286
287 return new HttpOperationResponse<ResourceResponse>
288 {
289 Body = resourceResponse,
290 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
291 };
292 }
293
294 /// <summary>
295 /// Sends an activity to an existing conversation.
296 /// </summary>
297 /// <param name="conversationId">The unique identifier of the conversation. Cannot be null or whitespace.</param>
298 /// <param name="activity">The activity to send. Cannot be null.</param>
299 /// <param name="customHeaders">Optional custom HTTP headers to include in the request.</param>
300 /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
301 /// <returns>
302 /// A task that represents the asynchronous operation. The task result contains an HTTP operation response with
303 /// a <see cref="ResourceResponse"/> containing the ID of the sent activity.
304 /// </returns>
305 public async Task<HttpOperationResponse<ResourceResponse>> SendToConversationWithHttpMessagesAsync(string conversationId, Activity activity, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
306 {
307 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
308
309 CoreActivity coreActivity = activity.FromCompatActivity();
310
311 // Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for sending activities
312 if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null)
313 {
314 coreActivity.ServiceUrl = new Uri(ServiceUrl);
315 }
316
317 // Ensure conversation ID is set
318 coreActivity.Conversation ??= new Microsoft.Teams.Bot.Core.Schema.Conversation { Id = conversationId };
319
320 SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, convertedHeaders, cancellationToken).ConfigureAwait(false);
321
322 ResourceResponse resourceResponse = new()
323 {
324 Id = response?.Id ?? string.Empty
325 };
326
327 return new HttpOperationResponse<ResourceResponse>
328 {
329 Body = resourceResponse,
330 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
331 };
332 }
333
334 /// <summary>
335 /// Updates an existing activity in a conversation.
336 /// </summary>
337 /// <param name="conversationId">The unique identifier of the conversation. Cannot be null or whitespace.</param>
338 /// <param name="activityId">The unique identifier of the activity to update. Cannot be null or whitespace.</param>
339 /// <param name="activity">The updated activity content. Cannot be null.</param>
340 /// <param name="customHeaders">Optional custom HTTP headers to include in the request.</param>
341 /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
342 /// <returns>
343 /// A task that represents the asynchronous operation. The task result contains an HTTP operation response with
344 /// a <see cref="ResourceResponse"/> containing the ID of the updated activity.
345 /// </returns>
346 public async Task<HttpOperationResponse<ResourceResponse>> UpdateActivityWithHttpMessagesAsync(string conversationId, string activityId, Activity activity, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
347 {
348 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
349
350 CoreActivity coreActivity = activity.FromCompatActivity();
351
352 // Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for updating activities
353 if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null)
354 {
355 coreActivity.ServiceUrl = new Uri(ServiceUrl);
356 }
357
358 UpdateActivityResponse response = await _client.UpdateActivityAsync(conversationId, activityId, coreActivity, convertedHeaders, cancellationToken).ConfigureAwait(false);
359
360 ResourceResponse resourceResponse = new()
361 {
362 Id = response.Id
363 };
364
365 return new HttpOperationResponse<ResourceResponse>
366 {
367 Body = resourceResponse,
368 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
369 };
370 }
371
372 public async Task<HttpOperationResponse<ResourceResponse>> UploadAttachmentWithHttpMessagesAsync(string conversationId, Microsoft.Bot.Schema.AttachmentData attachmentUpload, Dictionary<string, List<string>>? customHeaders = null, CancellationToken cancellationToken = default)
373 {
374 ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
375 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
376
377 Microsoft.Teams.Bot.Core.AttachmentData coreAttachmentData = new()
378 {
379 Type = attachmentUpload.Type,
380 Name = attachmentUpload.Name,
381 OriginalBase64 = attachmentUpload.OriginalBase64,
382 ThumbnailBase64 = attachmentUpload.ThumbnailBase64
383 };
384
385 UploadAttachmentResponse response = await _client.UploadAttachmentAsync(
386 conversationId,
387 coreAttachmentData,
388 new Uri(ServiceUrl),
389 null,
390 convertedHeaders,
391 cancellationToken).ConfigureAwait(false);
392
393 ResourceResponse resourceResponse = new()
394 {
395 Id = response.Id
396 };
397
398 return new HttpOperationResponse<ResourceResponse>
399 {
400 Body = resourceResponse,
401 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
402 };
403 }
404
405 private static Dictionary<string, string>? ConvertHeaders(Dictionary<string, List<string>>? customHeaders)
406 {
407 if (customHeaders == null)
408 {
409 return null;
410 }
411
412 Dictionary<string, string> convertedHeaders = [];
413 foreach (KeyValuePair<string, List<string>> kvp in customHeaders)
414 {
415 convertedHeaders[kvp.Key] = string.Join(",", kvp.Value);
416 }
417
418 return convertedHeaders;
419 }
420
421 public async Task<HttpOperationResponse<ChannelAccount>> GetConversationMemberWithHttpMessagesAsync(string userId, string conversationId, Dictionary<string, List<string>> customHeaders = null!, CancellationToken cancellationToken = default)
422 {
423 ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl);
424
425 Dictionary<string, string>? convertedHeaders = ConvertHeaders(customHeaders);
426
427 Microsoft.Teams.Bot.Apps.Schema.TeamsConversationAccount response = await _client.GetConversationMemberAsync<Microsoft.Teams.Bot.Apps.Schema.TeamsConversationAccount>(
428 conversationId, userId, new Uri(ServiceUrl), null!, convertedHeaders, cancellationToken).ConfigureAwait(false);
429
430 return new HttpOperationResponse<ChannelAccount>
431 {
432 Body = response.ToCompatChannelAccount(),
433 Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK)
434 };
435
436 }
437 }
438}
439