microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/issue-274-skipauth-unauthorized

Branches

Tags

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

Clone

HTTPS

Download ZIP

Libraries/Microsoft.Teams.Apps/App.cs

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