microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
next/core

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Apps/App.cs

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