microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/a365-mcp

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Apps/App.cs

488lines · 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);
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);
164 }
165
166 foreach (var plugin in Plugins)
167 {
168 await plugin.OnStart(this, cancellationToken);
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 );
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 Role = Role.Bot
207 },
208 Conversation = new()
209 {
210 Id = conversationId,
211 Type = conversationType
212 }
213 };
214
215 var sender = Plugins.Where(plugin => plugin is ISenderPlugin).Select(plugin => plugin as ISenderPlugin).First();
216
217 if (sender is null)
218 {
219 throw new Exception("no plugin that can send activities was found");
220 }
221
222 var res = await sender.Send(activity, reference, cancellationToken);
223
224 await Events.Emit(
225 sender,
226 EventType.ActivitySent,
227 new ActivitySentEvent() { Activity = res },
228 cancellationToken
229 );
230
231 return res;
232 }
233
234 /// <summary>
235 /// send a message activity to the conversation
236 /// </summary>
237 /// <param name="text">the text to send</param>
238 public async Task<MessageActivity> Send(string conversationId, string text, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default)
239 {
240 return await Send(conversationId, new MessageActivity(text), conversationType, serviceUrl, cancellationToken);
241 }
242
243 /// <summary>
244 /// send a message activity with a card attachment
245 /// </summary>
246 /// <param name="card">the card to send as an attachment</param>
247 public async Task<MessageActivity> Send(string conversationId, Cards.AdaptiveCard card, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default)
248 {
249 return await Send(conversationId, new MessageActivity().AddAttachment(card), conversationType, serviceUrl, cancellationToken);
250 }
251
252 /// <summary>
253 /// send an activity proactively to a conversation, optionally as a threaded reply.
254 /// Constructs a threaded conversation ID from the conversation ID
255 /// and message ID via <see cref="Conversation.ToThreadedConversationId"/>,
256 /// then sends to that thread. The service determines whether threading is
257 /// supported for the given conversation type.
258 /// </summary>
259 /// <param name="conversationId">the conversation ID</param>
260 /// <param name="messageId">the thread root message ID</param>
261 /// <param name="activity">the activity to send</param>
262 /// <param name="cancellationToken">optional cancellation token</param>
263 public Task<T> Reply<T>(string conversationId, string messageId, T activity, CancellationToken cancellationToken = default) where T : IActivity
264 {
265 return Send(Conversation.ToThreadedConversationId(conversationId, messageId), activity, cancellationToken: cancellationToken);
266 }
267
268 /// <summary>
269 /// send an activity proactively to a conversation.
270 /// Sends to the exact conversation ID provided - threaded if
271 /// it contains <c>;messageid=</c>, flat otherwise.
272 /// </summary>
273 /// <param name="conversationId">the conversation to send to</param>
274 /// <param name="activity">the activity to send</param>
275 /// <param name="cancellationToken">optional cancellation token</param>
276 public Task<T> Reply<T>(string conversationId, T activity, CancellationToken cancellationToken = default) where T : IActivity
277 {
278 return Send(conversationId, activity, cancellationToken: cancellationToken);
279 }
280
281 /// <summary>
282 /// send a message proactively to a thread
283 /// </summary>
284 public Task<MessageActivity> Reply(string conversationId, string messageId, string text, CancellationToken cancellationToken = default)
285 {
286 return Reply(conversationId, messageId, new MessageActivity(text), cancellationToken);
287 }
288
289 /// <summary>
290 /// send a message proactively to a conversation
291 /// </summary>
292 public Task<MessageActivity> Reply(string conversationId, string text, CancellationToken cancellationToken = default)
293 {
294 return Reply<MessageActivity>(conversationId, new MessageActivity(text), cancellationToken);
295 }
296
297 /// <summary>
298 /// send a card proactively to a thread
299 /// </summary>
300 public Task<MessageActivity> Reply(string conversationId, string messageId, Cards.AdaptiveCard card, CancellationToken cancellationToken = default)
301 {
302 return Reply(conversationId, messageId, new MessageActivity().AddAttachment(card), cancellationToken);
303 }
304
305 /// <summary>
306 /// send a card proactively to a conversation
307 /// </summary>
308 public Task<MessageActivity> Reply(string conversationId, Cards.AdaptiveCard card, CancellationToken cancellationToken = default)
309 {
310 return Reply<MessageActivity>(conversationId, new MessageActivity().AddAttachment(card), cancellationToken);
311 }
312
313 /// <summary>
314 /// process an activity
315 /// </summary>
316 /// <param name="sender">the plugin to use</param>
317 /// <param name="token">the request token</param>
318 /// <param name="activity">the inbound activity</param>
319 /// <param name="cancellationToken">the cancellation token</param>
320 public async Task<Response> Process(ISenderPlugin sender, IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default)
321 {
322 return await Process(sender, new()
323 {
324 Token = token,
325 Activity = activity,
326 Extra = extra
327 }, cancellationToken);
328 }
329
330 /// <summary>
331 /// process an activity
332 /// </summary>
333 /// <param name="sender">the plugin to use</param>
334 /// <param name="token">the request token</param>
335 /// <param name="activity">the inbound activity</param>
336 /// <param name="cancellationToken">the cancellation token</param>
337 /// <exception cref="Exception"></exception>
338 public Task<Response> Process(string sender, IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default)
339 {
340 var plugin = ((ISenderPlugin?)GetPlugin(sender)) ?? throw new Exception($"sender plugin '{sender}' not found");
341 return Process(plugin, token, activity, extra, cancellationToken);
342 }
343
344 /// <summary>
345 /// process an activity
346 /// </summary>
347 /// <param name="token">the request token</param>
348 /// <param name="activity">the inbound activity</param>
349 /// <param name="cancellationToken">the cancellation token</param>
350 /// <exception cref="Exception"></exception>
351 public Task<Response> Process<TPlugin>(IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default) where TPlugin : ISenderPlugin
352 {
353 var plugin = GetPlugin<TPlugin>() ?? throw new Exception($"sender plugin '{typeof(TPlugin).Name}' not found");
354 return Process(plugin, token, activity, extra, cancellationToken);
355 }
356
357 /// <summary>
358 /// process an activity
359 /// </summary>
360 /// <param name="sender">the plugin to use</param>
361 /// <param name="@event">the activity event</param>
362 /// <param name="cancellationToken">the cancellation token</param>
363 private async Task<Response> Process(ISenderPlugin sender, ActivityEvent @event, CancellationToken cancellationToken = default)
364 {
365 var start = DateTime.UtcNow;
366 var routes = Router.Select(@event.Activity);
367 JsonWebToken? userToken = null;
368
369 var api = new ApiClient(Api, cancellationToken);
370
371 if (AutoUserTokenLookup)
372 {
373 try
374 {
375 var tokenResponse = await api.Users.Token.GetAsync(new()
376 {
377 UserId = @event.Activity.From.Id,
378 ChannelId = @event.Activity.ChannelId,
379 ConnectionName = OAuth.DefaultConnectionName
380 });
381
382 userToken = new JsonWebToken(tokenResponse);
383 }
384 catch { }
385 }
386
387 var path = @event.Activity.GetPath();
388 Logger.Debug(path);
389
390 var serviceUrl = @event.Activity.ServiceUrl ?? @event.Token.ServiceUrl;
391
392 var reference = new ConversationReference()
393 {
394 ServiceUrl = serviceUrl,
395 ChannelId = @event.Activity.ChannelId,
396 Bot = @event.Activity.Recipient,
397 User = @event.Activity.From,
398 Locale = @event.Activity.Locale,
399 Conversation = @event.Activity.Conversation,
400 };
401
402 object? data = null;
403 var i = -1;
404 async Task<object?> Next(IContext<IActivity> context)
405 {
406 if (i + 1 == routes.Count) return data;
407
408 i++;
409 var res = await routes[i].Invoke(context);
410
411 if (res is not null)
412 data = res;
413
414 return res;
415 }
416
417 var stream = sender.CreateStream(reference, cancellationToken);
418 var context = new Context<IActivity>(sender, stream)
419 {
420 AppId = @event.Token.AppId ?? Id ?? string.Empty,
421 TenantId = @event.Token.TenantId ?? string.Empty,
422 Log = Logger.Child(path),
423 Storage = Storage,
424 Api = api,
425 Activity = @event.Activity,
426 Ref = reference,
427 IsSignedIn = userToken is not null,
428 OnNext = Next,
429 Extra = @event.Extra ?? new Dictionary<string, object?>(),
430 UserGraphToken = userToken,
431 CancellationToken = cancellationToken,
432 ConnectionName = OAuth.DefaultConnectionName,
433 OnActivitySent = async (activity, context) =>
434 {
435 await Events.Emit(
436 context.Sender,
437 EventType.ActivitySent,
438 new ActivitySentEvent() { Activity = activity },
439 context.CancellationToken
440 );
441 }
442 };
443
444 stream.OnChunk += async activity =>
445 {
446 await Events.Emit(
447 sender,
448 EventType.ActivitySent,
449 new ActivitySentEvent() { Activity = activity },
450 cancellationToken
451 );
452 };
453
454 if (@event.Services is not null)
455 {
456 var accessor = (IContext.Accessor?)@event.Services.GetService(typeof(IContext.Accessor));
457
458 if (accessor is not null)
459 {
460 accessor.Value = context;
461 }
462 }
463
464 foreach (var plugin in Plugins)
465 {
466 await plugin.OnActivity(this, sender, @event, cancellationToken);
467 }
468
469 var res = await Next(context);
470 await stream.Close(cancellationToken);
471
472 var response = res is Response value
473 ? value
474 : new Response(System.Net.HttpStatusCode.OK, res);
475
476 response.Meta.Routes = i + 1;
477 response.Meta.Elapse = (DateTime.UtcNow - start).Milliseconds;
478
479 await Events.Emit(
480 sender,
481 EventType.ActivityResponse,
482 new ActivityResponseEvent() { Response = response },
483 cancellationToken
484 );
485
486 return response;
487 }
488}