// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Microsoft.Teams.Api; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Api.Auth; using Microsoft.Teams.Api.Clients; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Events; using Microsoft.Teams.Apps.Plugins; using Microsoft.Teams.Common.Http; using Microsoft.Teams.Common.Logging; using Microsoft.Teams.Common.Storage; namespace Microsoft.Teams.Apps; public partial class App { public static AppBuilder Builder(AppOptions? options = null) => new(options); /// /// the apps id /// public string? Id => Token?.AppId; /// /// the apps name /// public string? Name => Token?.AppDisplayName; public Status? Status { get; internal set; } public ILogger Logger { get; } public IStorage Storage { get; } public ApiClient Api { get; internal set; } public IHttpClient Client { get; } public IHttpCredentials? Credentials { get; } public IToken? Token { get; internal set; } public OAuthSettings OAuth { get; internal set; } /// /// When true, performs a per-activity user OAuth token lookup to populate /// IContext.IsSignedIn / IContext.UserGraphToken. Set to false to /// skip the call when SSO is not configured. Defaults to true. /// public bool AutoUserTokenLookup { get; internal set; } internal IHttpClient TokenClient { get; set; } internal IServiceProvider? Provider { get; set; } internal IContainer Container { get; set; } internal string UserAgent { get { var version = ThisAssembly.NuGetPackageVersion ?? "0.0.0"; return $"teams.net[apps]/{version}"; } } public App(AppOptions? options = null) { var cloud = options?.Cloud ?? CloudEnvironment.Public; Logger = options?.Logger ?? new ConsoleLogger(); Storage = options?.Storage ?? new LocalStorage(); Credentials = options?.Credentials; Plugins = options?.Plugins ?? []; OAuth = options?.OAuth ?? new OAuthSettings(); AutoUserTokenLookup = options?.AutoUserTokenLookup ?? true; Provider = options?.Provider; TokenClient = new Common.Http.HttpClient(); Client = options?.Client ?? options?.ClientFactory?.CreateClient() ?? new Common.Http.HttpClient(); Client.Options.AddUserAgent(UserAgent); Client.Options.TokenFactory ??= () => { if (Credentials is not null) { if (Token is null) { var res = Api!.Bots.Token.GetAsync(Credentials, TokenClient) .ConfigureAwait(false) .GetAwaiter() .GetResult(); Token = new JsonWebToken(res.AccessToken); } if (Token.IsExpired) { var res = Credentials.Resolve(TokenClient, [.. Token.Scopes.DefaultIfEmpty(Api!.Bots.Token.ActiveBotScope)]) .ConfigureAwait(false) .GetAwaiter() .GetResult(); Token = new JsonWebToken(res.AccessToken); } } return Token; }; Api = new ApiClient("https://smba.trafficmanager.net/teams/", Client); Api.Bots.Token.ActiveBotScope = cloud.BotScope; Api.Bots.Token.ActiveGraphScope = cloud.GraphScope; Api.Bots.SignIn.TokenServiceUrl = cloud.TokenServiceUrl; Api.Users.Token.TokenServiceUrl = cloud.TokenServiceUrl; Container = new Container(); Container.Register(Logger); Container.Register(Storage); Container.Register(Client); Container.Register(Api); Container.Register(new FactoryProvider(() => Credentials)); Container.Register("AppId", new FactoryProvider(() => Id)); Container.Register("AppName", new FactoryProvider(() => Name)); Container.Register("Token", new FactoryProvider(() => Token)); this.OnTokenExchange(OnTokenExchangeActivity); this.OnVerifyState(OnVerifyStateActivity); this.OnSignInFailure(OnSignInFailureActivity); this.OnError(OnErrorEvent); this.OnActivitySent(OnActivitySentEvent); this.OnActivityResponse(OnActivityResponseEvent); Events.On(EventType.Activity, (plugin, @event, token) => { return OnActivityEvent((ISenderPlugin)plugin, (ActivityEvent)@event, token); }); Status = Apps.Status.Ready; } /// /// start the app /// public async Task Start(CancellationToken cancellationToken = default) { try { foreach (var plugin in Plugins) { Inject(plugin); } if (Credentials is not null) { try { var res = await Api.Bots.Token.GetAsync(Credentials, TokenClient).ConfigureAwait(false); Token = new JsonWebToken(res.AccessToken); } catch (Exception ex) { Logger.Error("Failed to get bot token on app startup.", ex); } } Logger.Debug(Id); Logger.Debug(Name); foreach (var plugin in Plugins) { await plugin.OnInit(this, cancellationToken).ConfigureAwait(false); } foreach (var plugin in Plugins) { await plugin.OnStart(this, cancellationToken).ConfigureAwait(false); } Status = Apps.Status.Started; } catch (Exception ex) { Status = Apps.Status.Stopped; await Events.Emit( null!, EventType.Error, new ErrorEvent() { Exception = ex } ).ConfigureAwait(false); } } /// /// send an activity proactively to a conversation. /// Sends to the exact conversation ID provided. For channel threads, /// the conversation ID must include ;messageid= -- use /// to construct it, or use /// which handles this automatically. /// public async Task Send(string conversationId, T activity, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default) where T : IActivity { if (Id is null) { throw new InvalidOperationException("app not started"); } var reference = new ConversationReference() { ChannelId = ChannelId.MsTeams, ServiceUrl = serviceUrl ?? Api.ServiceUrl, Bot = new() { Id = Id, Name = Name, }, Conversation = new() { Id = conversationId, Type = conversationType } }; var sender = Plugins.Where(plugin => plugin is ISenderPlugin).Select(plugin => plugin as ISenderPlugin).First(); if (sender is null) { throw new Exception("no plugin that can send activities was found"); } var res = await sender.Send(activity, reference, cancellationToken).ConfigureAwait(false); await Events.Emit( sender, EventType.ActivitySent, new ActivitySentEvent() { Activity = res }, cancellationToken ).ConfigureAwait(false); return res; } /// /// send a message activity to the conversation /// /// the text to send public async Task Send(string conversationId, string text, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default) { return await Send(conversationId, new MessageActivity(text), conversationType, serviceUrl, cancellationToken).ConfigureAwait(false); } /// /// send a message activity with a card attachment /// /// the card to send as an attachment public async Task Send(string conversationId, Cards.AdaptiveCard card, ConversationType? conversationType = null, string? serviceUrl = null, CancellationToken cancellationToken = default) { return await Send(conversationId, new MessageActivity().AddAttachment(card), conversationType, serviceUrl, cancellationToken).ConfigureAwait(false); } /// /// send an activity proactively to a conversation, optionally as a threaded reply. /// Constructs a threaded conversation ID from the conversation ID /// and message ID via , /// then sends to that thread. The service determines whether threading is /// supported for the given conversation type. /// /// the conversation ID /// the thread root message ID /// the activity to send /// optional cancellation token public Task Reply(string conversationId, string messageId, T activity, CancellationToken cancellationToken = default) where T : IActivity { return Send(Conversation.ToThreadedConversationId(conversationId, messageId), activity, cancellationToken: cancellationToken); } /// /// send an activity proactively to a conversation. /// Sends to the exact conversation ID provided - threaded if /// it contains ;messageid=, flat otherwise. /// /// the conversation to send to /// the activity to send /// optional cancellation token public Task Reply(string conversationId, T activity, CancellationToken cancellationToken = default) where T : IActivity { return Send(conversationId, activity, cancellationToken: cancellationToken); } /// /// send a message proactively to a thread /// public Task Reply(string conversationId, string messageId, string text, CancellationToken cancellationToken = default) { return Reply(conversationId, messageId, new MessageActivity(text), cancellationToken); } /// /// send a message proactively to a conversation /// public Task Reply(string conversationId, string text, CancellationToken cancellationToken = default) { return Reply(conversationId, new MessageActivity(text), cancellationToken); } /// /// send a card proactively to a thread /// public Task Reply(string conversationId, string messageId, Cards.AdaptiveCard card, CancellationToken cancellationToken = default) { return Reply(conversationId, messageId, new MessageActivity().AddAttachment(card), cancellationToken); } /// /// send a card proactively to a conversation /// public Task Reply(string conversationId, Cards.AdaptiveCard card, CancellationToken cancellationToken = default) { return Reply(conversationId, new MessageActivity().AddAttachment(card), cancellationToken); } /// /// process an activity /// /// the plugin to use /// the request token /// the inbound activity /// the cancellation token public async Task Process(ISenderPlugin sender, IToken token, IActivity activity, IDictionary? extra = null, CancellationToken cancellationToken = default) { return await Process(sender, new() { Token = token, Activity = activity, Extra = extra }, cancellationToken).ConfigureAwait(false); } /// /// process an activity /// /// the plugin to use /// the request token /// the inbound activity /// the cancellation token /// public Task Process(string sender, IToken token, IActivity activity, IDictionary? extra = null, CancellationToken cancellationToken = default) { var plugin = ((ISenderPlugin?)GetPlugin(sender)) ?? throw new Exception($"sender plugin '{sender}' not found"); return Process(plugin, token, activity, extra, cancellationToken); } /// /// process an activity /// /// the request token /// the inbound activity /// the cancellation token /// public Task Process(IToken token, IActivity activity, IDictionary? extra = null, CancellationToken cancellationToken = default) where TPlugin : ISenderPlugin { var plugin = GetPlugin() ?? throw new Exception($"sender plugin '{typeof(TPlugin).Name}' not found"); return Process(plugin, token, activity, extra, cancellationToken); } /// /// process an activity /// /// the plugin to use /// the activity event /// the cancellation token private async Task Process(ISenderPlugin sender, ActivityEvent @event, CancellationToken cancellationToken = default) { var start = DateTime.UtcNow; var routes = Router.Select(@event.Activity); JsonWebToken? userToken = null; var api = new ApiClient(Api, cancellationToken); if (AutoUserTokenLookup) { try { var tokenResponse = await api.Users.Token.GetAsync(new() { UserId = @event.Activity.From.Id, ChannelId = @event.Activity.ChannelId, ConnectionName = OAuth.DefaultConnectionName }, cancellationToken).ConfigureAwait(false); userToken = new JsonWebToken(tokenResponse); } catch (OperationCanceledException) { throw; } catch { } } var path = @event.Activity.GetPath(); Logger.Debug(path); var serviceUrl = @event.Activity.ServiceUrl ?? @event.Token.ServiceUrl; var reference = new ConversationReference() { ServiceUrl = serviceUrl, ChannelId = @event.Activity.ChannelId, Bot = @event.Activity.Recipient, User = @event.Activity.From, Locale = @event.Activity.Locale, Conversation = @event.Activity.Conversation, }; object? data = null; var i = -1; async Task Next(IContext context) { if (i + 1 == routes.Count) return data; i++; var res = await routes[i].Invoke(context).ConfigureAwait(false); if (res is not null) data = res; return res; } var stream = sender.CreateStream(reference, cancellationToken); var context = new Context(sender, stream) { AppId = @event.Token.AppId ?? Id ?? string.Empty, TenantId = @event.Token.TenantId ?? string.Empty, Log = Logger.Child(path), Storage = Storage, Api = api, Activity = @event.Activity, Ref = reference, IsSignedIn = userToken is not null, OnNext = Next, Extra = @event.Extra ?? new Dictionary(), UserGraphToken = userToken, CancellationToken = cancellationToken, ConnectionName = OAuth.DefaultConnectionName, OnActivitySent = async (activity, context) => { await Events.Emit( context.Sender, EventType.ActivitySent, new ActivitySentEvent() { Activity = activity }, context.CancellationToken ).ConfigureAwait(false); } }; stream.OnChunk += async activity => { await Events.Emit( sender, EventType.ActivitySent, new ActivitySentEvent() { Activity = activity }, cancellationToken ).ConfigureAwait(false); }; if (@event.Services is not null) { var accessor = (IContext.Accessor?)@event.Services.GetService(typeof(IContext.Accessor)); if (accessor is not null) { accessor.Value = context; } } foreach (var plugin in Plugins) { await plugin.OnActivity(this, sender, @event, cancellationToken).ConfigureAwait(false); } var res = await Next(context).ConfigureAwait(false); await stream.Close(cancellationToken).ConfigureAwait(false); var response = res is Response value ? value : new Response(System.Net.HttpStatusCode.OK, res); response.Meta.Routes = i + 1; response.Meta.Elapse = (DateTime.UtcNow - start).Milliseconds; await Events.Emit( sender, EventType.ActivityResponse, new ActivityResponseEvent() { Response = response }, cancellationToken ).ConfigureAwait(false); return response; } }