diff --git a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/AdapterWithErrorHandler.cs b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/AdapterWithErrorHandler.cs new file mode 100644 index 0000000000..6915912d89 --- /dev/null +++ b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/AdapterWithErrorHandler.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + + +namespace Microsoft.BotBuilderSamples +{ + public class AdapterWithErrorHandler : BotFrameworkHttpAdapter + { + public AdapterWithErrorHandler(IConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + OnTurnError = async (turnContext, exception) => + { + // Log any leaked exception from the application. + logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); + + // Send a message to the user + await turnContext.SendActivityAsync("The bot encounted an error or bug."); + await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code."); + + // Send a trace activity, which will be displayed in the Bot Framework Emulator + await SendTraceActivityAsync(turnContext, exception); + }; + } + + private static async Task SendTraceActivityAsync(ITurnContext turnContext, Exception exception) + { + // Only send a trace activity if we're talking to the Bot Framework Emulator + if (turnContext.Activity.ChannelId == Channels.Emulator) + { + Activity traceActivity = new Activity(ActivityTypes.Trace) + { + Label = "TurnError", + Name = "OnTurnError Trace", + Value = exception.Message, + ValueType = "https://www.botframework.com/schemas/error", + }; + + // Send a trace activity + await turnContext.SendActivityAsync(traceActivity); + } + } + } +} diff --git a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Bots/TeamsMessagingExtensionsSearchAuthConfigBot.cs b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Bots/TeamsMessagingExtensionsSearchAuthConfigBot.cs new file mode 100644 index 0000000000..c8534eef12 --- /dev/null +++ b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Bots/TeamsMessagingExtensionsSearchAuthConfigBot.cs @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using AdaptiveCards; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Teams; +using Microsoft.Bot.Schema; +using Microsoft.Bot.Schema.Teams; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json.Linq; + +namespace Microsoft.BotBuilderSamples.Bots +{ + public class TeamsMessagingExtensionsSearchAuthConfigBot : TeamsActivityHandler + { + readonly string _connectionName; + readonly string _siteUrl; + readonly UserState _userState; + readonly IStatePropertyAccessor _userConfigProperty; + + public TeamsMessagingExtensionsSearchAuthConfigBot(IConfiguration configuration, UserState userState) + { + _connectionName = configuration["ConnectionName"] ?? throw new NullReferenceException("ConnectionName"); + _siteUrl = configuration["SiteUrl"] ?? throw new NullReferenceException("SiteUrl"); + _userState = userState ?? throw new NullReferenceException(nameof(userState)); + _userConfigProperty = userState.CreateProperty("UserConfiguration"); + } + + public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) + { + await base.OnTurnAsync(turnContext, cancellationToken); + + // After the turn is complete, persist any UserState changes. + await _userState.SaveChangesAsync(turnContext); + } + + protected override async Task OnTeamsMessagingExtensionConfigurationQuerySettingUrlAsync(ITurnContext turnContext, MessagingExtensionQuery query, CancellationToken cancellationToken) + { + // The user has requested the Messaging Extension Configuration page. + var escapedSettings = string.Empty; + var userConfigSettings = await _userConfigProperty.GetAsync(turnContext, () => string.Empty); + + if (!string.IsNullOrEmpty(userConfigSettings)) + { + escapedSettings = Uri.EscapeDataString(userConfigSettings); + } + + return new MessagingExtensionResponse + { + ComposeExtension = new MessagingExtensionResult + { + Type = "config", + SuggestedActions = new MessagingExtensionSuggestedAction + { + Actions = new List + { + new CardAction + { + Type = ActionTypes.OpenUrl, + Value = $"{_siteUrl}/searchSettings.html?settings={escapedSettings}", + }, + }, + }, + }, + }; + } + + protected override async Task OnTeamsMessagingExtensionConfigurationSettingAsync(ITurnContext turnContext, JObject settings, CancellationToken cancellationToken) + { + // When the user submits the settings page, this event is fired. + var state = settings["state"]; + if (state != null) + { + var userConfigSettings = state.ToString(); + await _userConfigProperty.SetAsync(turnContext, userConfigSettings, cancellationToken); + } + } + + protected override async Task OnTeamsMessagingExtensionQueryAsync(ITurnContext turnContext, MessagingExtensionQuery action, CancellationToken cancellationToken) + { + var text = action?.Parameters?[0]?.Value as string ?? string.Empty; + + var attachments = new List(); + var userConfigSettings = await _userConfigProperty.GetAsync(turnContext, () => string.Empty); + if (userConfigSettings.ToUpper().Contains("EMAIL")) + { + // When the Bot Service Auth flow completes, the action.State will contain a magic code used for verification. + var magicCode = string.Empty; + var state = action.State; + if (!string.IsNullOrEmpty(state)) + { + int parsed = 0; + if (int.TryParse(state, out parsed)) + { + magicCode = parsed.ToString(); + } + } + + var tokenResponse = await (turnContext.Adapter as IUserTokenProvider).GetUserTokenAsync(turnContext, _connectionName, magicCode, cancellationToken: cancellationToken); + if (tokenResponse == null || string.IsNullOrEmpty(tokenResponse.Token)) + { + // There is no token, so the user has not signed in yet. + + // Retrieve the OAuth Sign in Link to use in the MessagingExtensionResult Suggested Actions + var signInLink = await (turnContext.Adapter as IUserTokenProvider).GetOauthSignInLinkAsync(turnContext, _connectionName, cancellationToken); + + return new MessagingExtensionResponse + { + ComposeExtension = new MessagingExtensionResult + { + Type = "auth", + SuggestedActions = new MessagingExtensionSuggestedAction + { + Actions = new List + { + new CardAction + { + Type = ActionTypes.OpenUrl, + Value = signInLink, + Title = "Bot Service OAuth", + }, + }, + }, + }, + }; + } + + var client = new SimpleGraphClient(tokenResponse.Token); + + var messages = await client.SearchMailInboxAsync(text); + + // Here we construct a ThumbnailCard for every attachment, and provide a HeroCard which will be + // displayed if the selects that item. + attachments = messages.Select(msg => new MessagingExtensionAttachment + { + ContentType = HeroCard.ContentType, + Content = new HeroCard + { + Title = msg.From.EmailAddress.Address, + Subtitle = msg.Subject, + Text = msg.Body.Content, + }, + Preview = new ThumbnailCard + { + Title = msg.From.EmailAddress.Address, + Text = $"{msg.Subject}
{msg.BodyPreview}", + Images = new List() + { + new CardImage("https://raw.githubusercontent.com/microsoft/botbuilder-samples/master/docs/media/OutlookLogo.jpg", "Outlook Logo"), + }, + }.ToAttachment() + } + ).ToList(); + } + else + { + var packages = await FindPackages(text); + // We take every row of the results and wrap them in cards wrapped in in MessagingExtensionAttachment objects. + // The Preview is optional, if it includes a Tap, that will trigger the OnTeamsMessagingExtensionSelectItemAsync event back on this bot. + attachments = packages.Select(package => { + var previewCard = new ThumbnailCard { Title = package.Item1, Tap = new CardAction { Type = "invoke", Value = package } }; + if (!string.IsNullOrEmpty(package.Item5)) + { + previewCard.Images = new List() { new CardImage(package.Item5, "Icon") }; + } + + var attachment = new MessagingExtensionAttachment + { + ContentType = HeroCard.ContentType, + Content = new HeroCard { Title = package.Item1 }, + Preview = previewCard.ToAttachment() + }; + + return attachment; + }).ToList(); + } + + // The list of MessagingExtensionAttachments must we wrapped in a MessagingExtensionResult wrapped in a MessagingExtensionResponse. + return new MessagingExtensionResponse + { + ComposeExtension = new MessagingExtensionResult + { + Type = "result", + AttachmentLayout = "list", + Attachments = attachments + } + }; + } + + protected override Task OnTeamsMessagingExtensionSelectItemAsync(ITurnContext turnContext, JObject query, CancellationToken cancellationToken) + { + // The Preview card's Tap should have a Value property assigned, this will be returned to the bot in this event. + var (packageId, version, description, projectUrl, iconUrl) = query.ToObject<(string, string, string, string, string)>(); + + // We take every row of the results and wrap them in cards wrapped in in MessagingExtensionAttachment objects. + // The Preview is optional, if it includes a Tap, that will trigger the OnTeamsMessagingExtensionSelectItemAsync event back on this bot. + var card = new ThumbnailCard + { + Title = $"{packageId}, {version}", + Subtitle = description, + Buttons = new List + { + new CardAction { Type = ActionTypes.OpenUrl, Title = "Nuget Package", Value = $"https://www.nuget.org/packages/{packageId}" }, + new CardAction { Type = ActionTypes.OpenUrl, Title = "Project", Value = projectUrl }, + }, + }; + + if (!string.IsNullOrEmpty(iconUrl)) + { + card.Images = new List() { new CardImage(iconUrl, "Icon") }; + } + + var attachment = new MessagingExtensionAttachment + { + ContentType = ThumbnailCard.ContentType, + Content = card, + }; + + return Task.FromResult(new MessagingExtensionResponse + { + ComposeExtension = new MessagingExtensionResult + { + Type = "result", + AttachmentLayout = "list", + Attachments = new List { attachment } + } + }); + } + + protected override Task OnTeamsMessagingExtensionSubmitActionAsync(ITurnContext turnContext, MessagingExtensionAction action, CancellationToken cancellationToken) + { + // This method is to handle the 'Close' button on the confirmation Task Module after the user signs out. + return Task.FromResult(new MessagingExtensionActionResponse()); + } + + protected override async Task OnTeamsMessagingExtensionFetchTaskAsync(ITurnContext turnContext, MessagingExtensionQuery query, CancellationToken cancellationToken) + { + if (query.CommandId.ToUpper() == "SIGNOUTCOMMAND") + { + await (turnContext.Adapter as IUserTokenProvider).SignOutUserAsync(turnContext, _connectionName, turnContext.Activity.From.Id, cancellationToken); + + return new MessagingExtensionActionResponse + { + Task = new TaskModuleContinueResponse + { + Value = new TaskModuleTaskInfo + { + Card = new Attachment + { + Content = new AdaptiveCard(new AdaptiveSchemaVersion("1.0")) + { + Body = new List() { new AdaptiveTextBlock() { Text = "You have been signed out." } }, + Actions = new List() { new AdaptiveSubmitAction() { Title = "Close" } }, + }, + ContentType = AdaptiveCard.ContentType, + }, + Height = 200, + Width = 400, + Title = "Adaptive Card: Inputs", + }, + }, + }; + } + return null; + } + + // Generate a set of substrings to illustrate the idea of a set of results coming back from a query. + private async Task> FindPackages(string text) + { + var obj = JObject.Parse(await (new HttpClient()).GetStringAsync($"https://azuresearch-usnc.nuget.org/query?q=id:{text}&prerelease=true")); + return obj["data"].Select(item => (item["id"].ToString(), item["version"].ToString(), item["description"].ToString(), item["projectUrl"]?.ToString(), item["iconUrl"]?.ToString())); + } + } +} diff --git a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Controllers/BotController.cs b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Controllers/BotController.cs new file mode 100644 index 0000000000..35514d52ea --- /dev/null +++ b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Controllers/BotController.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; + +namespace Microsoft.BotBuilderSamples.Controllers +{ + // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot + // implementation at runtime. Multiple different IBot implementations running at different endpoints can be + // achieved by specifying a more specific type for the bot constructor argument. + [Route("api/messages")] + [ApiController] + public class BotController : ControllerBase + { + private readonly IBotFrameworkHttpAdapter Adapter; + private readonly IBot Bot; + + public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) + { + Adapter = adapter; + Bot = bot; + } + + [HttpPost] + public async Task PostAsync() + { + // Delegate the processing of the HTTP POST to the adapter. + // The adapter will invoke the bot. + await Adapter.ProcessAsync(Request, Response, Bot); + } + } +} diff --git a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Program.cs b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Program.cs index c86ee6a28d..8a3b6c4a16 100644 --- a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Program.cs +++ b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Program.cs @@ -10,6 +10,11 @@ public class Program { public static void Main(string[] args) { + CreateWebHostBuilder(args).Build().Run(); } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); } } diff --git a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Properties/launchSettings.json b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Properties/launchSettings.json index d083f1d86d..078ea792fc 100644 --- a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Properties/launchSettings.json +++ b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:10734/", + "applicationUrl": "http://localhost:3978/", "sslPort": 0 } }, @@ -21,7 +21,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://localhost:10735/" + "applicationUrl": "http://localhost:3978/" } } } \ No newline at end of file diff --git a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/README.md b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/README.md index 4d11ba06c2..03dead1365 100644 --- a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/README.md +++ b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/README.md @@ -2,7 +2,7 @@ Bot Framework v4 Teams Messaging Extensions Search with Auth and Config sample. -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts search requests from the user and returns the results. The sample also incorporates auth and config. +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts search requests from the user and returns the results. The sample also incorporates auth and config via Messaging Extension. ## Prerequisites @@ -42,11 +42,17 @@ This bot has been created using [Bot Framework](https://dev.botframework.com), i ## Testing the bot using Teams 1) run ngrok - point to port 3978 -1) create bot framework registration - using ngrok URL -1) update your manifest.json to include the app id from bot framework -1) zip up teams-manifest folder to create a manifest.zip -1) upload manifest.zip to teams (from Apps view click "Upload a custom app") -1) pick your bot from the compose command menu +2) add the ngrok url to appsettings.json - SiteUrl +3) create bot framework registration - using ngrok URL +4) add MicrosoftAppId and MicrosoftAppPassword values to appsettings.json +5) add an AAD V2 OAuth Connection Setting +scopes: email Mail.Read User.Read openid profile User.ReadBasic.All Mail.Send.Shared +(see https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication) +6) add AAD ConnectionName to appsettings.json +7) update your manifest.json to include the app id from bot framework +8) zip up teams-manifest folder to create a manifest.zip +9) upload manifest.zip to teams (from Apps view click "Upload a custom app") +10) pick your bot from the compose command menu ## Deploy the bot to Azure diff --git a/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/SimpleGraphClient.cs b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/SimpleGraphClient.cs new file mode 100644 index 0000000000..696862fa2b --- /dev/null +++ b/samples/csharp_dotnetcore/52.teams-messaging-extensions-search-auth-config/SimpleGraphClient.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.Graph; + +namespace Microsoft.BotBuilderSamples +{ + // This class is a wrapper for the Microsoft Graph API + // See: https://developer.microsoft.com/en-us/graph + public class SimpleGraphClient + { + private readonly string _token; + + public SimpleGraphClient(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new ArgumentNullException(nameof(token)); + } + + _token = token; + } + + // Searches the user's mail Inbox using the Microsoft Graph API + public async Task SearchMailInboxAsync(string search) + { + var graphClient = GetAuthenticatedClient(); + var searchQuery = new QueryOption("search", search); + var messages = await graphClient.Me.MailFolders.Inbox.Messages.Request(new List