├── .gitignore ├── JamesMann.BotFramework.Dialogs ├── Date │ ├── DisambiguateDateDialog.cs │ └── DisambiguateDateDialogStateWrapper.cs ├── Extensions │ ├── RecognizerExtensions.cs │ └── Resolution.cs ├── JamesMann.BotFramework.Dialogs.csproj └── Time │ ├── DisambiguateTimeDialog.cs │ └── DisambiguateTimeDialogStateWrapper.cs ├── JamesMann.BotFramework.Middleware ├── AzureAdAuthMiddleware.cs ├── Controllers │ └── AuthController.cs ├── Extensions │ ├── ActivityExtensions.cs │ ├── AzureAdExtensions.cs │ ├── CardExtensions.cs │ ├── SentimentExtensions.cs │ └── SpellCheckExtensions.cs ├── IAuthTokenStorage.cs ├── JamesMann.BotFramework.Middleware.csproj ├── JamesMann.BotFramework.Middleware.sln ├── Resource.Designer.cs ├── Resource.resx ├── SentimentMiddleware.cs ├── ServiceCredentials │ └── ApiKeyServiceClientCredentials.cs ├── SpellCheckMiddleware.cs └── TypingMiddleware.cs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/db.lock 2 | **/bin/**/*.* 3 | **/obj/**/*.* 4 | **/.vs/**/*.* -------------------------------------------------------------------------------- /JamesMann.BotFramework.Dialogs/Date/DisambiguateDateDialog.cs: -------------------------------------------------------------------------------- 1 | using JamesMann.BotFramework.Dialogs.Extensions; 2 | using Microsoft.Bot.Builder.Dialogs; 3 | using Microsoft.Bot.Builder.Prompts.Choices; 4 | using Microsoft.Recognizers.Text; 5 | using Microsoft.Recognizers.Text.DateTime; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace JamesMann.BotFramework.Dialogs.Date 10 | { 11 | public class DisambiguateDateDialog : DialogContainer 12 | { 13 | private DisambiguateDateDialog() : base(Id) 14 | { 15 | var recognizer = new DateTimeRecognizer(Culture.English); 16 | var model = recognizer.GetDateTimeModel(); 17 | 18 | Dialogs.Add(Id, new WaterfallStep[] 19 | { 20 | async(dc,args, next) =>{ 21 | await dc.Context.SendActivity("What date?"); 22 | }, 23 | async (dc, args, next) => 24 | { 25 | var stateWrapper = new DisambiguateDateDialogStateWrapper(dc.ActiveDialog.State); 26 | 27 | var value = model.Parse(dc.Context.Activity.Text).ParseRecognizer(); 28 | 29 | stateWrapper.Resolutions = value; 30 | 31 | if (value.ResolutionType != Resolution.ResolutionTypes.DateTime && value.ResolutionType != Resolution.ResolutionTypes.DateTimeRange) 32 | { 33 | await dc.Context.SendActivity("I don't understand. Please provide a date"); 34 | await dc.Replace(Id); 35 | } 36 | else if (value.NeedsDisambiguation) 37 | { 38 | var amOrPmChoices = new List(new []{new Choice() { Value = "AM" }, new Choice() { Value = "PM" } }); 39 | await dc.Prompt("choicePrompt", "Is that AM or PM?", new ChoicePromptOptions 40 | { 41 | Choices = amOrPmChoices 42 | }).ConfigureAwait(false); 43 | } 44 | else 45 | { 46 | stateWrapper.Date = value.FirstOrDefault().Date1.Value; 47 | await dc.End(dc.ActiveDialog.State); 48 | } 49 | }, 50 | async (dc, args, next) => 51 | { 52 | var stateWrapper = new DisambiguateDateDialogStateWrapper(dc.ActiveDialog.State); 53 | var amOrPmChoice = ((FoundChoice) args["Value"]).Value; 54 | var availableTimes = stateWrapper.Resolutions.Select(x=>x.Date1.Value); 55 | stateWrapper.Date = amOrPmChoice == "AM" ? availableTimes.Min() : availableTimes.Max(); 56 | await dc.End(dc.ActiveDialog.State); 57 | } 58 | }); 59 | 60 | Dialogs.Add("textPrompt", new TextPrompt()); 61 | Dialogs.Add("choicePrompt", new ChoicePrompt("en")); 62 | } 63 | public static string Id => "disambiguateDateDialog"; 64 | 65 | public static DisambiguateDateDialog Instance = new DisambiguateDateDialog(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Dialogs/Date/DisambiguateDateDialogStateWrapper.cs: -------------------------------------------------------------------------------- 1 | using JamesMann.BotFramework.Dialogs.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace JamesMann.BotFramework.Dialogs.Date 6 | { 7 | // convenience helper to get/set dialog state 8 | internal class DisambiguateDateDialogStateWrapper 9 | { 10 | public DisambiguateDateDialogStateWrapper(IDictionary state) 11 | { 12 | State = state; 13 | } 14 | 15 | public IDictionary State { get; } 16 | 17 | public RecognizerExtensions.ResolutionList Resolutions { 18 | get 19 | { 20 | return State["recognizedDateTime"] as RecognizerExtensions.ResolutionList; 21 | } 22 | set 23 | { 24 | State["recognizedDateTime"] = value; 25 | } 26 | } 27 | 28 | public DateTime Date 29 | { 30 | get 31 | { 32 | return (DateTime)State["date"]; 33 | } 34 | set 35 | { 36 | State["date"] = value; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Dialogs/Extensions/RecognizerExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Recognizers.Text; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace JamesMann.BotFramework.Dialogs.Extensions 7 | { 8 | internal static class RecognizerExtensions 9 | { 10 | internal static ResolutionList ParseRecognizer(this List value) 11 | { 12 | var result = new ResolutionList(); 13 | 14 | foreach (var modelResult in value) 15 | { 16 | var resolution = modelResult.Resolution; 17 | if (resolution["values"] is List> resolutionValues) 18 | { 19 | foreach (Dictionary possibleTime in resolutionValues) 20 | { 21 | if (possibleTime.ContainsKey("type") && possibleTime["type"] == "time") 22 | { 23 | result.Add(new Resolution() { ResolutionType = Resolution.ResolutionTypes.Time, Time1 = TimeSpan.Parse(possibleTime["value"]) }); 24 | } 25 | else if (possibleTime.ContainsKey("type") && possibleTime["type"] == "datetime") 26 | { 27 | var date1 = DateTime.Parse(possibleTime["value"]); 28 | if (date1 > DateTime.Now) 29 | { 30 | result.Add(new Resolution() { ResolutionType = Resolution.ResolutionTypes.DateTime, Date1 = date1 }); 31 | } 32 | } 33 | else if (possibleTime.ContainsKey("type") && possibleTime["type"] == "date") 34 | { 35 | var date1 = DateTime.Parse(possibleTime["value"]); 36 | if (date1 > DateTime.Now) 37 | { 38 | result.Add(new Resolution() { ResolutionType = Resolution.ResolutionTypes.DateTime, Date1 = date1 }); 39 | } 40 | } 41 | else if (possibleTime.ContainsKey("type") && possibleTime["type"] == "datetimerange") 42 | { 43 | var date1 = DateTime.Parse(possibleTime["start"]); 44 | var date2 = DateTime.Parse(possibleTime["end"]); 45 | 46 | if (date1 > DateTime.Now && date2 > DateTime.Now) 47 | { 48 | result.Add(new Resolution() { ResolutionType = Resolution.ResolutionTypes.DateTimeRange, Date1 = DateTime.Parse(possibleTime["start"]), Date2 = DateTime.Parse(possibleTime["end"]) }); 49 | } 50 | } 51 | else if (possibleTime.ContainsKey("type") && possibleTime["type"] == "timerange") 52 | { 53 | result.Add(new Resolution() { ResolutionType = Resolution.ResolutionTypes.TimeRange, Time1 = TimeSpan.Parse(possibleTime["start"]), Time2 = TimeSpan.Parse(possibleTime["end"]) }); 54 | } 55 | else 56 | { 57 | 58 | } 59 | } 60 | } 61 | } 62 | 63 | return result; 64 | } 65 | 66 | internal class ResolutionList : List 67 | { 68 | public Resolution.ResolutionTypes ResolutionType 69 | { 70 | get 71 | { 72 | return this.Count > 0 ? this.FirstOrDefault().ResolutionType : Resolution.ResolutionTypes.Unknown; 73 | } 74 | } 75 | 76 | public bool NeedsDisambiguation 77 | { 78 | get 79 | { 80 | return this.Count > 1; 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Dialogs/Extensions/Resolution.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JamesMann.BotFramework.Dialogs.Extensions 4 | { 5 | internal class Resolution 6 | { 7 | internal enum ResolutionTypes 8 | { 9 | Time, DateTime, TimeRange, DateTimeRange, Unknown 10 | } 11 | 12 | public ResolutionTypes ResolutionType { get; set; } 13 | 14 | public TimeSpan? Time1 { get; set; } 15 | public TimeSpan? Time2 { get; set; } 16 | public DateTime? Date1 { get; set; } 17 | public DateTime? Date2 { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Dialogs/JamesMann.BotFramework.Dialogs.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | jamesemann;arafat 18 | A collection of Bot Framework V4 extensions and dialogs. 19 | https://github.com/jamesemann/JamesMann.BotFramework/blob/master/LICENSE 20 | https://github.com/jamesemann/JamesMann.BotFramework 21 | bot framework;cognitive services 22 | https://github.com/jamesemann/JamesMann.BotFramework 23 | 24 | 25 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Dialogs/Time/DisambiguateTimeDialog.cs: -------------------------------------------------------------------------------- 1 | using JamesMann.BotFramework.Dialogs.Extensions; 2 | using Microsoft.Bot.Builder.Dialogs; 3 | using Microsoft.Bot.Builder.Prompts.Choices; 4 | using Microsoft.Recognizers.Text; 5 | using Microsoft.Recognizers.Text.DateTime; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace JamesMann.BotFramework.Dialogs.Time 10 | { 11 | public class DisambiguateTimeDialog : DialogContainer 12 | { 13 | private DisambiguateTimeDialog() : base(Id) 14 | { 15 | var recognizer = new DateTimeRecognizer(Culture.English); 16 | var model = recognizer.GetDateTimeModel(); 17 | 18 | Dialogs.Add(Id, new WaterfallStep[] 19 | { 20 | async(dc,args, next) =>{ 21 | await dc.Context.SendActivity("What time?"); 22 | }, 23 | async (dc, args, next) => 24 | { 25 | var stateWrapper = new DisambiguateTimeDialogStateWrapper(dc.ActiveDialog.State); 26 | 27 | var value = model.Parse(dc.Context.Activity.Text).ParseRecognizer(); 28 | 29 | stateWrapper.Resolutions = value; 30 | 31 | if (value.ResolutionType != Resolution.ResolutionTypes.Time && value.ResolutionType != Resolution.ResolutionTypes.TimeRange) 32 | { 33 | await dc.Context.SendActivity("I don't understand. Please provide a time"); 34 | await dc.Replace(Id); 35 | } 36 | else if (value.NeedsDisambiguation) 37 | { 38 | var amOrPmChoices = new List(new []{new Choice() { Value = "AM" }, new Choice() { Value = "PM" } }); 39 | await dc.Prompt("choicePrompt", "Is that AM or PM?", new ChoicePromptOptions 40 | { 41 | Choices = amOrPmChoices 42 | }).ConfigureAwait(false); 43 | } 44 | else 45 | { 46 | stateWrapper.Time = value.FirstOrDefault().Time1.Value; 47 | await dc.End(dc.ActiveDialog.State); 48 | } 49 | }, 50 | async (dc, args, next) => 51 | { 52 | var stateWrapper = new DisambiguateTimeDialogStateWrapper(dc.ActiveDialog.State); 53 | var amOrPmChoice = ((FoundChoice) args["Value"]).Value; 54 | var availableTimes = stateWrapper.Resolutions.Select(x=>x.Time1.Value); 55 | stateWrapper.Time = amOrPmChoice == "AM" ? availableTimes.Min() : availableTimes.Max(); 56 | await dc.End(dc.ActiveDialog.State); 57 | } 58 | }); 59 | 60 | Dialogs.Add("textPrompt", new TextPrompt()); 61 | Dialogs.Add("choicePrompt", new ChoicePrompt("en")); 62 | } 63 | public static string Id => "disambiguateTimeDialog"; 64 | 65 | public static DisambiguateTimeDialog Instance = new DisambiguateTimeDialog(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Dialogs/Time/DisambiguateTimeDialogStateWrapper.cs: -------------------------------------------------------------------------------- 1 | using JamesMann.BotFramework.Dialogs.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace JamesMann.BotFramework.Dialogs.Time 6 | { 7 | // convenience helper to get/set dialog state 8 | internal class DisambiguateTimeDialogStateWrapper 9 | { 10 | public DisambiguateTimeDialogStateWrapper(IDictionary state) 11 | { 12 | State = state; 13 | } 14 | 15 | public IDictionary State { get; } 16 | 17 | public RecognizerExtensions.ResolutionList Resolutions { 18 | get 19 | { 20 | return State["recognizedDateTime"] as RecognizerExtensions.ResolutionList; 21 | } 22 | set 23 | { 24 | State["recognizedDateTime"] = value; 25 | } 26 | } 27 | 28 | public TimeSpan Time 29 | { 30 | get 31 | { 32 | return (TimeSpan)State["time"]; 33 | } 34 | set 35 | { 36 | State["time"] = value; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/AzureAdAuthMiddleware.cs: -------------------------------------------------------------------------------- 1 | using JamesMann.BotFramework.Middleware.Extensions; 2 | using Microsoft.Bot.Builder; 3 | using Microsoft.Extensions.Configuration; 4 | using Newtonsoft.Json; 5 | using RoomBookingBot.Chatbot.Extensions; 6 | using RoomBookingBot.Extensions; 7 | using System; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | 12 | namespace JamesMann.BotFramework.Middleware 13 | { 14 | public class AzureAdAuthMiddleware : IMiddleware 15 | { 16 | public AzureAdAuthMiddleware(IAuthTokenStorage tokenStorage, IConfiguration configuration) 17 | { 18 | TokenStorage = tokenStorage; 19 | AzureAdTenant = configuration.GetValue("AzureAdTenant"); 20 | AppClientId = configuration.GetValue("AppClientId"); 21 | AppRedirectUri = configuration.GetValue("AppRedirectUri"); 22 | AppClientSecret = configuration.GetValue("AppClientSecret"); 23 | PermissionsRequested = configuration.GetValue("PermissionsRequested"); 24 | } 25 | 26 | public IAuthTokenStorage TokenStorage { get; } 27 | public string AzureAdTenant { get; } 28 | public string AppClientId { get; } 29 | public string AppRedirectUri { get; } 30 | public string AppClientSecret { get; } 31 | public string PermissionsRequested { get; } 32 | 33 | public const string AUTH_TOKEN_KEY = "authToken"; 34 | 35 | public async Task OnTurn(ITurnContext context, MiddlewareSet.NextDelegate next) 36 | { 37 | var authToken = TokenStorage.LoadConfiguration(context.Activity.Conversation.Id); 38 | 39 | if (authToken == null) 40 | { 41 | if (context.Activity.UserHasJustSentMessage() || context.Activity.UserHasJustJoinedConversation()) 42 | { 43 | var conversationReference = TurnContext.GetConversationReference(context.Activity); 44 | 45 | var serializedCookie = WebUtility.UrlEncode(JsonConvert.SerializeObject(conversationReference)); 46 | 47 | var signInUrl = AzureAdExtensions.GetUserConsentLoginUrl(AzureAdTenant, AppClientId, AppRedirectUri, PermissionsRequested, serializedCookie); 48 | 49 | var activity = context.Activity.CreateReply(); 50 | activity.AddSignInCard(signInUrl); 51 | 52 | await context.SendActivity(activity); 53 | } 54 | } 55 | else if (authToken.ExpiresIn < DateTime.Now.AddMinutes(-10)) 56 | { 57 | if (context.Activity.UserHasJustSentMessage() || context.Activity.UserHasJustJoinedConversation()) 58 | { 59 | var client = new HttpClient(); 60 | var accessToken = await AzureAdExtensions.GetAccessTokenUsingRefreshToken(client, AzureAdTenant, authToken.RefreshToken, AppClientId, AppRedirectUri, AppClientSecret, PermissionsRequested); 61 | 62 | // have to save it 63 | authToken = new ConversationAuthToken(context.Activity.Conversation.Id) 64 | { 65 | AccessToken = accessToken.accessToken, 66 | RefreshToken = accessToken.refreshToken, 67 | ExpiresIn = accessToken.refreshTokenExpiresIn 68 | }; 69 | TokenStorage.SaveConfiguration(authToken); 70 | 71 | // make the authtoken available to downstream pipeline components 72 | context.Services.Add(AUTH_TOKEN_KEY, authToken); 73 | await next(); 74 | } 75 | } 76 | else 77 | { 78 | // make the authtoken available to downstream pipeline components 79 | context.Services.Add(AUTH_TOKEN_KEY, authToken); 80 | await next(); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Bot.Schema; 3 | using Microsoft.Extensions.Configuration; 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Threading.Tasks; 10 | using RoomBookingBot.Extensions; 11 | using Microsoft.Bot.Connector; 12 | namespace JamesMann.BotFramework.Middleware.Controllers 13 | { 14 | [Produces("application/json")] 15 | [Route("redirect")] 16 | public class AuthController : Controller 17 | { 18 | public AuthController(IAuthTokenStorage stateManager, IConfiguration configuration) 19 | { 20 | StateManager = stateManager; 21 | AzureAdTenant = configuration.GetValue("AzureAdTenant"); 22 | AppClientId = configuration.GetValue("AppClientId"); 23 | AppRedirectUri = configuration.GetValue("AppRedirectUri"); 24 | AppClientSecret = configuration.GetValue("AppClientSecret"); 25 | PermissionsRequested = configuration.GetValue("PermissionsRequested"); 26 | } 27 | 28 | public IAuthTokenStorage StateManager { get; } 29 | public string AzureAdTenant { get; } 30 | public string AppClientId { get; } 31 | public string AppRedirectUri { get; } 32 | public string AppClientSecret { get; } 33 | public string PermissionsRequested { get; } 34 | 35 | public async Task Get(string code, string state) 36 | { 37 | // get the conversation reference 38 | var botFrameworkConversationReference = JsonConvert.DeserializeObject(state); 39 | 40 | // get the access token and store against the conversation id 41 | var authToken = await new HttpClient().GetAccessTokenUsingAuthorizationCode(AzureAdTenant, code, AppClientId, AppRedirectUri, AppClientSecret, PermissionsRequested); 42 | StateManager.SaveConfiguration(new ConversationAuthToken(botFrameworkConversationReference.Conversation.Id) 43 | { 44 | AccessToken = authToken.accessToken, 45 | ExpiresIn = authToken.refreshTokenExpiresIn, 46 | RefreshToken = authToken.refreshToken 47 | }); 48 | 49 | // send a proactive message back to user 50 | var connectorClient = new ConnectorClient(new Uri(botFrameworkConversationReference.ServiceUrl)); 51 | var proactiveMessage = botFrameworkConversationReference.GetPostToUserMessage(); 52 | proactiveMessage.Text = "How can i help?"; 53 | //proactiveMessage.AddSuggestedActions(); 54 | connectorClient.Conversations.SendToConversation(proactiveMessage); 55 | return Content("Thank you! you have been logged in and may now close this window!"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/Extensions/ActivityExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Bot.Connector; 2 | using Microsoft.Bot.Schema; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace JamesMann.BotFramework.Middleware.Extensions 10 | { 11 | public static class ActivityExtensions 12 | { 13 | public static bool UserHasJustSentMessage(this Activity activity) 14 | { 15 | return activity.Type == ActivityTypes.Message; 16 | } 17 | 18 | public static bool UserHasJustJoinedConversation(this Activity activity) 19 | { 20 | return activity.Type == ActivityTypes.ConversationUpdate && activity.MembersAdded.FirstOrDefault().Id != activity.Recipient.Id; 21 | } 22 | 23 | public static async Task DoWithTyping(this IActivity activity, Func action) 24 | { 25 | var cts = new CancellationTokenSource(); 26 | 27 | activity.SendTypingActivity(cts.Token); 28 | 29 | await action.Invoke().ContinueWith(task => { cts.Cancel(); }); 30 | } 31 | 32 | private static async Task SendTypingActivity(this IActivity iactivity, CancellationToken cancellationToken) 33 | { 34 | if (iactivity is Activity activity) 35 | { 36 | var connector = new ConnectorClient(new Uri(activity.ServiceUrl)); 37 | 38 | while (!cancellationToken.IsCancellationRequested) 39 | { 40 | var isTypingReply = activity.CreateReply(); 41 | isTypingReply.Type = ActivityTypes.Typing; 42 | await connector.Conversations.ReplyToActivityAsync(isTypingReply); 43 | 44 | await Task.Delay(1000, cancellationToken).ContinueWith(task => { }); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/Extensions/AzureAdExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Dynamic; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | namespace RoomBookingBot.Extensions 9 | { 10 | public static class AzureAdExtensions 11 | { 12 | public static string GetUserConsentLoginUrl(string tenant, string clientId, string redirectUri, string permissionsRequested, string state) 13 | { 14 | return $"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?client_id={clientId}&scope={permissionsRequested}&response_type=code&response_mode=query&redirect_uri={redirectUri}&state={state}"; 15 | } 16 | 17 | public static async Task<(string accessToken, string refreshToken, DateTime refreshTokenExpiresIn)> GetAccessTokenUsingAuthorizationCode(this HttpClient client, string tenant, string code, string clientId, string redirectUri, string clientSecret, string permissionsRequested) 18 | { 19 | var timestampBeforeExecutingRequest = DateTime.Now; 20 | var formFields = new List> 21 | { 22 | new KeyValuePair("client_id", clientId), 23 | new KeyValuePair("redirect_uri", redirectUri), 24 | new KeyValuePair("client_secret", clientSecret), 25 | new KeyValuePair("scope", $"{permissionsRequested} offline_access"), 26 | new KeyValuePair("code", code), 27 | new KeyValuePair("grant_type", "authorization_code") 28 | }; 29 | var aadAccessTokenRequest = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token") { Content = new FormUrlEncodedContent(formFields) }; 30 | var aadAccessTokenResponse = await client.SendAsync(aadAccessTokenRequest); 31 | dynamic result = JsonConvert.DeserializeObject(await aadAccessTokenResponse.Content.ReadAsStringAsync()); 32 | 33 | return (accessToken: result.access_token, refreshToken: result.refresh_token, refreshTokenExpiresIn: timestampBeforeExecutingRequest.AddSeconds((int)result.expires_in)); 34 | } 35 | 36 | public static async Task<(string accessToken, string refreshToken, DateTime refreshTokenExpiresIn)> GetAccessTokenUsingRefreshToken(this HttpClient client, string tenant, string refreshToken, string clientId, string redirectUri, string clientSecret, string permissionsRequested) 37 | { 38 | var formFields = new List> 39 | { 40 | new KeyValuePair("client_id", clientId), 41 | new KeyValuePair("refresh_token", refreshToken), 42 | new KeyValuePair("redirect_uri", redirectUri), 43 | new KeyValuePair("client_secret", clientSecret), 44 | new KeyValuePair("scope", $"{permissionsRequested} offline_access"), 45 | new KeyValuePair("grant_type", "refresh_token") 46 | }; 47 | var aadAccessTokenRequest = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token") { Content = new FormUrlEncodedContent(formFields) }; 48 | var aadAccessTokenResponse = await client.SendAsync(aadAccessTokenRequest); 49 | dynamic result = JsonConvert.DeserializeObject(await aadAccessTokenResponse.Content.ReadAsStringAsync()); 50 | 51 | return (accessToken: result.access_token, refreshToken: result.refresh_token, refreshTokenExpiresIn: DateTime.Now.AddSeconds((int)result.expires_in).AddMinutes(-10)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/Extensions/CardExtensions.cs: -------------------------------------------------------------------------------- 1 | using JamesMann.BotFramework.Middleware; 2 | using Microsoft.Bot.Schema; 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace RoomBookingBot.Chatbot.Extensions 9 | { 10 | public static class CardExtensions 11 | { 12 | 13 | public static void AddAdaptiveCardRoomConfirmationAttachment(this Activity activity, string room, string date, string time, string attendees) 14 | { 15 | activity.Attachments = new List() { CreateAdaptiveCardRoomConfirmationAttachment(room, date, time, attendees) }; 16 | } 17 | 18 | public static void AddSignInCard(this Activity activity, string url) 19 | { 20 | activity.Attachments = new List() { CreateSignInCard(url) }; 21 | } 22 | 23 | public static void AddAdaptiveCardChoiceForm(this Activity activity, (string text, object value)[] choices) 24 | { 25 | activity.Attachments = new List { CreateChoiceAdaptiveCardAttachment(choices) }; 26 | } 27 | 28 | private static Attachment CreateChoiceAdaptiveCardAttachment((string text, object value)[] choices) 29 | { 30 | var choiceItems = new List(choices.Select(choice => new { title = choice.text, choice.value })); 31 | 32 | var serializedChoices = JsonConvert.SerializeObject(choiceItems.ToArray()); 33 | 34 | var adaptiveCard = Resource.AdaptiveCardChoiceTemplate; 35 | adaptiveCard = adaptiveCard.Replace("$(choices)", serializedChoices); 36 | 37 | return new Attachment 38 | { 39 | ContentType = "application/vnd.microsoft.card.adaptive", 40 | Content = JsonConvert.DeserializeObject(adaptiveCard) 41 | }; 42 | } 43 | 44 | 45 | private static Attachment CreateAdaptiveCardRoomConfirmationAttachment(string room, string date, string time, string attendees) 46 | { 47 | var adaptiveCard = Resource.AdaptiveCardRoomTemplate; 48 | adaptiveCard= adaptiveCard.Replace("$(room)", room); 49 | adaptiveCard= adaptiveCard.Replace("$(date)", date); 50 | adaptiveCard= adaptiveCard.Replace("$(time)", time); 51 | adaptiveCard= adaptiveCard.Replace("$(attendees)", attendees); 52 | return new Attachment() 53 | { 54 | ContentType = "application/vnd.microsoft.card.adaptive", 55 | Content = JsonConvert.DeserializeObject(adaptiveCard) 56 | }; 57 | } 58 | 59 | private static Attachment CreateSignInCard(string url) 60 | { 61 | return new SigninCard() 62 | { 63 | Text = "Please sign in with your Office 365 account to continue", 64 | Buttons = new List() 65 | { 66 | new CardAction() 67 | { 68 | Type = ActionTypes.Signin, 69 | Title = "Sign in", 70 | Value = url 71 | } 72 | } 73 | }.ToAttachment(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/Extensions/SentimentExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.CognitiveServices.Language.TextAnalytics; 2 | using Microsoft.Azure.CognitiveServices.Language.TextAnalytics.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace JamesMann.BotFramework.Middleware.Extensions 9 | { 10 | internal static class SentimentExtensions 11 | { 12 | internal static async Task Sentiment(this string text, string apiKey) 13 | { 14 | if (string.IsNullOrEmpty(text)) 15 | { 16 | return "0.0"; 17 | } 18 | 19 | // Create a client 20 | var client = new TextAnalyticsAPI(new ServiceCredentials.ApiKeyServiceClientCredentials(apiKey)); 21 | 22 | // Extract the language 23 | var result = await client.DetectLanguageAsync(new BatchInput(new List() { new Input("1", text) })); 24 | var language = result.Documents?[0].DetectedLanguages?[0].Name; 25 | 26 | // Get the sentiment 27 | var sentimentResult = await client.SentimentAsync( 28 | new MultiLanguageBatchInput( 29 | new List() 30 | { 31 | new MultiLanguageInput(language, "0", text), 32 | 33 | })); 34 | 35 | return sentimentResult.Documents?[0].Score?.ToString("#.#"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/Extensions/SpellCheckExtensions.cs: -------------------------------------------------------------------------------- 1 | using JamesMann.BotFramework.Middleware.ServiceCredentials; 2 | using Microsoft.Azure.CognitiveServices.Language.SpellCheck; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace JamesMann.BotFramework.Middleware.Extensions 7 | { 8 | internal static class SpellCheckExtensions 9 | { 10 | internal static async Task SpellCheck(this string text, string apiKey) 11 | { 12 | if (string.IsNullOrEmpty(text)) 13 | { 14 | return text; 15 | } 16 | 17 | var client = new SpellCheckClient(new ServiceCredentials.ApiKeyServiceClientCredentials(apiKey)); 18 | var spellCheckResult = await client.SpellCheckerAsync(text); 19 | 20 | foreach (var flaggedToken in spellCheckResult.FlaggedTokens) 21 | { 22 | text = text.Replace(flaggedToken.Token, flaggedToken.Suggestions.FirstOrDefault().Suggestion); 23 | } 24 | return text; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/IAuthTokenStorage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace JamesMann.BotFramework.Middleware 9 | { 10 | public interface IAuthTokenStorage 11 | { 12 | ConversationAuthToken LoadConfiguration(string id); 13 | void SaveConfiguration(ConversationAuthToken state); 14 | } 15 | 16 | public class ConversationAuthToken 17 | { 18 | public ConversationAuthToken(string id) 19 | { 20 | Id = id; 21 | } 22 | 23 | public string Id { get; set; } 24 | 25 | // Note this is stored in memory in plain text for demonstration purposes 26 | // use your common sense when applying this in your apps! i.e. take appropriate precautions 27 | public string AccessToken { get; set; } 28 | 29 | public string RefreshToken { get; set; } 30 | 31 | public DateTime ExpiresIn { get; set; } 32 | } 33 | 34 | public class InMemoryAuthTokenStorage : IAuthTokenStorage 35 | { 36 | private static readonly Dictionary InMemoryDictionary = new Dictionary(); 37 | 38 | public ConversationAuthToken LoadConfiguration(string id) 39 | { 40 | if (InMemoryDictionary.ContainsKey(id)) 41 | { 42 | return InMemoryDictionary[id]; 43 | } 44 | 45 | return null; 46 | } 47 | 48 | public void SaveConfiguration(ConversationAuthToken state) 49 | { 50 | InMemoryDictionary[state.Id] = state; 51 | } 52 | } 53 | 54 | public class DiskAuthTokenStorage : IAuthTokenStorage 55 | { 56 | //private static readonly Dictionary InMemoryDictionary = new Dictionary(); 57 | 58 | public ConversationAuthToken LoadConfiguration(string id) 59 | { 60 | id = "singletonkey"; 61 | 62 | Dictionary values = File.Exists("accessTokens.json") ? JsonConvert.DeserializeObject>(File.ReadAllText("accessTokens.json")) : new Dictionary(); 63 | 64 | if (values.ContainsKey(id)) 65 | { 66 | return values[id]; 67 | } 68 | 69 | return null; 70 | } 71 | 72 | public void SaveConfiguration(ConversationAuthToken state) 73 | { 74 | state.Id = "singletonkey"; 75 | 76 | Dictionary values = File.Exists("accessTokens.json") ? JsonConvert.DeserializeObject>(File.ReadAllText("accessTokens.json")) : new Dictionary(); 77 | 78 | values[state.Id] = state; 79 | 80 | File.WriteAllText("accessTokens.json", JsonConvert.SerializeObject(values)); 81 | } 82 | } 83 | 84 | // write azure key value token storage 85 | } 86 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/JamesMann.BotFramework.Middleware.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | True 20 | True 21 | Resource.resx 22 | 23 | 24 | 25 | 26 | 27 | ResXFileCodeGenerator 28 | Resource.Designer.cs 29 | 30 | 31 | 32 | 33 | jamesemann;arafat 34 | A collection of Bot Framework V4 extensions and middleware. Including: Azure AD Authentication, Typing, Spell Check, Sentiment Analysis. 35 | https://github.com/jamesemann/JamesMann.BotFramework/blob/master/LICENSE 36 | https://github.com/jamesemann/JamesMann.BotFramework 37 | bot framework;cognitive services 38 | https://github.com/jamesemann/JamesMann.BotFramework 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/JamesMann.BotFramework.Middleware.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2000 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JamesMann.BotFramework.Middleware", "JamesMann.BotFramework.Middleware.csproj", "{4D0D220B-2A15-4FD1-9466-E36097751BDF}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JamesMann.BotFramework.Client", "..\JamesMann.BotFramework.Client\JamesMann.BotFramework.Client.csproj", "{14D00FFB-01B0-47A6-9432-CBD7EC5EFFA1}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JamesMann.BotFramework.Dialogs", "..\JamesMann.BotFramework.Dialogs\JamesMann.BotFramework.Dialogs.csproj", "{C19B58AC-D552-462A-A369-E5C3CD14CE1E}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {4D0D220B-2A15-4FD1-9466-E36097751BDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {4D0D220B-2A15-4FD1-9466-E36097751BDF}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {4D0D220B-2A15-4FD1-9466-E36097751BDF}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {4D0D220B-2A15-4FD1-9466-E36097751BDF}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {14D00FFB-01B0-47A6-9432-CBD7EC5EFFA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {14D00FFB-01B0-47A6-9432-CBD7EC5EFFA1}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {14D00FFB-01B0-47A6-9432-CBD7EC5EFFA1}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {14D00FFB-01B0-47A6-9432-CBD7EC5EFFA1}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {C19B58AC-D552-462A-A369-E5C3CD14CE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {C19B58AC-D552-462A-A369-E5C3CD14CE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {C19B58AC-D552-462A-A369-E5C3CD14CE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {C19B58AC-D552-462A-A369-E5C3CD14CE1E}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {C57FED53-7430-4FF8-AB73-AF96A18B3EF4} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/Resource.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace JamesMann.BotFramework.Middleware { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resource { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resource() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("JamesMann.BotFramework.Middleware.Resource", typeof(Resource).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to { 65 | /// "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 66 | /// "type": "AdaptiveCard", 67 | /// "version": "1.0", 68 | /// "body": [ 69 | /// { 70 | /// "type": "TextBlock", 71 | /// "text": "Please select your appointment and confirm" 72 | /// }, 73 | /// { 74 | /// "type": "Input.ChoiceSet", 75 | /// "id": "chosenRoom", 76 | /// "style": "expanded", 77 | /// "isMultiSelect": false, 78 | /// "value": "1", 79 | /// "choices": $(choices), 80 | /// } 81 | /// ], 82 | /// "actions": [ 83 | /// { 84 | /// "type": "Action.Submit", 85 | /// "title": "OK" 86 | /// } 87 | /// ] [rest of string was truncated]";. 88 | /// 89 | internal static string AdaptiveCardChoiceTemplate { 90 | get { 91 | return ResourceManager.GetString("AdaptiveCardChoiceTemplate", resourceCulture); 92 | } 93 | } 94 | 95 | /// 96 | /// Looks up a localized string similar to { 97 | /// "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 98 | /// "type": "AdaptiveCard", 99 | /// "version": "1.0", 100 | /// "body": [ 101 | /// { 102 | /// "type": "Container", 103 | /// "items": [ 104 | /// { 105 | /// "type": "TextBlock", 106 | /// "text": "Appointment Confirmation", 107 | /// "weight": "bolder", 108 | /// "size": "medium" 109 | /// }, 110 | /// { 111 | /// "type": "ColumnSet", 112 | /// "columns": [ 113 | /// 114 | /// { 115 | /// "type": "Column", 116 | /// "width": "stretch", 117 | /// "items": [ 118 | /// [rest of string was truncated]";. 119 | /// 120 | internal static string AdaptiveCardRoomTemplate { 121 | get { 122 | return ResourceManager.GetString("AdaptiveCardRoomTemplate", resourceCulture); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/Resource.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | { 122 | "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 123 | "type": "AdaptiveCard", 124 | "version": "1.0", 125 | "body": [ 126 | { 127 | "type": "TextBlock", 128 | "text": "Please select your appointment and confirm" 129 | }, 130 | { 131 | "type": "Input.ChoiceSet", 132 | "id": "chosenRoom", 133 | "style": "expanded", 134 | "isMultiSelect": false, 135 | "value": "1", 136 | "choices": $(choices), 137 | } 138 | ], 139 | "actions": [ 140 | { 141 | "type": "Action.Submit", 142 | "title": "OK" 143 | } 144 | ] 145 | } 146 | 147 | 148 | { 149 | "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 150 | "type": "AdaptiveCard", 151 | "version": "1.0", 152 | "body": [ 153 | { 154 | "type": "Container", 155 | "items": [ 156 | { 157 | "type": "TextBlock", 158 | "text": "Appointment Confirmation", 159 | "weight": "bolder", 160 | "size": "medium" 161 | }, 162 | { 163 | "type": "ColumnSet", 164 | "columns": [ 165 | 166 | { 167 | "type": "Column", 168 | "width": "stretch", 169 | "items": [ 170 | { 171 | "type": "TextBlock", 172 | "text": "$(room)", 173 | "weight": "bolder", 174 | "wrap": true 175 | } 176 | ] 177 | } 178 | ] 179 | } 180 | ] 181 | }, 182 | { 183 | "type": "Container", 184 | "items": [ 185 | { 186 | "type": "TextBlock", 187 | "text": "Your meeting space is now reserved!", 188 | "wrap": true 189 | }, 190 | { 191 | "type": "FactSet", 192 | "facts": [ 193 | { 194 | "title": "From:", 195 | "value": "$(date)" 196 | }, 197 | { 198 | "title": "To:", 199 | "value": "$(time)" 200 | } 201 | ] 202 | } 203 | ] 204 | } 205 | ], 206 | "actions": [ 207 | { 208 | "type": "Action.OpenUrl", 209 | "title": "View Appointment", 210 | "url": "$(attendees)" 211 | } 212 | ] 213 | } 214 | 215 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/SentimentMiddleware.cs: -------------------------------------------------------------------------------- 1 | using JamesMann.BotFramework.Middleware.Extensions; 2 | using Microsoft.Bot.Builder; 3 | using Microsoft.Bot.Schema; 4 | using Microsoft.Extensions.Configuration; 5 | using System.Threading.Tasks; 6 | 7 | namespace JamesMann.BotFramework.Middleware 8 | { 9 | public class SentimentMiddleware : IMiddleware 10 | { 11 | public SentimentMiddleware(IConfiguration configuration) 12 | { 13 | ApiKey = configuration.GetValue("SentimentKey"); 14 | } 15 | 16 | public string ApiKey { get; } 17 | 18 | public async Task OnTurn(ITurnContext context, MiddlewareSet.NextDelegate next) 19 | { 20 | if (context.Activity.Type is ActivityTypes.Message) 21 | { 22 | context.Services.Add(await context.Activity.Text.Sentiment(ApiKey)); 23 | } 24 | 25 | await next(); 26 | } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/ServiceCredentials/ApiKeyServiceClientCredentials.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Rest; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace JamesMann.BotFramework.Middleware.ServiceCredentials 10 | { 11 | class ApiKeyServiceClientCredentials : ServiceClientCredentials 12 | { 13 | string SubscriptionKey { get; set; } 14 | public ApiKeyServiceClientCredentials(string subscriptionKey) 15 | { 16 | SubscriptionKey = subscriptionKey; 17 | } 18 | 19 | public override Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) 20 | { 21 | request.Headers.Add("Ocp-Apim-Subscription-Key", SubscriptionKey); 22 | return base.ProcessHttpRequestAsync(request, cancellationToken); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/SpellCheckMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Bot.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | using System.Threading.Tasks; 4 | using JamesMann.BotFramework.Middleware.Extensions; 5 | 6 | namespace JamesMann.BotFramework.Middleware 7 | { 8 | public class SpellCheckMiddleware :IMiddleware 9 | { 10 | public SpellCheckMiddleware(IConfiguration configuration) 11 | { 12 | ApiKey = configuration.GetValue("SpellCheckKey"); 13 | } 14 | 15 | public string ApiKey { get; } 16 | 17 | public async Task OnTurn(ITurnContext context, MiddlewareSet.NextDelegate next) 18 | { 19 | context.Activity.Text = await context.Activity.Text.SpellCheck(ApiKey); 20 | 21 | await next(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /JamesMann.BotFramework.Middleware/TypingMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Bot.Builder; 2 | using JamesMann.BotFramework.Middleware.Extensions; 3 | using System.Threading.Tasks; 4 | 5 | namespace JamesMann.BotFramework.Middleware 6 | { 7 | public class TypingMiddleware : IMiddleware 8 | { 9 | public async Task OnTurn(ITurnContext context, MiddlewareSet.NextDelegate next) 10 | { 11 | if (context.Activity.UserHasJustJoinedConversation() || context.Activity.UserHasJustSentMessage()) 12 | { 13 | await context.Activity.DoWithTyping(async () => 14 | { 15 | await next(); 16 | }); 17 | } 18 | else 19 | { 20 | await next(); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 James Mann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JamesMann.BotFramework 2 | 3 | A collection of Bot Framework V4 extensions and middleware. I'll be migrating all the reusable stuff in existing demos to this library, and making it modular over time. In the meantime I'm using it for my video series on YouTube. Feel free to use, licensed with MIT - free to use / no liability or warranty. 4 | 5 | ## JamesMann.BotFramework.Middleware 6 | 7 | This package is available on NuGet 👉 https://www.nuget.org/packages/JamesMann.BotFramework.Middleware 8 | 9 | ### AzureAdAuthMiddleware 10 | 11 | This middleware will allow your bot to authenticate with Azure AD. 12 | 13 | It was created to support integration with Microsoft Graph but it will work with any application that uses the OAuth 2.0 authorization code flow. https://docs.microsoft.com/en-gb/azure/active-directory/develop/v2-oauth2-auth-code-flow 14 | 15 | It supports: 16 | - Request an authorization code (Client side user consent) 17 | - Request an access token using authorization code (Server side) 18 | - Request an access token using refresh token (Server side) 19 | 20 | **Note: This middleware requires you provide a class to store OAuth access/refresh tokens somewhere. I have purposefully not prescribed how to store these access tokens. If you make use of this middleware you need to provide an implementation of `IAuthTokenStorage`. This should use secure storage like Azure Key Vault. Read up on that here. https://docs.microsoft.com/en-us/azure/key-vault/quick-create-net.** 21 | 22 | #### Usage 23 | 24 | ##### Step 1 - Define an implementation of `IAuthTokenStorage` to store and retrieve tokens 25 | This is an example of an in-memory `IAuthTokenStorage`. This is to demonstrate the principle only. 26 | 27 | **💣 AGAIN, DO NOT USE THIS SAMPLE FOR PRODUCTION APPLICATIONS 💣** 28 | 29 | ``` 30 | public class InMemoryAuthTokenStorage : IAuthTokenStorage 31 | { 32 | private static readonly Dictionary InMemoryDictionary = new Dictionary(); 33 | 34 | public ConversationAuthToken LoadConfiguration(string id) 35 | { 36 | if (InMemoryDictionary.ContainsKey(id)) 37 | { 38 | return InMemoryDictionary[id]; 39 | } 40 | 41 | return null; 42 | } 43 | 44 | public void SaveConfiguration(ConversationAuthToken token) 45 | { 46 | InMemoryDictionary[state.Id] = token; 47 | } 48 | } 49 | ``` 50 | ##### Step 2 - Register the middleware 51 | 52 | To ensure that users are always authenticated, add this middleware to the start of the pipeline. 53 | 54 | In your `Startup.cs` file, register an your `IAuthTokenStorage` implementation as a singleton into the asp dotnet core ioc container. Then configure your bot type to use an instance of `AzureAdAuthMiddleware`. 55 | 56 | 57 | ``` 58 | var tokenStorage = new InMemoryAuthTokenStorage(); 59 | services.AddSingleton(tokenStorage); 60 | 61 | services.AddBot((options) => { 62 | options.CredentialProvider = new ConfigurationCredentialProvider(Configuration); 63 | 64 | options.Middleware.Add(new AzureAdAuthMiddleware(tokenStorage, Configuration)); 65 | // more middleware 66 | }); 67 | ``` 68 | 69 | Note this requires an instance of `IConfiguration` passing to it. Use the instance injected into the `Startup.cs` class. 70 | 71 | The configuration can be read from your `appsettings.json` file which needs the following keys (I've included some sample permissions- you can change these to meet your needs). 72 | ``` 73 | { 74 | "AzureAdTenant": "", 75 | "AppClientId": "", 76 | "AppRedirectUri": "https://:/redirect", 77 | "PermissionsRequested": "Calendars.ReadWrite.Shared User.ReadBasic.All People.Read", 78 | "AppClientSecret": "" 79 | } 80 | ``` 81 | 82 | 83 | ### TypingMiddleware 84 | 85 | This middleware will show a 'typing' event whenever a long running operation is occurring in your bot or other middeware components in the pipeline. 86 | 87 | This is a good visual cue to the user that your bot is doing something. 88 | 89 | #### Usage 90 | 91 | To ensure that users get appropriate feedback at all times, add this middleware to the start of the pipeline. 92 | 93 | In your `Startup.cs` file, configure your bot type to use an instance of `TypingMiddleware`: 94 | 95 | ``` 96 | services.AddBot((options) => { 97 | options.CredentialProvider = new ConfigurationCredentialProvider(Configuration); 98 | 99 | options.Middleware.Add(new TypingMiddleware()); 100 | // more middleware 101 | }); 102 | ``` 103 | 104 | 105 | ### SpellCheckMiddleware 106 | 107 | This middleware will spell check inbound text using Cognitive Services Spell Check and therefore requires a key. There is a free tier which meets my demo/PoC needs. You can get more info at https://azure.microsoft.com/en-gb/services/cognitive-services/spell-check/ 108 | 109 | The implementation is naive at the moment in that it assumes that the suggestions are correct and replaces inbound text automatically. If you have more sophisticated needs please feel free to contribute! 110 | 111 | #### Usage 112 | 113 | Typically I would place this middleware at the end of the pipeline, but it will work anywhere. 114 | 115 | 116 | ``` 117 | services.AddBot((options) => { 118 | options.CredentialProvider = new ConfigurationCredentialProvider(Configuration); 119 | 120 | // more middleware 121 | options.Middleware.Add(new SpellCheckMiddleware(Configuration)); 122 | }); 123 | ``` 124 | 125 | Note this requires an instance of `IConfiguration` passing to it. Use the instance injected into the `Startup.cs` class. 126 | 127 | The configuration can be read from your `appsettings.json` file which needs the following key 128 | 129 | ``` 130 | { 131 | "SpellCheckKey": "" 132 | } 133 | ``` 134 | 135 | ### SentimentAnalysisMiddleware 136 | 137 | This middleware will record the sentiment of each incoming text using Cognitive Services Text Analytics API and therefore requires a key. There is a free tier which meets my demo/PoC needs. You can get more info at https://azure.microsoft.com/en-us/services/cognitive-services/text-analytics/ 138 | 139 | The implementation detects the language of the text and get the sentiment of the same. Currently, nearly 17 languages are supported. Full list of supported languages can be seen at https://docs.microsoft.com/en-us/azure/cognitive-services/text-analytics/text-analytics-supported-languages 140 | 141 | This middleware can help your business enhance customer service. 142 | 143 | #### Usage 144 | 145 | Typically I would place this middleware at the end of the pipeline, but it will work anywhere. 146 | 147 | 148 | ``` 149 | services.AddBot((options) => { 150 | options.CredentialProvider = new ConfigurationCredentialProvider(Configuration); 151 | 152 | // more middleware 153 | options.Middleware.Add(new SentimentAnalysisMiddleware(Configuration)); 154 | }); 155 | ``` 156 | 157 | Note this requires an instance of `IConfiguration` passing to it. Use the instance injected into the `Startup.cs` class. 158 | 159 | The configuration can be read from your `appsettings.json` file which needs the following key 160 | 161 | ``` 162 | { 163 | "SentimentKey": "" 164 | } 165 | ``` 166 | --------------------------------------------------------------------------------