microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/add-new-feature

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Apps/App.cs

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