microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feature/user-agent-header

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Bot.Apps/TeamsApiClient.cs

447lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Text.Json;
5using Microsoft.Extensions.Logging;
6using AppsAssemblyInfo;
7using Microsoft.Teams.Bot.Core.Http;
8using Microsoft.Teams.Bot.Core.Schema;
9
10namespace Microsoft.Teams.Bot.Apps;
11
12using CustomHeaders = Dictionary<string, string>;
13
14/// <summary>
15/// Provides methods for interacting with Teams-specific APIs.
16/// </summary>
17/// <param name="httpClient">The HTTP client instance used to send requests to the Teams service. Must not be null.</param>
18/// <param name="logger">The logger instance used for logging. Optional.</param>
19[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "<Pending>")]
20public class TeamsApiClient(HttpClient httpClient, ILogger<TeamsApiClient> logger = default!)
21{
22 private readonly BotHttpClient _botHttpClient = new(httpClient, logger);
23 internal const string TeamsHttpClientName = "TeamsAPXClient";
24
25 /// <summary>
26 /// Gets the default custom headers that will be included in all requests.
27 /// </summary>
28 public CustomHeaders DefaultCustomHeaders { get; } = new()
29 {
30 ["User-Agent"] = $"{ThisAssembly.AssemblyName}/{ThisAssembly.AssemblyInformationalVersion}"
31 };
32
33 #region Team Operations
34
35 /// <summary>
36 /// Fetches the list of channels for a given team.
37 /// </summary>
38 /// <param name="teamId">The ID of the team. Cannot be null or whitespace.</param>
39 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
40 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
41 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
42 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
43 /// <returns>A task that represents the asynchronous operation. The task result contains the list of channels.</returns>
44 /// <exception cref="HttpRequestException">Thrown if the channel list could not be retrieved successfully.</exception>
45 public async Task<ChannelList> FetchChannelListAsync(string teamId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
46 {
47 ArgumentException.ThrowIfNullOrWhiteSpace(teamId);
48 ArgumentNullException.ThrowIfNull(serviceUrl);
49
50 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/teams/{Uri.EscapeDataString(teamId)}/conversations";
51
52 logger?.LogTrace("Fetching channel list from {Url}", url);
53
54 return (await _botHttpClient.SendAsync<ChannelList>(
55 HttpMethod.Get,
56 url,
57 body: null,
58 CreateRequestOptions(agenticIdentity, "fetching channel list", customHeaders),
59 cancellationToken).ConfigureAwait(false))!;
60 }
61
62 /// <summary>
63 /// Fetches details related to a team.
64 /// </summary>
65 /// <param name="teamId">The ID of the team. Cannot be null or whitespace.</param>
66 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
67 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
68 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
69 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
70 /// <returns>A task that represents the asynchronous operation. The task result contains the team details.</returns>
71 /// <exception cref="HttpRequestException">Thrown if the team details could not be retrieved successfully.</exception>
72 public async Task<TeamDetails> FetchTeamDetailsAsync(string teamId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
73 {
74 ArgumentException.ThrowIfNullOrWhiteSpace(teamId);
75 ArgumentNullException.ThrowIfNull(serviceUrl);
76
77 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/teams/{Uri.EscapeDataString(teamId)}";
78
79 logger?.LogTrace("Fetching team details from {Url}", url);
80
81 return (await _botHttpClient.SendAsync<TeamDetails>(
82 HttpMethod.Get,
83 url,
84 body: null,
85 CreateRequestOptions(agenticIdentity, "fetching team details", customHeaders),
86 cancellationToken).ConfigureAwait(false))!;
87 }
88
89 #endregion
90
91 #region Meeting Operations
92
93 /// <summary>
94 /// Fetches information about a meeting.
95 /// </summary>
96 /// <param name="meetingId">The ID of the meeting, encoded as a BASE64 string. Cannot be null or whitespace.</param>
97 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
98 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
99 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
100 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
101 /// <returns>A task that represents the asynchronous operation. The task result contains the meeting information.</returns>
102 /// <exception cref="HttpRequestException">Thrown if the meeting info could not be retrieved successfully.</exception>
103 public async Task<MeetingInfo> FetchMeetingInfoAsync(string meetingId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
104 {
105 ArgumentException.ThrowIfNullOrWhiteSpace(meetingId);
106 ArgumentNullException.ThrowIfNull(serviceUrl);
107
108 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v1/meetings/{Uri.EscapeDataString(meetingId)}";
109
110 logger?.LogTrace("Fetching meeting info from {Url}", url);
111
112 return (await _botHttpClient.SendAsync<MeetingInfo>(
113 HttpMethod.Get,
114 url,
115 body: null,
116 CreateRequestOptions(agenticIdentity, "fetching meeting info", customHeaders),
117 cancellationToken).ConfigureAwait(false))!;
118 }
119
120 /// <summary>
121 /// Fetches details for a meeting participant.
122 /// </summary>
123 /// <param name="meetingId">The ID of the meeting. Cannot be null or whitespace.</param>
124 /// <param name="participantId">The ID of the participant. Cannot be null or whitespace.</param>
125 /// <param name="tenantId">The ID of the tenant. Cannot be null or whitespace.</param>
126 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
127 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
128 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
129 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
130 /// <returns>A task that represents the asynchronous operation. The task result contains the participant details.</returns>
131 /// <exception cref="HttpRequestException">Thrown if the participant details could not be retrieved successfully.</exception>
132 public async Task<MeetingParticipant> FetchParticipantAsync(string meetingId, string participantId, string tenantId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
133 {
134 ArgumentException.ThrowIfNullOrWhiteSpace(meetingId);
135 ArgumentException.ThrowIfNullOrWhiteSpace(participantId);
136 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
137 ArgumentNullException.ThrowIfNull(serviceUrl);
138
139 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v1/meetings/{Uri.EscapeDataString(meetingId)}/participants/{Uri.EscapeDataString(participantId)}?tenantId={Uri.EscapeDataString(tenantId)}";
140
141 logger?.LogTrace("Fetching meeting participant from {Url}", url);
142
143 return (await _botHttpClient.SendAsync<MeetingParticipant>(
144 HttpMethod.Get,
145 url,
146 body: null,
147 CreateRequestOptions(agenticIdentity, "fetching meeting participant", customHeaders),
148 cancellationToken).ConfigureAwait(false))!;
149 }
150
151 /// <summary>
152 /// Sends a notification to meeting participants.
153 /// </summary>
154 /// <param name="meetingId">The ID of the meeting. Cannot be null or whitespace.</param>
155 /// <param name="notification">The notification to send. Cannot be null.</param>
156 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
157 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
158 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
159 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
160 /// <returns>A task that represents the asynchronous operation. The task result contains information about failed recipients.</returns>
161 /// <exception cref="HttpRequestException">Thrown if the notification could not be sent successfully.</exception>
162 public async Task<MeetingNotificationResponse> SendMeetingNotificationAsync(string meetingId, TargetedMeetingNotification notification, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
163 {
164 ArgumentException.ThrowIfNullOrWhiteSpace(meetingId);
165 ArgumentNullException.ThrowIfNull(notification);
166 ArgumentNullException.ThrowIfNull(serviceUrl);
167
168 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v1/meetings/{Uri.EscapeDataString(meetingId)}/notification";
169 string body = JsonSerializer.Serialize(notification);
170
171 logger?.LogTrace("Sending meeting notification to {Url}: {Notification}", url, body);
172
173 return (await _botHttpClient.SendAsync<MeetingNotificationResponse>(
174 HttpMethod.Post,
175 url,
176 body,
177 CreateRequestOptions(agenticIdentity, "sending meeting notification", customHeaders),
178 cancellationToken).ConfigureAwait(false))!;
179 }
180
181 #endregion
182
183 #region Batch Message Operations
184
185 /// <summary>
186 /// Sends a message to a list of Teams users.
187 /// </summary>
188 /// <param name="activity">The activity to send. Cannot be null.</param>
189 /// <param name="teamsMembers">The list of team members to send the message to. Cannot be null or empty.</param>
190 /// <param name="tenantId">The ID of the tenant. Cannot be null or whitespace.</param>
191 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
192 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
193 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
194 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
195 /// <returns>A task that represents the asynchronous operation. The task result contains the operation ID.</returns>
196 /// <exception cref="HttpRequestException">Thrown if the message could not be sent successfully.</exception>
197 public async Task<string> SendMessageToListOfUsersAsync(CoreActivity activity, IList<TeamMember> teamsMembers, string tenantId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
198 {
199 ArgumentNullException.ThrowIfNull(activity);
200 ArgumentNullException.ThrowIfNull(teamsMembers);
201 if (teamsMembers.Count == 0)
202 {
203 throw new ArgumentException("teamsMembers cannot be empty", nameof(teamsMembers));
204 }
205 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
206 ArgumentNullException.ThrowIfNull(serviceUrl);
207
208 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/users/";
209 SendMessageToUsersRequest request = new()
210 {
211 Members = teamsMembers,
212 Activity = activity,
213 TenantId = tenantId
214 };
215 string body = JsonSerializer.Serialize(request);
216
217 logger?.LogTrace("Sending message to list of users at {Url}: {Request}", url, body);
218
219 return (await _botHttpClient.SendAsync<string>(
220 HttpMethod.Post,
221 url,
222 body,
223 CreateRequestOptions(agenticIdentity, "sending message to list of users", customHeaders),
224 cancellationToken).ConfigureAwait(false))!;
225 }
226
227 /// <summary>
228 /// Sends a message to all users in a tenant.
229 /// </summary>
230 /// <param name="activity">The activity to send. Cannot be null.</param>
231 /// <param name="tenantId">The ID of the tenant. Cannot be null or whitespace.</param>
232 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
233 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
234 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
235 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
236 /// <returns>A task that represents the asynchronous operation. The task result contains the operation ID.</returns>
237 /// <exception cref="HttpRequestException">Thrown if the message could not be sent successfully.</exception>
238 public async Task<string> SendMessageToAllUsersInTenantAsync(CoreActivity activity, string tenantId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
239 {
240 ArgumentNullException.ThrowIfNull(activity);
241 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
242 ArgumentNullException.ThrowIfNull(serviceUrl);
243
244 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/tenant/";
245 SendMessageToTenantRequest request = new()
246 {
247 Activity = activity,
248 TenantId = tenantId
249 };
250 string body = JsonSerializer.Serialize(request);
251
252 logger?.LogTrace("Sending message to all users in tenant at {Url}: {Request}", url, body);
253
254 return (await _botHttpClient.SendAsync<string>(
255 HttpMethod.Post,
256 url,
257 body,
258 CreateRequestOptions(agenticIdentity, "sending message to all users in tenant", customHeaders),
259 cancellationToken).ConfigureAwait(false))!;
260 }
261
262 /// <summary>
263 /// Sends a message to all users in a team.
264 /// </summary>
265 /// <param name="activity">The activity to send. Cannot be null.</param>
266 /// <param name="teamId">The ID of the team. Cannot be null or whitespace.</param>
267 /// <param name="tenantId">The ID of the tenant. Cannot be null or whitespace.</param>
268 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
269 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
270 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
271 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
272 /// <returns>A task that represents the asynchronous operation. The task result contains the operation ID.</returns>
273 /// <exception cref="HttpRequestException">Thrown if the message could not be sent successfully.</exception>
274 public async Task<string> SendMessageToAllUsersInTeamAsync(CoreActivity activity, string teamId, string tenantId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
275 {
276 ArgumentNullException.ThrowIfNull(activity);
277 ArgumentException.ThrowIfNullOrWhiteSpace(teamId);
278 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
279 ArgumentNullException.ThrowIfNull(serviceUrl);
280
281 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/team/";
282 SendMessageToTeamRequest request = new()
283 {
284 Activity = activity,
285 TeamId = teamId,
286 TenantId = tenantId
287 };
288 string body = JsonSerializer.Serialize(request);
289
290 logger?.LogTrace("Sending message to all users in team at {Url}: {Request}", url, body);
291
292 return (await _botHttpClient.SendAsync<string>(
293 HttpMethod.Post,
294 url,
295 body,
296 CreateRequestOptions(agenticIdentity, "sending message to all users in team", customHeaders),
297 cancellationToken).ConfigureAwait(false))!;
298 }
299
300 /// <summary>
301 /// Sends a message to a list of Teams channels.
302 /// </summary>
303 /// <param name="activity">The activity to send. Cannot be null.</param>
304 /// <param name="channelMembers">The list of channels to send the message to. Cannot be null or empty.</param>
305 /// <param name="tenantId">The ID of the tenant. Cannot be null or whitespace.</param>
306 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
307 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
308 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
309 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
310 /// <returns>A task that represents the asynchronous operation. The task result contains the operation ID.</returns>
311 /// <exception cref="HttpRequestException">Thrown if the message could not be sent successfully.</exception>
312 public async Task<string> SendMessageToListOfChannelsAsync(CoreActivity activity, IList<TeamMember> channelMembers, string tenantId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
313 {
314 ArgumentNullException.ThrowIfNull(activity);
315 ArgumentNullException.ThrowIfNull(channelMembers);
316 if (channelMembers.Count == 0)
317 {
318 throw new ArgumentException("channelMembers cannot be empty", nameof(channelMembers));
319 }
320 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
321 ArgumentNullException.ThrowIfNull(serviceUrl);
322
323 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/channels/";
324 SendMessageToUsersRequest request = new()
325 {
326 Members = channelMembers,
327 Activity = activity,
328 TenantId = tenantId
329 };
330 string body = JsonSerializer.Serialize(request);
331
332 logger?.LogTrace("Sending message to list of channels at {Url}: {Request}", url, body);
333
334 return (await _botHttpClient.SendAsync<string>(
335 HttpMethod.Post,
336 url,
337 body,
338 CreateRequestOptions(agenticIdentity, "sending message to list of channels", customHeaders),
339 cancellationToken).ConfigureAwait(false))!;
340 }
341
342 #endregion
343
344 #region Batch Operation Management
345
346 /// <summary>
347 /// Gets the state of a batch operation.
348 /// </summary>
349 /// <param name="operationId">The ID of the operation. Cannot be null or whitespace.</param>
350 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
351 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
352 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
353 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
354 /// <returns>A task that represents the asynchronous operation. The task result contains the operation state.</returns>
355 /// <exception cref="HttpRequestException">Thrown if the operation state could not be retrieved successfully.</exception>
356 public async Task<BatchOperationState> GetOperationStateAsync(string operationId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
357 {
358 ArgumentException.ThrowIfNullOrWhiteSpace(operationId);
359 ArgumentNullException.ThrowIfNull(serviceUrl);
360
361 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/{Uri.EscapeDataString(operationId)}";
362
363 logger?.LogTrace("Getting operation state from {Url}", url);
364
365 return (await _botHttpClient.SendAsync<BatchOperationState>(
366 HttpMethod.Get,
367 url,
368 body: null,
369 CreateRequestOptions(agenticIdentity, "getting operation state", customHeaders),
370 cancellationToken).ConfigureAwait(false))!;
371 }
372
373 /// <summary>
374 /// Gets the failed entries of a batch operation with error code and message.
375 /// </summary>
376 /// <param name="operationId">The ID of the operation. Cannot be null or whitespace.</param>
377 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
378 /// <param name="continuationToken">Optional continuation token for pagination.</param>
379 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
380 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
381 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
382 /// <returns>A task that represents the asynchronous operation. The task result contains the failed entries.</returns>
383 /// <exception cref="HttpRequestException">Thrown if the failed entries could not be retrieved successfully.</exception>
384 public async Task<BatchFailedEntriesResponse> GetPagedFailedEntriesAsync(string operationId, Uri serviceUrl, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
385 {
386 ArgumentException.ThrowIfNullOrWhiteSpace(operationId);
387 ArgumentNullException.ThrowIfNull(serviceUrl);
388
389 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/failedentries/{Uri.EscapeDataString(operationId)}";
390
391 if (!string.IsNullOrWhiteSpace(continuationToken))
392 {
393 url += $"?continuationToken={Uri.EscapeDataString(continuationToken)}";
394 }
395
396 logger?.LogTrace("Getting paged failed entries from {Url}", url);
397
398 return (await _botHttpClient.SendAsync<BatchFailedEntriesResponse>(
399 HttpMethod.Get,
400 url,
401 body: null,
402 CreateRequestOptions(agenticIdentity, "getting paged failed entries", customHeaders),
403 cancellationToken).ConfigureAwait(false))!;
404 }
405
406 /// <summary>
407 /// Cancels a batch operation by its ID.
408 /// </summary>
409 /// <param name="operationId">The ID of the operation to cancel. Cannot be null or whitespace.</param>
410 /// <param name="serviceUrl">The service URL for the Teams service. Cannot be null.</param>
411 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
412 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
413 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
414 /// <returns>A task that represents the asynchronous operation.</returns>
415 /// <exception cref="HttpRequestException">Thrown if the operation could not be cancelled successfully.</exception>
416 public async Task CancelOperationAsync(string operationId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
417 {
418 ArgumentException.ThrowIfNullOrWhiteSpace(operationId);
419 ArgumentNullException.ThrowIfNull(serviceUrl);
420
421 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/batch/conversation/{Uri.EscapeDataString(operationId)}";
422
423 logger?.LogTrace("Cancelling operation at {Url}", url);
424
425 await _botHttpClient.SendAsync(
426 HttpMethod.Delete,
427 url,
428 body: null,
429 CreateRequestOptions(agenticIdentity, "cancelling operation", customHeaders),
430 cancellationToken).ConfigureAwait(false);
431 }
432
433 #endregion
434
435 #region Private Methods
436
437 private BotRequestOptions CreateRequestOptions(AgenticIdentity? agenticIdentity, string operationDescription, CustomHeaders? customHeaders) =>
438 new()
439 {
440 AgenticIdentity = agenticIdentity,
441 OperationDescription = operationDescription,
442 DefaultHeaders = DefaultCustomHeaders,
443 CustomHeaders = customHeaders
444 };
445
446 #endregion
447}