microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/port-similar-changes-409-again

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Apps/App.cs

462lines · 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 public App(AppOptions? options = null)
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(BotTokenClient.BotScope)])
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 Container = new Container();
97 Container.Register(Logger);
98 Container.Register(Storage);
99 Container.Register(Client);
100 Container.Register(Api);
101 Container.Register<IHttpCredentials>(new FactoryProvider(() => Credentials));
102 Container.Register("AppId", new FactoryProvider(() => Id));
103 Container.Register("AppName", new FactoryProvider(() => Name));
104 Container.Register("Token", new FactoryProvider(() => Token));
105
106 this.OnTokenExchange(OnTokenExchangeActivity);
107 this.OnVerifyState(OnVerifyStateActivity);
108 this.OnError(OnErrorEvent);
109 this.OnActivitySent(OnActivitySentEvent);
110 this.OnActivityResponse(OnActivityResponseEvent);
111
112 Events.On(EventType.Activity, (plugin, @event, token) =>
113 {
114 return OnActivityEvent((ISenderPlugin)plugin, (ActivityEvent)@event, token);
115 });
116
117 Status = Apps.Status.Ready;
118 }
119
120 /// <summary>
121 /// start the app
122 /// </summary>
123 public async Task Start(CancellationToken cancellationToken = default)
124 {
125 try
126 {
127 foreach (var plugin in Plugins)
128 {
129 Inject(plugin);
130 }
131
132 if (Credentials is not null)
133 {
134 try
135 {
136 var res = await Api.Bots.Token.GetAsync(Credentials, TokenClient);
137 Token = new JsonWebToken(res.AccessToken);
138 }
139 catch (Exception ex)
140 {
141 Logger.Error("Failed to get bot token on app startup.", ex);
142 }
143 }
144
145 Logger.Debug(Id);
146 Logger.Debug(Name);
147
148 foreach (var plugin in Plugins)
149 {
150 await plugin.OnInit(this, cancellationToken);
151 }
152
153 foreach (var plugin in Plugins)
154 {
155 await plugin.OnStart(this, cancellationToken);
156 }
157
158 Status = Apps.Status.Started;
159 }
160 catch (Exception ex)
161 {
162 Status = Apps.Status.Stopped;
163 await Events.Emit(
164 null!,
165 EventType.Error,
166 new ErrorEvent() { Exception = ex }
167 );
168 }
169 }
170
171 /// <summary>
172 /// send an activity to the conversation
173 /// </summary>
174 /// <param name="activity">activity activity to send</param>
175 public async Task<T> Send<T>(string conversationId, T activity, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default) where T : IActivity
176 {
177 return await Send(conversationId, activity, conversationType, serviceUrl, false, cancellationToken);
178 }
179
180 /// <summary>
181 /// send an activity to the conversation
182 /// </summary>
183 /// <param name="activity">activity activity to send</param>
184 /// <param name="isTargeted">when true, sends the message privately to the specified recipient; when false, sends to all conversation participants</param>
185 /// <remarks>
186 /// <para>Targeted messages are delivered privately to the recipient specified in the activity's Recipient property.</para>
187 /// <para>The <paramref name="isTargeted"/> parameter is in preview.</para>
188 /// </remarks>
189 public async Task<T> Send<T>(string conversationId, T activity, ConversationType? conversationType, string? serviceUrl, bool isTargeted = false, CancellationToken cancellationToken = default) where T : IActivity
190 {
191 if (Id is null)
192 {
193 throw new InvalidOperationException("app not started");
194 }
195
196 var reference = new ConversationReference()
197 {
198 ChannelId = ChannelId.MsTeams,
199 ServiceUrl = serviceUrl ?? Api.ServiceUrl,
200 Bot = new()
201 {
202 Id = Id,
203 Name = Name,
204 Role = Role.Bot
205 },
206 Conversation = new()
207 {
208 Id = conversationId,
209 Type = conversationType ?? ConversationType.Personal
210 }
211 };
212
213 var sender = Plugins.Where(plugin => plugin is ISenderPlugin).Select(plugin => plugin as ISenderPlugin).First();
214
215 if (sender is null)
216 {
217 throw new Exception("no plugin that can send activities was found");
218 }
219
220 var res = await sender.Send(activity, reference, isTargeted, cancellationToken);
221
222 await Events.Emit(
223 sender,
224 EventType.ActivitySent,
225 new ActivitySentEvent() { Activity = res },
226 cancellationToken
227 );
228
229 return res;
230 }
231
232 /// <summary>
233 /// send a message activity to the conversation
234 /// </summary>
235 /// <param name="text">the text to send</param>
236 public async Task<MessageActivity> Send(string conversationId, string text, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default)
237 {
238 return await Send(conversationId, new MessageActivity(text), conversationType, serviceUrl, false, cancellationToken);
239 }
240
241 /// <summary>
242 /// send a message activity to the conversation
243 /// </summary>
244 /// <param name="text">the text to send</param>
245 /// <param name="isTargeted">when true, sends the message privately to the specified recipient; when false, sends to all conversation participants</param>
246 /// <remarks>
247 /// <para>Targeted messages are delivered privately to the recipient specified in the activity's Recipient property.</para>
248 /// <para>The <paramref name="isTargeted"/> parameter is in preview.</para>
249 /// </remarks>
250 public async Task<MessageActivity> Send(string conversationId, string text, ConversationType? conversationType, string? serviceUrl, bool isTargeted = false, CancellationToken cancellationToken = default)
251 {
252 return await Send(conversationId, new MessageActivity(text), conversationType, serviceUrl, isTargeted, cancellationToken);
253 }
254
255 /// <summary>
256 /// send a message activity with a card attachment
257 /// </summary>
258 /// <param name="card">the card to send as an attachment</param>
259 public async Task<MessageActivity> Send(string conversationId, Cards.AdaptiveCard card, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default)
260 {
261 return await Send(conversationId, new MessageActivity().AddAttachment(card), conversationType, serviceUrl, false, cancellationToken);
262 }
263
264 /// <summary>
265 /// send a message activity with a card attachment
266 /// </summary>
267 /// <param name="card">the card to send as an attachment</param>
268 /// <param name="isTargeted">when true, sends the message privately to the specified recipient; when false, sends to all conversation participants</param>
269 /// <remarks>
270 /// <para>Targeted messages are delivered privately to the recipient specified in the activity's Recipient property.</para>
271 /// <para>The <paramref name="isTargeted"/> parameter is in preview.</para>
272 /// </remarks>
273 public async Task<MessageActivity> Send(string conversationId, Cards.AdaptiveCard card, ConversationType? conversationType, string? serviceUrl, bool isTargeted = false, CancellationToken cancellationToken = default)
274 {
275 return await Send(conversationId, new MessageActivity().AddAttachment(card), conversationType, serviceUrl, isTargeted, cancellationToken);
276 }
277
278 /// <summary>
279 /// process an activity
280 /// </summary>
281 /// <param name="sender">the plugin to use</param>
282 /// <param name="token">the request token</param>
283 /// <param name="activity">the inbound activity</param>
284 /// <param name="cancellationToken">the cancellation token</param>
285 public async Task<Response> Process(ISenderPlugin sender, IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default)
286 {
287 return await Process(sender, new()
288 {
289 Token = token,
290 Activity = activity,
291 Extra = extra
292 }, cancellationToken);
293 }
294
295 /// <summary>
296 /// process an activity
297 /// </summary>
298 /// <param name="sender">the plugin to use</param>
299 /// <param name="token">the request token</param>
300 /// <param name="activity">the inbound activity</param>
301 /// <param name="cancellationToken">the cancellation token</param>
302 /// <exception cref="Exception"></exception>
303 public Task<Response> Process(string sender, IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default)
304 {
305 var plugin = ((ISenderPlugin?)GetPlugin(sender)) ?? throw new Exception($"sender plugin '{sender}' not found");
306 return Process(plugin, token, activity, extra, cancellationToken);
307 }
308
309 /// <summary>
310 /// process an activity
311 /// </summary>
312 /// <param name="token">the request token</param>
313 /// <param name="activity">the inbound activity</param>
314 /// <param name="cancellationToken">the cancellation token</param>
315 /// <exception cref="Exception"></exception>
316 public Task<Response> Process<TPlugin>(IToken token, IActivity activity, IDictionary<string, object?>? extra = null, CancellationToken cancellationToken = default) where TPlugin : ISenderPlugin
317 {
318 var plugin = GetPlugin<TPlugin>() ?? throw new Exception($"sender plugin '{typeof(TPlugin).Name}' not found");
319 return Process(plugin, token, activity, extra, cancellationToken);
320 }
321
322 /// <summary>
323 /// process an activity
324 /// </summary>
325 /// <param name="sender">the plugin to use</param>
326 /// <param name="@event">the activity event</param>
327 /// <param name="cancellationToken">the cancellation token</param>
328 private async Task<Response> Process(ISenderPlugin sender, ActivityEvent @event, CancellationToken cancellationToken = default)
329 {
330 var start = DateTime.UtcNow;
331 var routes = Router.Select(@event.Activity);
332 JsonWebToken? userToken = null;
333
334 var api = new ApiClient(Api);
335
336 try
337 {
338 var tokenResponse = await api.Users.Token.GetAsync(new()
339 {
340 UserId = @event.Activity.From.Id,
341 ChannelId = @event.Activity.ChannelId,
342 ConnectionName = OAuth.DefaultConnectionName
343 });
344
345 userToken = new JsonWebToken(tokenResponse);
346 }
347 catch { }
348
349 var path = @event.Activity.GetPath();
350 Logger.Debug(path);
351
352 var reference = new ConversationReference()
353 {
354 ServiceUrl = @event.Activity.ServiceUrl ?? @event.Token.ServiceUrl,
355 ChannelId = @event.Activity.ChannelId,
356 Bot = @event.Activity.Recipient,
357 User = @event.Activity.From,
358 Locale = @event.Activity.Locale,
359 Conversation = @event.Activity.Conversation,
360 };
361
362 object? data = null;
363 var i = -1;
364 async Task<object?> Next(IContext<IActivity> context)
365 {
366 if (i + 1 == routes.Count) return data;
367
368 i++;
369 var res = await routes[i].Invoke(context);
370
371 if (res is not null)
372 data = res;
373
374 return res;
375 }
376
377 var stream = sender.CreateStream(reference, cancellationToken);
378 var context = new Context<IActivity>(sender, stream)
379 {
380 AppId = @event.Token.AppId ?? Id ?? string.Empty,
381 TenantId = @event.Token.TenantId ?? string.Empty,
382 Log = Logger.Child(path),
383 Storage = Storage,
384 Api = api,
385 Activity = @event.Activity,
386 Ref = reference,
387 IsSignedIn = userToken is not null,
388 OnNext = Next,
389 Extra = @event.Extra ?? new Dictionary<string, object?>(),
390 UserGraphToken = userToken,
391 CancellationToken = cancellationToken,
392 ConnectionName = OAuth.DefaultConnectionName,
393 OnActivitySent = async (activity, context) =>
394 {
395 await Events.Emit(
396 context.Sender,
397 EventType.ActivitySent,
398 new ActivitySentEvent() { Activity = activity },
399 context.CancellationToken
400 );
401 }
402 };
403
404 stream.OnChunk += async activity =>
405 {
406 await Events.Emit(
407 sender,
408 EventType.ActivitySent,
409 new ActivitySentEvent() { Activity = activity },
410 cancellationToken
411 );
412 };
413
414 try
415 {
416 if (@event.Services is not null)
417 {
418 var accessor = (IContext.Accessor?)@event.Services.GetService(typeof(IContext.Accessor));
419
420 if (accessor is not null)
421 {
422 accessor.Value = context;
423 }
424 }
425
426 foreach (var plugin in Plugins)
427 {
428 await plugin.OnActivity(this, sender, @event, cancellationToken);
429 }
430
431 var res = await Next(context);
432 await stream.Close();
433
434 var response = res is Response value
435 ? value
436 : new Response(System.Net.HttpStatusCode.OK, res);
437
438 response.Meta.Routes = i + 1;
439 response.Meta.Elapse = (DateTime.UtcNow - start).Milliseconds;
440
441 await Events.Emit(
442 sender,
443 EventType.ActivityResponse,
444 new ActivityResponseEvent() { Response = response },
445 cancellationToken
446 );
447
448 return response;
449 }
450 catch (Exception ex)
451 {
452 await Events.Emit(
453 sender,
454 EventType.Error,
455 new ErrorEvent() { Exception = ex, Context = context.ToActivityType<IActivity>() },
456 cancellationToken
457 );
458
459 return new Response(System.Net.HttpStatusCode.InternalServerError);
460 }
461 }
462}