microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
chore/bump-versions-post-release

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Apps/App.cs

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