microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/msal

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Apps/App.cs

465lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License.
3
4using System.Reflection;
5
6using Microsoft.Teams.Api;
7using Microsoft.Teams.Api.Activities;
8using Microsoft.Teams.Api.Auth;
9using Microsoft.Teams.Api.Clients;
10using Microsoft.Teams.Apps.Activities.Invokes;
11using Microsoft.Teams.Apps.Events;
12using Microsoft.Teams.Apps.Plugins;
13using Microsoft.Teams.Common.Http;
14using Microsoft.Teams.Common.Logging;
15using Microsoft.Teams.Common.Storage;
16
17namespace Microsoft.Teams.Apps;
18
19public partial class App
20{
21 public static AppBuilder Builder(AppOptions? options = null) => new(options);
22
23 /// <summary>
24 /// the apps id
25 /// </summary>
26 public string? Id => Token?.AppId;
27
28 /// <summary>
29 /// the apps name
30 /// </summary>
31 public string? Name => Token?.AppDisplayName;
32
33 public Status? Status { get; internal set; }
34 public ILogger Logger { get; }
35 public IStorage<string, object> Storage { get; }
36 public ApiClient Api { get; internal set; }
37 public IHttpClient Client { get; }
38 public IHttpCredentials? Credentials { get; }
39 public IToken? Token { get; internal set; }
40 public OAuthSettings OAuth { get; internal set; }
41
42 internal IHttpClient TokenClient { get; set; }
43 internal IServiceProvider? Provider { get; set; }
44 internal IContainer Container { get; set; }
45 internal string UserAgent
46 {
47 get
48 {
49 var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
50 version ??= "0.0.0";
51 return $"teams.net[apps]/{version}";
52 }
53 }
54
55 internal App() : this(null!, null)
56 { }
57
58 public App(IHttpCredentials credentials, AppOptions? options = null)
59 {
60 Logger = options?.Logger ?? new ConsoleLogger();
61 Storage = options?.Storage ?? new LocalStorage<object>();
62 Credentials = credentials;
63 Plugins = options?.Plugins ?? [];
64 OAuth = options?.OAuth ?? new OAuthSettings();
65 Provider = options?.Provider;
66
67 TokenClient = new Common.Http.HttpClient();
68 Client = options?.Client ?? options?.ClientFactory?.CreateClient() ?? new Common.Http.HttpClient();
69 Client.Options.AddUserAgent(UserAgent);
70 Client.Options.TokenFactory ??= () =>
71 {
72 if (Credentials is not null)
73 {
74 if (Token is null)
75 {
76 var res = Api!.Bots.Token.GetAsync(Credentials, TokenClient)
77 .ConfigureAwait(false)
78 .GetAwaiter()
79 .GetResult();
80
81 Token = new JsonWebToken(res.AccessToken);
82 }
83
84 if (Token.IsExpired)
85 {
86 var res = Credentials.Resolve(TokenClient, [.. Token.Scopes.DefaultIfEmpty(BotTokenClient.BotScope)])
87 .ConfigureAwait(false)
88 .GetAwaiter()
89 .GetResult();
90
91 Token = new JsonWebToken(res.AccessToken);
92 }
93 }
94
95 return Token;
96 };
97
98 Api = new ApiClient("https://smba.trafficmanager.net/teams/", Client);
99 Container = new Container();
100 Container.Register(Logger);
101 Container.Register(Storage);
102 Container.Register(Client);
103 Container.Register(Api);
104 Container.Register<IHttpCredentials>(new FactoryProvider(() => Credentials));
105 Container.Register("AppId", new FactoryProvider(() => Id));
106 Container.Register("AppName", new FactoryProvider(() => Name));
107 Container.Register("Token", new FactoryProvider(() => Token));
108
109 this.OnTokenExchange(OnTokenExchangeActivity);
110 this.OnVerifyState(OnVerifyStateActivity);
111 this.OnError(OnErrorEvent);
112 this.OnActivitySent(OnActivitySentEvent);
113 this.OnActivityResponse(OnActivityResponseEvent);
114
115 Events.On(EventType.Activity, (plugin, @event, token) =>
116 {
117 return OnActivityEvent((ISenderPlugin)plugin, (ActivityEvent)@event, token);
118 });
119
120 Status = Apps.Status.Ready;
121 }
122
123 /// <summary>
124 /// start the app
125 /// </summary>
126 public async Task Start(CancellationToken cancellationToken = default)
127 {
128 try
129 {
130 foreach (var plugin in Plugins)
131 {
132 Inject(plugin);
133 }
134
135 if (Credentials is not null)
136 {
137 try
138 {
139 var res = await Api.Bots.Token.GetAsync(Credentials, TokenClient);
140 Token = new JsonWebToken(res.AccessToken);
141 }
142 catch (Exception ex)
143 {
144 Logger.Error("Failed to get bot token on app startup.", ex);
145 }
146 }
147
148 Logger.Debug(Id);
149 Logger.Debug(Name);
150
151 foreach (var plugin in Plugins)
152 {
153 await plugin.OnInit(this, cancellationToken);
154 }
155
156 foreach (var plugin in Plugins)
157 {
158 await plugin.OnStart(this, cancellationToken);
159 }
160
161 Status = Apps.Status.Started;
162 }
163 catch (Exception ex)
164 {
165 Status = Apps.Status.Stopped;
166 await Events.Emit(
167 null!,
168 EventType.Error,
169 new ErrorEvent() { Exception = ex }
170 );
171 }
172 }
173
174 /// <summary>
175 /// send an activity to the conversation
176 /// </summary>
177 /// <param name="activity">activity activity to send</param>
178 public async Task<T> Send<T>(string conversationId, T activity, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default) where T : IActivity
179 {
180 return await Send(conversationId, activity, conversationType, serviceUrl, false, cancellationToken);
181 }
182
183 /// <summary>
184 /// send an activity to the conversation
185 /// </summary>
186 /// <param name="activity">activity activity to send</param>
187 /// <param name="isTargeted">when true, sends the message privately to the specified recipient; when false, sends to all conversation participants</param>
188 /// <remarks>
189 /// <para>Targeted messages are delivered privately to the recipient specified in the activity's Recipient property.</para>
190 /// <para>The <paramref name="isTargeted"/> parameter is in preview.</para>
191 /// </remarks>
192 public async Task<T> Send<T>(string conversationId, T activity, ConversationType? conversationType, string? serviceUrl, bool isTargeted = false, 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 ?? ConversationType.Personal
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, isTargeted, 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, false, cancellationToken);
242 }
243
244 /// <summary>
245 /// send a message activity to the conversation
246 /// </summary>
247 /// <param name="text">the text to send</param>
248 /// <param name="isTargeted">when true, sends the message privately to the specified recipient; when false, sends to all conversation participants</param>
249 /// <remarks>
250 /// <para>Targeted messages are delivered privately to the recipient specified in the activity's Recipient property.</para>
251 /// <para>The <paramref name="isTargeted"/> parameter is in preview.</para>
252 /// </remarks>
253 public async Task<MessageActivity> Send(string conversationId, string text, ConversationType? conversationType, string? serviceUrl, bool isTargeted = false, CancellationToken cancellationToken = default)
254 {
255 return await Send(conversationId, new MessageActivity(text), conversationType, serviceUrl, isTargeted, cancellationToken);
256 }
257
258 /// <summary>
259 /// send a message activity with a card attachment
260 /// </summary>
261 /// <param name="card">the card to send as an attachment</param>
262 public async Task<MessageActivity> Send(string conversationId, Cards.AdaptiveCard card, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default)
263 {
264 return await Send(conversationId, new MessageActivity().AddAttachment(card), conversationType, serviceUrl, false, cancellationToken);
265 }
266
267 /// <summary>
268 /// send a message activity with a card attachment
269 /// </summary>
270 /// <param name="card">the card to send as an attachment</param>
271 /// <param name="isTargeted">when true, sends the message privately to the specified recipient; when false, sends to all conversation participants</param>
272 /// <remarks>
273 /// <para>Targeted messages are delivered privately to the recipient specified in the activity's Recipient property.</para>
274 /// <para>The <paramref name="isTargeted"/> parameter is in preview.</para>
275 /// </remarks>
276 public async Task<MessageActivity> Send(string conversationId, Cards.AdaptiveCard card, ConversationType? conversationType, string? serviceUrl, bool isTargeted = false, CancellationToken cancellationToken = default)
277 {
278 return await Send(conversationId, new MessageActivity().AddAttachment(card), conversationType, serviceUrl, isTargeted, cancellationToken);
279 }
280
281 /// <summary>
282 /// process an activity
283 /// </summary>
284 /// <param name="sender">the plugin to use</param>
285 /// <param name="token">the request token</param>
286 /// <param name="activity">the inbound activity</param>
287 /// <param name="cancellationToken">the cancellation token</param>
288 public async Task<Response> Process(ISenderPlugin sender, IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default)
289 {
290 return await Process(sender, new()
291 {
292 Token = token,
293 Activity = activity,
294 Extra = extra
295 }, cancellationToken);
296 }
297
298 /// <summary>
299 /// process an activity
300 /// </summary>
301 /// <param name="sender">the plugin to use</param>
302 /// <param name="token">the request token</param>
303 /// <param name="activity">the inbound activity</param>
304 /// <param name="cancellationToken">the cancellation token</param>
305 /// <exception cref="Exception"></exception>
306 public Task<Response> Process(string sender, IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default)
307 {
308 var plugin = ((ISenderPlugin?)GetPlugin(sender)) ?? throw new Exception($"sender plugin '{sender}' not found");
309 return Process(plugin, token, activity, extra, cancellationToken);
310 }
311
312 /// <summary>
313 /// process an activity
314 /// </summary>
315 /// <param name="token">the request token</param>
316 /// <param name="activity">the inbound activity</param>
317 /// <param name="cancellationToken">the cancellation token</param>
318 /// <exception cref="Exception"></exception>
319 public Task<Response> Process<TPlugin>(IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default) where TPlugin : ISenderPlugin
320 {
321 var plugin = GetPlugin<TPlugin>() ?? throw new Exception($"sender plugin '{typeof(TPlugin).Name}' not found");
322 return Process(plugin, token, activity, extra, cancellationToken);
323 }
324
325 /// <summary>
326 /// process an activity
327 /// </summary>
328 /// <param name="sender">the plugin to use</param>
329 /// <param name="@event">the activity event</param>
330 /// <param name="cancellationToken">the cancellation token</param>
331 private async Task<Response> Process(ISenderPlugin sender, ActivityEvent @event, CancellationToken cancellationToken = default)
332 {
333 var start = DateTime.UtcNow;
334 var routes = Router.Select(@event.Activity);
335 JsonWebToken? userToken = null;
336
337 var api = new ApiClient(Api);
338
339 try
340 {
341 var tokenResponse = await api.Users.Token.GetAsync(new()
342 {
343 UserId = @event.Activity.From.Id,
344 ChannelId = @event.Activity.ChannelId,
345 ConnectionName = OAuth.DefaultConnectionName
346 });
347
348 userToken = new JsonWebToken(tokenResponse);
349 }
350 catch { }
351
352 var path = @event.Activity.GetPath();
353 Logger.Debug(path);
354
355 var reference = new ConversationReference()
356 {
357 ServiceUrl = @event.Activity.ServiceUrl ?? @event.Token.ServiceUrl,
358 ChannelId = @event.Activity.ChannelId,
359 Bot = @event.Activity.Recipient,
360 User = @event.Activity.From,
361 Locale = @event.Activity.Locale,
362 Conversation = @event.Activity.Conversation,
363 };
364
365 object? data = null;
366 var i = -1;
367 async Task<object?> Next(IContext<IActivity> context)
368 {
369 if (i + 1 == routes.Count) return data;
370
371 i++;
372 var res = await routes[i].Invoke(context);
373
374 if (res is not null)
375 data = res;
376
377 return res;
378 }
379
380 var stream = sender.CreateStream(reference, cancellationToken);
381 var context = new Context<IActivity>(sender, stream)
382 {
383 AppId = @event.Token.AppId ?? Id ?? string.Empty,
384 TenantId = @event.Token.TenantId ?? string.Empty,
385 Log = Logger.Child(path),
386 Storage = Storage,
387 Api = api,
388 Activity = @event.Activity,
389 Ref = reference,
390 IsSignedIn = userToken is not null,
391 OnNext = Next,
392 Extra = @event.Extra ?? new Dictionary<string, object?>(),
393 UserGraphToken = userToken,
394 CancellationToken = cancellationToken,
395 ConnectionName = OAuth.DefaultConnectionName,
396 OnActivitySent = async (activity, context) =>
397 {
398 await Events.Emit(
399 context.Sender,
400 EventType.ActivitySent,
401 new ActivitySentEvent() { Activity = activity },
402 context.CancellationToken
403 );
404 }
405 };
406
407 stream.OnChunk += async activity =>
408 {
409 await Events.Emit(
410 sender,
411 EventType.ActivitySent,
412 new ActivitySentEvent() { Activity = activity },
413 cancellationToken
414 );
415 };
416
417 try
418 {
419 if (@event.Services is not null)
420 {
421 var accessor = (IContext.Accessor?)@event.Services.GetService(typeof(IContext.Accessor));
422
423 if (accessor is not null)
424 {
425 accessor.Value = context;
426 }
427 }
428
429 foreach (var plugin in Plugins)
430 {
431 await plugin.OnActivity(this, sender, @event, cancellationToken);
432 }
433
434 var res = await Next(context);
435 await stream.Close();
436
437 var response = res is Response value
438 ? value
439 : new Response(System.Net.HttpStatusCode.OK, res);
440
441 response.Meta.Routes = i + 1;
442 response.Meta.Elapse = (DateTime.UtcNow - start).Milliseconds;
443
444 await Events.Emit(
445 sender,
446 EventType.ActivityResponse,
447 new ActivityResponseEvent() { Response = response },
448 cancellationToken
449 );
450
451 return response;
452 }
453 catch (Exception ex)
454 {
455 await Events.Emit(
456 sender,
457 EventType.Error,
458 new ErrorEvent() { Exception = ex, Context = context.ToActivityType<IActivity>() },
459 cancellationToken
460 );
461
462 return new Response(System.Net.HttpStatusCode.InternalServerError);
463 }
464 }
465}