microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
cg/sovereign-cloud-nextcore

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Apps/App.cs

492lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License.
3
4using Microsoft.Teams.Api;
5using Microsoft.Teams.Api.Activities;
6using Microsoft.Teams.Api.Auth;
7using Microsoft.Teams.Api.Clients;
8using Microsoft.Teams.Apps.Activities.Invokes;
9using Microsoft.Teams.Apps.Events;
10using Microsoft.Teams.Apps.Plugins;
11using Microsoft.Teams.Common.Http;
12using Microsoft.Teams.Common.Logging;
13using Microsoft.Teams.Common.Storage;
14
15namespace Microsoft.Teams.Apps;
16
17public partial class App
18{
19 public static AppBuilder Builder(AppOptions? options = null) => new(options);
20
21 /// <summary>
22 /// the apps id
23 /// </summary>
24 public string? Id => Token?.AppId;
25
26 /// <summary>
27 /// the apps name
28 /// </summary>
29 public string? Name => Token?.AppDisplayName;
30
31 public Status? Status { get; internal set; }
32 public ILogger Logger { get; }
33 public IStorage<string, object> Storage { get; }
34 public ApiClient Api { get; internal set; }
35 public IHttpClient Client { get; }
36 public IHttpCredentials? Credentials { get; }
37 public IToken? Token { get; internal set; }
38 public OAuthSettings OAuth { get; internal set; }
39
40 internal IHttpClient TokenClient { get; set; }
41 internal IServiceProvider? Provider { get; set; }
42 internal IContainer Container { get; set; }
43
44 private readonly IEnumerable<string>? _additionalAllowedDomains;
45 private readonly CloudEnvironment _cloud;
46 internal string UserAgent
47 {
48 get
49 {
50 var version = ThisAssembly.NuGetPackageVersion ?? "0.0.0";
51 return $"teams.net[apps]/{version}";
52 }
53 }
54
55 public App(AppOptions? options = null)
56 {
57 var cloud = options?.Cloud ?? CloudEnvironment.Public;
58 _cloud = cloud;
59
60 Logger = options?.Logger ?? new ConsoleLogger();
61 Storage = options?.Storage ?? new LocalStorage<object>();
62 Credentials = options?.Credentials;
63 Plugins = options?.Plugins ?? [];
64 OAuth = options?.OAuth ?? new OAuthSettings();
65 Provider = options?.Provider;
66 _additionalAllowedDomains = options?.AdditionalAllowedDomains;
67
68 if (_additionalAllowedDomains?.Contains("*") == true)
69 {
70 Logger.Warn("Service URL validation is disabled via wildcard in AdditionalAllowedDomains");
71 }
72
73 TokenClient = new Common.Http.HttpClient();
74 Client = options?.Client ?? options?.ClientFactory?.CreateClient() ?? new Common.Http.HttpClient();
75 Client.Options.AddUserAgent(UserAgent);
76 Client.Options.TokenFactory ??= () =>
77 {
78 if (Credentials is not null)
79 {
80 if (Token is null)
81 {
82 var res = Api!.Bots.Token.GetAsync(Credentials, TokenClient)
83 .ConfigureAwait(false)
84 .GetAwaiter()
85 .GetResult();
86
87 Token = new JsonWebToken(res.AccessToken);
88 }
89
90 if (Token.IsExpired)
91 {
92 var res = Credentials.Resolve(TokenClient, [.. Token.Scopes.DefaultIfEmpty(Api!.Bots.Token.ActiveBotScope)])
93 .ConfigureAwait(false)
94 .GetAwaiter()
95 .GetResult();
96
97 Token = new JsonWebToken(res.AccessToken);
98 }
99 }
100
101 return Token;
102 };
103
104 Api = new ApiClient("https://smba.trafficmanager.net/teams/", Client);
105 Api.Bots.Token.ActiveBotScope = cloud.BotScope;
106 Api.Bots.Token.ActiveGraphScope = cloud.GraphScope;
107 Api.Bots.SignIn.TokenServiceUrl = cloud.TokenServiceUrl;
108 Api.Users.Token.TokenServiceUrl = cloud.TokenServiceUrl;
109 Container = new Container();
110 Container.Register(Logger);
111 Container.Register(Storage);
112 Container.Register(Client);
113 Container.Register(Api);
114 Container.Register<IHttpCredentials>(new FactoryProvider(() => Credentials));
115 Container.Register("AppId", new FactoryProvider(() => Id));
116 Container.Register("AppName", new FactoryProvider(() => Name));
117 Container.Register("Token", new FactoryProvider(() => Token));
118
119 this.OnTokenExchange(OnTokenExchangeActivity);
120 this.OnVerifyState(OnVerifyStateActivity);
121 this.OnSignInFailure(OnSignInFailureActivity);
122 this.OnError(OnErrorEvent);
123 this.OnActivitySent(OnActivitySentEvent);
124 this.OnActivityResponse(OnActivityResponseEvent);
125
126 Events.On(EventType.Activity, (plugin, @event, token) =>
127 {
128 return OnActivityEvent((ISenderPlugin)plugin, (ActivityEvent)@event, token);
129 });
130
131 Status = Apps.Status.Ready;
132 }
133
134 /// <summary>
135 /// start the app
136 /// </summary>
137 public async Task Start(CancellationToken cancellationToken = default)
138 {
139 try
140 {
141 foreach (var plugin in Plugins)
142 {
143 Inject(plugin);
144 }
145
146 if (Credentials is not null)
147 {
148 try
149 {
150 var res = await Api.Bots.Token.GetAsync(Credentials, TokenClient);
151 Token = new JsonWebToken(res.AccessToken);
152 }
153 catch (Exception ex)
154 {
155 Logger.Error("Failed to get bot token on app startup.", ex);
156 }
157 }
158
159 Logger.Debug(Id);
160 Logger.Debug(Name);
161
162 foreach (var plugin in Plugins)
163 {
164 await plugin.OnInit(this, cancellationToken);
165 }
166
167 foreach (var plugin in Plugins)
168 {
169 await plugin.OnStart(this, cancellationToken);
170 }
171
172 Status = Apps.Status.Started;
173 }
174 catch (Exception ex)
175 {
176 Status = Apps.Status.Stopped;
177 await Events.Emit(
178 null!,
179 EventType.Error,
180 new ErrorEvent() { Exception = ex }
181 );
182 }
183 }
184
185 /// <summary>
186 /// send an activity proactively to a conversation.
187 /// Sends to the exact conversation ID provided. For channel threads,
188 /// the conversation ID must include <c>;messageid=</c> -- use
189 /// <see cref="Conversation.ToThreadedConversationId"/> to construct it, or use
190 /// <see cref="Reply{T}(string, string, T, CancellationToken)"/> which handles this automatically.
191 /// </summary>
192 public async Task<T> Send<T>(string conversationId, T activity, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default) where T : IActivity
193 {
194 if (Id is null)
195 {
196 throw new InvalidOperationException("app not started");
197 }
198
199 var reference = new ConversationReference()
200 {
201 ChannelId = ChannelId.MsTeams,
202 ServiceUrl = serviceUrl ?? Api.ServiceUrl,
203 Bot = new()
204 {
205 Id = Id,
206 Name = Name,
207 Role = Role.Bot
208 },
209 Conversation = new()
210 {
211 Id = conversationId,
212 Type = conversationType
213 }
214 };
215
216 var sender = Plugins.Where(plugin => plugin is ISenderPlugin).Select(plugin => plugin as ISenderPlugin).First();
217
218 if (sender is null)
219 {
220 throw new Exception("no plugin that can send activities was found");
221 }
222
223 var res = await sender.Send(activity, reference, cancellationToken);
224
225 await Events.Emit(
226 sender,
227 EventType.ActivitySent,
228 new ActivitySentEvent() { Activity = res },
229 cancellationToken
230 );
231
232 return res;
233 }
234
235 /// <summary>
236 /// send a message activity to the conversation
237 /// </summary>
238 /// <param name="text">the text to send</param>
239 public async Task<MessageActivity> Send(string conversationId, string text, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default)
240 {
241 return await Send(conversationId, new MessageActivity(text), conversationType, serviceUrl, cancellationToken);
242 }
243
244 /// <summary>
245 /// send a message activity with a card attachment
246 /// </summary>
247 /// <param name="card">the card to send as an attachment</param>
248 public async Task<MessageActivity> Send(string conversationId, Cards.AdaptiveCard card, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default)
249 {
250 return await Send(conversationId, new MessageActivity().AddAttachment(card), conversationType, serviceUrl, cancellationToken);
251 }
252
253 /// <summary>
254 /// send an activity proactively to a conversation, optionally as a threaded reply.
255 /// Constructs a threaded conversation ID from the conversation ID
256 /// and message ID via <see cref="Conversation.ToThreadedConversationId"/>,
257 /// then sends to that thread. The service determines whether threading is
258 /// supported for the given conversation type.
259 /// </summary>
260 /// <param name="conversationId">the conversation ID</param>
261 /// <param name="messageId">the thread root message ID</param>
262 /// <param name="activity">the activity to send</param>
263 /// <param name="cancellationToken">optional cancellation token</param>
264 public Task<T> Reply<T>(string conversationId, string messageId, T activity, CancellationToken cancellationToken = default) where T : IActivity
265 {
266 return Send(Conversation.ToThreadedConversationId(conversationId, messageId), activity, cancellationToken: cancellationToken);
267 }
268
269 /// <summary>
270 /// send an activity proactively to a conversation.
271 /// Sends to the exact conversation ID provided - threaded if
272 /// it contains <c>;messageid=</c>, flat otherwise.
273 /// </summary>
274 /// <param name="conversationId">the conversation to send to</param>
275 /// <param name="activity">the activity to send</param>
276 /// <param name="cancellationToken">optional cancellation token</param>
277 public Task<T> Reply<T>(string conversationId, T activity, CancellationToken cancellationToken = default) where T : IActivity
278 {
279 return Send(conversationId, activity, cancellationToken: cancellationToken);
280 }
281
282 /// <summary>
283 /// send a message proactively to a thread
284 /// </summary>
285 public Task<MessageActivity> Reply(string conversationId, string messageId, string text, CancellationToken cancellationToken = default)
286 {
287 return Reply(conversationId, messageId, new MessageActivity(text), cancellationToken);
288 }
289
290 /// <summary>
291 /// send a message proactively to a conversation
292 /// </summary>
293 public Task<MessageActivity> Reply(string conversationId, string text, CancellationToken cancellationToken = default)
294 {
295 return Reply<MessageActivity>(conversationId, new MessageActivity(text), cancellationToken);
296 }
297
298 /// <summary>
299 /// send a card proactively to a thread
300 /// </summary>
301 public Task<MessageActivity> Reply(string conversationId, string messageId, Cards.AdaptiveCard card, CancellationToken cancellationToken = default)
302 {
303 return Reply(conversationId, messageId, new MessageActivity().AddAttachment(card), cancellationToken);
304 }
305
306 /// <summary>
307 /// send a card proactively to a conversation
308 /// </summary>
309 public Task<MessageActivity> Reply(string conversationId, Cards.AdaptiveCard card, CancellationToken cancellationToken = default)
310 {
311 return Reply<MessageActivity>(conversationId, new MessageActivity().AddAttachment(card), cancellationToken);
312 }
313
314 /// <summary>
315 /// process an activity
316 /// </summary>
317 /// <param name="sender">the plugin to use</param>
318 /// <param name="token">the request token</param>
319 /// <param name="activity">the inbound activity</param>
320 /// <param name="cancellationToken">the cancellation token</param>
321 public async Task<Response> Process(ISenderPlugin sender, IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default)
322 {
323 return await Process(sender, new()
324 {
325 Token = token,
326 Activity = activity,
327 Extra = extra
328 }, cancellationToken);
329 }
330
331 /// <summary>
332 /// process an activity
333 /// </summary>
334 /// <param name="sender">the plugin to use</param>
335 /// <param name="token">the request token</param>
336 /// <param name="activity">the inbound activity</param>
337 /// <param name="cancellationToken">the cancellation token</param>
338 /// <exception cref="Exception"></exception>
339 public Task<Response> Process(string sender, IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default)
340 {
341 var plugin = ((ISenderPlugin?)GetPlugin(sender)) ?? throw new Exception($"sender plugin '{sender}' not found");
342 return Process(plugin, token, activity, extra, cancellationToken);
343 }
344
345 /// <summary>
346 /// process an activity
347 /// </summary>
348 /// <param name="token">the request token</param>
349 /// <param name="activity">the inbound activity</param>
350 /// <param name="cancellationToken">the cancellation token</param>
351 /// <exception cref="Exception"></exception>
352 public Task<Response> Process<TPlugin>(IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default) where TPlugin : ISenderPlugin
353 {
354 var plugin = GetPlugin<TPlugin>() ?? throw new Exception($"sender plugin '{typeof(TPlugin).Name}' not found");
355 return Process(plugin, token, activity, extra, cancellationToken);
356 }
357
358 /// <summary>
359 /// process an activity
360 /// </summary>
361 /// <param name="sender">the plugin to use</param>
362 /// <param name="@event">the activity event</param>
363 /// <param name="cancellationToken">the cancellation token</param>
364 private async Task<Response> Process(ISenderPlugin sender, ActivityEvent @event, CancellationToken cancellationToken = default)
365 {
366 var start = DateTime.UtcNow;
367 var routes = Router.Select(@event.Activity);
368 JsonWebToken? userToken = null;
369
370 var api = new ApiClient(Api, cancellationToken);
371
372 try
373 {
374 var tokenResponse = await api.Users.Token.GetAsync(new()
375 {
376 UserId = @event.Activity.From.Id,
377 ChannelId = @event.Activity.ChannelId,
378 ConnectionName = OAuth.DefaultConnectionName
379 });
380
381 userToken = new JsonWebToken(tokenResponse);
382 }
383 catch { }
384
385 var path = @event.Activity.GetPath();
386 Logger.Debug(path);
387
388 var serviceUrl = @event.Activity.ServiceUrl ?? @event.Token.ServiceUrl;
389 if (!ServiceUrlValidator.IsAllowed(serviceUrl, _cloud, _additionalAllowedDomains))
390 {
391 Logger.Warn($"Rejected service URL: {serviceUrl}");
392 throw new InvalidOperationException("Service URL is not from an allowed domain");
393 }
394
395 var reference = new ConversationReference()
396 {
397 ServiceUrl = serviceUrl,
398 ChannelId = @event.Activity.ChannelId,
399 Bot = @event.Activity.Recipient,
400 User = @event.Activity.From,
401 Locale = @event.Activity.Locale,
402 Conversation = @event.Activity.Conversation,
403 };
404
405 object? data = null;
406 var i = -1;
407 async Task<object?> Next(IContext<IActivity> context)
408 {
409 if (i + 1 == routes.Count) return data;
410
411 i++;
412 var res = await routes[i].Invoke(context);
413
414 if (res is not null)
415 data = res;
416
417 return res;
418 }
419
420 var stream = sender.CreateStream(reference, cancellationToken);
421 var context = new Context<IActivity>(sender, stream)
422 {
423 AppId = @event.Token.AppId ?? Id ?? string.Empty,
424 TenantId = @event.Token.TenantId ?? string.Empty,
425 Cloud = _cloud,
426 Log = Logger.Child(path),
427 Storage = Storage,
428 Api = api,
429 Activity = @event.Activity,
430 Ref = reference,
431 IsSignedIn = userToken is not null,
432 OnNext = Next,
433 Extra = @event.Extra ?? new Dictionary<string, object?>(),
434 UserGraphToken = userToken,
435 CancellationToken = cancellationToken,
436 ConnectionName = OAuth.DefaultConnectionName,
437 OnActivitySent = async (activity, context) =>
438 {
439 await Events.Emit(
440 context.Sender,
441 EventType.ActivitySent,
442 new ActivitySentEvent() { Activity = activity },
443 context.CancellationToken
444 );
445 }
446 };
447
448 stream.OnChunk += async activity =>
449 {
450 await Events.Emit(
451 sender,
452 EventType.ActivitySent,
453 new ActivitySentEvent() { Activity = activity },
454 cancellationToken
455 );
456 };
457
458 if (@event.Services is not null)
459 {
460 var accessor = (IContext.Accessor?)@event.Services.GetService(typeof(IContext.Accessor));
461
462 if (accessor is not null)
463 {
464 accessor.Value = context;
465 }
466 }
467
468 foreach (var plugin in Plugins)
469 {
470 await plugin.OnActivity(this, sender, @event, cancellationToken);
471 }
472
473 var res = await Next(context);
474 await stream.Close(cancellationToken);
475
476 var response = res is Response value
477 ? value
478 : new Response(System.Net.HttpStatusCode.OK, res);
479
480 response.Meta.Routes = i + 1;
481 response.Meta.Elapse = (DateTime.UtcNow - start).Milliseconds;
482
483 await Events.Emit(
484 sender,
485 EventType.ActivityResponse,
486 new ActivityResponseEvent() { Response = response },
487 cancellationToken
488 );
489
490 return response;
491 }
492}