From 16115c4d99666867720c38f4f338561534a3bdff Mon Sep 17 00:00:00 2001 From: Tomi Paananen Date: Mon, 28 Aug 2017 13:27:31 +0300 Subject: [PATCH 1/7] Removed back channel message handling - moved to https://github.com/tompaana/intermediator-bot-sample --- BotMessageRouting/BotMessageRouting.csproj | 6 +- .../DataStore/LocalRoutingDataManager.cs | 28 +++--- .../MessageRouting/MessageRouterManager.cs | 97 ++----------------- .../MessageRouting/MessageRouterResult.cs | 2 +- ...ageableParty.cs => PartyWithTimestamps.cs} | 4 +- BotMessageRouting/Properties/AssemblyInfo.cs | 4 +- BotMessageRouting/Utils/MessagingUtils.cs | 4 +- BotMessageRouting/packages.config | 4 +- README.md | 1 - 9 files changed, 34 insertions(+), 116 deletions(-) rename BotMessageRouting/Models/{EngageableParty.cs => PartyWithTimestamps.cs} (92%) diff --git a/BotMessageRouting/BotMessageRouting.csproj b/BotMessageRouting/BotMessageRouting.csproj index c56ccef..d2875a2 100644 --- a/BotMessageRouting/BotMessageRouting.csproj +++ b/BotMessageRouting/BotMessageRouting.csproj @@ -70,8 +70,8 @@ $(SolutionDir)\packages\Microsoft.WindowsAzure.ConfigurationManager.3.2.3\lib\net40\Microsoft.WindowsAzure.Configuration.dll - - $(SolutionDir)\packages\WindowsAzure.Storage.8.3.0\lib\net45\Microsoft.WindowsAzure.Storage.dll + + $(SolutionDir)\packages\WindowsAzure.Storage.8.4.0\lib\net45\Microsoft.WindowsAzure.Storage.dll $(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll @@ -108,7 +108,7 @@ - + diff --git a/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs b/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs index 413381f..c49461d 100644 --- a/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs +++ b/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs @@ -131,7 +131,7 @@ public virtual bool AddParty(string serviceUrl, string channelId, ChannelAccount channelAccount, ConversationAccount conversationAccount, bool isUser = true) { - Party newParty = new EngageableParty(serviceUrl, channelId, channelAccount, conversationAccount); + Party newParty = new PartyWithTimestamps(serviceUrl, channelId, channelAccount, conversationAccount); return AddParty(newParty, isUser); } @@ -261,9 +261,9 @@ public virtual MessageRouterResult AddPendingRequest(Party party) } else { - if (party is EngageableParty) + if (party is PartyWithTimestamps) { - (party as EngageableParty).RequestMadeTime = DateTime.UtcNow; + (party as PartyWithTimestamps).RequestMadeTime = DateTime.UtcNow; } PendingRequests.Add(party); @@ -282,9 +282,9 @@ public virtual MessageRouterResult AddPendingRequest(Party party) public virtual bool RemovePendingRequest(Party party) { - if (party is EngageableParty) + if (party is PartyWithTimestamps) { - (party as EngageableParty).ResetRequestMadeTime(); + (party as PartyWithTimestamps).ResetRequestMadeTime(); } return PendingRequests.Remove(party); @@ -355,15 +355,15 @@ public virtual MessageRouterResult AddEngagementAndClearPendingRequest(Party con DateTime engagementStartedTime = DateTime.UtcNow; - if (conversationClientParty is EngageableParty) + if (conversationClientParty is PartyWithTimestamps) { - (conversationClientParty as EngageableParty).ResetRequestMadeTime(); - (conversationClientParty as EngageableParty).EngagementStartedTime = engagementStartedTime; + (conversationClientParty as PartyWithTimestamps).ResetRequestMadeTime(); + (conversationClientParty as PartyWithTimestamps).EngagementStartedTime = engagementStartedTime; } - if (conversationOwnerParty is EngageableParty) + if (conversationOwnerParty is PartyWithTimestamps) { - (conversationOwnerParty as EngageableParty).EngagementStartedTime = engagementStartedTime; + (conversationOwnerParty as PartyWithTimestamps).EngagementStartedTime = engagementStartedTime; } result.Type = MessageRouterResultType.EngagementAdded; @@ -585,14 +585,14 @@ protected virtual IList RemoveEngagements(IList conv if (EngagedParties.Remove(conversationOwnerParty)) { - if (conversationOwnerParty is EngageableParty) + if (conversationOwnerParty is PartyWithTimestamps) { - (conversationOwnerParty as EngageableParty).ResetEngagementStartedTime(); + (conversationOwnerParty as PartyWithTimestamps).ResetEngagementStartedTime(); } - if (conversationClientParty is EngageableParty) + if (conversationClientParty is PartyWithTimestamps) { - (conversationClientParty as EngageableParty).ResetEngagementStartedTime(); + (conversationClientParty as PartyWithTimestamps).ResetEngagementStartedTime(); } messageRouterResults.Add(new MessageRouterResult() diff --git a/BotMessageRouting/MessageRouting/MessageRouterManager.cs b/BotMessageRouting/MessageRouting/MessageRouterManager.cs index 41eb815..fff47ab 100644 --- a/BotMessageRouting/MessageRouting/MessageRouterManager.cs +++ b/BotMessageRouting/MessageRouting/MessageRouterManager.cs @@ -16,8 +16,6 @@ public class MessageRouterManager { // Constants public const string RejectPendingRequestIfNoAggregationChannelAppSetting = "RejectPendingRequestIfNoAggregationChannel"; - private const string DefaultBackChannelId = "backchannel"; - private const string DefaultPartyPropertyId = "conversationId"; /// /// The routing data and all the parties the bot has seen including the instances of itself. @@ -28,26 +26,6 @@ public IRoutingDataManager RoutingDataManager set; } - /// - /// The ID for back channel messages that should establish a 1:1 conversation relationship. - /// See HandleBackChannelMessage(). - /// - public string BackChannelId - { - get; - set; - } - - /// - /// The ID for finding the party details from back channel messages. - /// See HandleBackChannelMessage(). - /// - public string PartyPropertyId - { - get; - set; - } - /// /// Constructor. /// @@ -55,8 +33,6 @@ public string PartyPropertyId public MessageRouterManager(IRoutingDataManager routingDataManager) { RoutingDataManager = routingDataManager; - BackChannelId = DefaultBackChannelId; - PartyPropertyId = DefaultPartyPropertyId; } /// @@ -157,26 +133,15 @@ public async Task HandleActivityAsync( // Make sure we have the details of the sender and the receiver (bot) stored MakeSurePartiesAreTracked(activity); + + result = await HandleMessageAsync(activity, addClientNameToMessage, addOwnerNameToMessage); - // Check for back channel messages - // If agent UI is in use, conversation requests are accepted by these messages - if (HandleBackChannelMessage(activity).Type == MessageRouterResultType.EngagementAdded) - { - // A back channel message was detected and handled - result.Type = MessageRouterResultType.OK; - } - else + if (result.Type == MessageRouterResultType.NoActionTaken) { - // No command to the bot was issued so it must be an actual message then - result = await HandleMessageAsync(activity, addClientNameToMessage, addOwnerNameToMessage); - - if (result.Type == MessageRouterResultType.NoActionTaken) + // The message was not handled, because the sender is not engaged in a conversation + if (tryToInitiateEngagementIfNotEngaged) { - // The message was not handled, because the sender is not engaged in a conversation - if (tryToInitiateEngagementIfNotEngaged) - { - result = InitiateEngagement(activity); - } + result = InitiateEngagement(activity); } } @@ -324,13 +289,13 @@ await connectorClient.Conversations.CreateDirectConversationAsync( ConversationAccount directConversationAccount = new ConversationAccount(id: conversationOwnerParty.ConversationAccount.Id); - Party acceptorPartyEngaged = new EngageableParty( + Party acceptorPartyEngaged = new PartyWithTimestamps( conversationOwnerParty.ServiceUrl, conversationOwnerParty.ChannelId, conversationOwnerParty.ChannelAccount, directConversationAccount); RoutingDataManager.AddParty(acceptorPartyEngaged); RoutingDataManager.AddParty( - new EngageableParty(botParty.ServiceUrl, botParty.ChannelId, botParty.ChannelAccount, directConversationAccount), false); + new PartyWithTimestamps(botParty.ServiceUrl, botParty.ChannelId, botParty.ChannelAccount, directConversationAccount), false); result = RoutingDataManager.AddEngagementAndClearPendingRequest(acceptorPartyEngaged, conversationClientParty); result.ConversationResourceResponse = response; @@ -388,52 +353,6 @@ public List EndEngagement(Party conversationOwnerParty) return messageRouterResults; } - /// - /// Checks the given activity for back channel messages and handles them, if detected. - /// Currently the only back channel message supported is for adding engagements - /// (establishing 1:1 conversations). - /// - /// The activity to check for back channel messages. - /// The result; if the type of the result is - /// MessageRouterResultType.EngagementAdded, the operation was successful. - public MessageRouterResult HandleBackChannelMessage(Activity activity) - { - MessageRouterResult messageRouterResult = new MessageRouterResult(); - - if (activity == null || string.IsNullOrEmpty(activity.Text)) - { - messageRouterResult.Type = MessageRouterResultType.Error; - messageRouterResult.ErrorMessage = $"The given activity ({nameof(activity)}) is either null or the message is missing"; - } - else if (activity.Text.StartsWith(BackChannelId)) - { - if (activity.ChannelData == null) - { - messageRouterResult.Type = MessageRouterResultType.Error; - messageRouterResult.ErrorMessage = "No channel data"; - } - else - { - // Handle accepted request and start 1:1 conversation - string partyAsJsonString = ((JObject)activity.ChannelData)[BackChannelId][DefaultPartyPropertyId].ToString(); - Party conversationClientParty = Party.FromJsonString(partyAsJsonString); - - Party conversationOwnerParty = MessagingUtils.CreateSenderParty(activity); - - messageRouterResult = RoutingDataManager.AddEngagementAndClearPendingRequest( - conversationOwnerParty, conversationClientParty); - messageRouterResult.Activity = activity; - } - } - else - { - // No back channel message detected - messageRouterResult.Type = MessageRouterResultType.NoActionTaken; - } - - return messageRouterResult; - } - /// /// Handles the incoming message activities. For instance, if it is a message from party /// engaged in a chat, the message will be forwarded to the counterpart in whatever diff --git a/BotMessageRouting/MessageRouting/MessageRouterResult.cs b/BotMessageRouting/MessageRouting/MessageRouterResult.cs index 338fce6..6e0e443 100644 --- a/BotMessageRouting/MessageRouting/MessageRouterResult.cs +++ b/BotMessageRouting/MessageRouting/MessageRouterResult.cs @@ -1,5 +1,4 @@ using Microsoft.Bot.Connector; -using System; using Underscore.Bot.Models; namespace Underscore.Bot.MessageRouting @@ -79,6 +78,7 @@ public string ErrorMessage /// public MessageRouterResult() { + Type = MessageRouterResultType.NoActionTaken; ErrorMessage = string.Empty; } diff --git a/BotMessageRouting/Models/EngageableParty.cs b/BotMessageRouting/Models/PartyWithTimestamps.cs similarity index 92% rename from BotMessageRouting/Models/EngageableParty.cs rename to BotMessageRouting/Models/PartyWithTimestamps.cs index cf6cab0..baeef49 100644 --- a/BotMessageRouting/Models/EngageableParty.cs +++ b/BotMessageRouting/Models/PartyWithTimestamps.cs @@ -7,7 +7,7 @@ namespace Underscore.Bot.Models /// Like Party, but with timestamps to mark times for when requests were made etc. /// [Serializable] - public class EngageableParty : Party + public class PartyWithTimestamps : Party { /// /// Represents the time when a request was made. @@ -32,7 +32,7 @@ public DateTime EngagementStartedTime /// /// Constructor. /// - public EngageableParty(string serviceUrl, string channelId, + public PartyWithTimestamps(string serviceUrl, string channelId, ChannelAccount channelAccount, ConversationAccount conversationAccount) : base(serviceUrl, channelId, channelAccount, conversationAccount) { diff --git a/BotMessageRouting/Properties/AssemblyInfo.cs b/BotMessageRouting/Properties/AssemblyInfo.cs index 3077962..4bcecb7 100644 --- a/BotMessageRouting/Properties/AssemblyInfo.cs +++ b/BotMessageRouting/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.9.0.0")] -[assembly: AssemblyFileVersion("0.9.0.0")] +[assembly: AssemblyVersion("0.9.1.0")] +[assembly: AssemblyFileVersion("0.9.1.0")] diff --git a/BotMessageRouting/Utils/MessagingUtils.cs b/BotMessageRouting/Utils/MessagingUtils.cs index 08e7da1..fe2d9de 100644 --- a/BotMessageRouting/Utils/MessagingUtils.cs +++ b/BotMessageRouting/Utils/MessagingUtils.cs @@ -26,7 +26,7 @@ public static Party CreateSenderParty(IActivity activity, bool engageable = true { if (engageable) { - return new EngageableParty(activity.ServiceUrl, activity.ChannelId, activity.From, activity.Conversation); + return new PartyWithTimestamps(activity.ServiceUrl, activity.ChannelId, activity.From, activity.Conversation); } return new Party(activity.ServiceUrl, activity.ChannelId, activity.From, activity.Conversation); @@ -42,7 +42,7 @@ public static Party CreateRecipientParty(IActivity activity, bool engageable = t { if (engageable) { - return new EngageableParty(activity.ServiceUrl, activity.ChannelId, activity.Recipient, activity.Conversation); + return new PartyWithTimestamps(activity.ServiceUrl, activity.ChannelId, activity.Recipient, activity.Conversation); } return new Party(activity.ServiceUrl, activity.ChannelId, activity.Recipient, activity.Conversation); diff --git a/BotMessageRouting/packages.config b/BotMessageRouting/packages.config index 6adfc0c..7b555d3 100644 --- a/BotMessageRouting/packages.config +++ b/BotMessageRouting/packages.config @@ -5,7 +5,7 @@ - + @@ -21,5 +21,5 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 3264614..16d32f5 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,6 @@ conversation. called when a chat request is accepted. * `EndEngagement`: Ends the engagement and severs the connection between the users so that the messages are no longer relayed. -* `HandleBackChannelMessage`: Handles (hidden) messages from the agent UI component. * `HandleMessageAsync`: Handles the incoming messages: Relays the messages between engaged parties. ### Other classes ### From 3efd76ddc2921fa61f7428cfe274f4ef5d21450f Mon Sep 17 00:00:00 2001 From: Tomi Paananen Date: Mon, 28 Aug 2017 15:21:02 +0300 Subject: [PATCH 2/7] Fixed the issue with direct conversation creation. For channels (platforms) that don't return valid ConversationResourceResponse.Id values (if any), use createNewDirectConversation == false. --- .../MessageRouting/MessageRouterManager.cs | 66 +++++++++---------- BotMessageRouting/Models/Party.cs | 2 +- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/BotMessageRouting/MessageRouting/MessageRouterManager.cs b/BotMessageRouting/MessageRouting/MessageRouterManager.cs index fff47ab..b6bff9b 100644 --- a/BotMessageRouting/MessageRouting/MessageRouterManager.cs +++ b/BotMessageRouting/MessageRouting/MessageRouterManager.cs @@ -244,9 +244,13 @@ public MessageRouterResult RejectPendingRequest(Party partyToReject, Party rejec /// /// The party who owns the conversation (e.g. customer service agent). /// The other party in the conversation. + /// If true, will try to create a new direct conversation between + /// the bot and the conversation owner (e.g. agent) where the messages from the other (client) party are routed. + /// Note that this will result in the conversation owner having a new separate party in the created engagement + /// (for the new direct conversation). /// The result of the operation. public async Task AddEngagementAsync( - Party conversationOwnerParty, Party conversationClientParty) + Party conversationOwnerParty, Party conversationClientParty, bool createNewDirectConversation) { if (conversationOwnerParty == null || conversationClientParty == null) { @@ -265,52 +269,42 @@ public async Task AddEngagementAsync( if (botParty != null) { - ConnectorClient connectorClient = new ConnectorClient(new Uri(conversationOwnerParty.ServiceUrl)); - - try + if (createNewDirectConversation) { - ConversationResourceResponse response = - await connectorClient.Conversations.CreateDirectConversationAsync( - botParty.ChannelAccount, conversationOwnerParty.ChannelAccount); - - // ResponseId and conversationOwnerParty.ConversationAccount.Id are not consistent - // with each other across channels. Here we need the ConversationAccountId to route - // messages correctly across channels, e.g.: - // * In Slack they are the same: - // * response.Id: B6JJQ7939: T6HKNHCP7: D6H04L58R - // * conversationOwnerParty.ConversationAccount.Id: B6JJQ7939: T6HKNHCP7: D6H04L58R - // * In Skype they are not: - // * response.Id: 8:daltskin - // * conversationOwnerParty.ConversationAccount.Id: 29:11MZyI5R2Eak3t7bFjDwXmjQYnSl7aTBEB8zaSMDIEpA - if (response != null && !string.IsNullOrEmpty(conversationOwnerParty.ConversationAccount.Id)) + ConnectorClient connectorClient = new ConnectorClient(new Uri(conversationOwnerParty.ServiceUrl)); + ConversationResourceResponse conversationResourceResponse = null; + + try + { + conversationResourceResponse = + await connectorClient.Conversations.CreateDirectConversationAsync( + botParty.ChannelAccount, conversationOwnerParty.ChannelAccount); + } + catch (Exception) + { + // Do nothing here as we fallback (continue without creating a direct conversation) + } + + if (conversationResourceResponse != null && !string.IsNullOrEmpty(conversationResourceResponse.Id)) { // The conversation account of the conversation owner for this 1:1 chat is different - - // thus, we need to create a new party instance + // thus, we need to re-create the conversation owner instance ConversationAccount directConversationAccount = - new ConversationAccount(id: conversationOwnerParty.ConversationAccount.Id); + new ConversationAccount(id: conversationResourceResponse.Id); - Party acceptorPartyEngaged = new PartyWithTimestamps( + conversationOwnerParty = new PartyWithTimestamps( conversationOwnerParty.ServiceUrl, conversationOwnerParty.ChannelId, conversationOwnerParty.ChannelAccount, directConversationAccount); - RoutingDataManager.AddParty(acceptorPartyEngaged); - RoutingDataManager.AddParty( - new PartyWithTimestamps(botParty.ServiceUrl, botParty.ChannelId, botParty.ChannelAccount, directConversationAccount), false); + RoutingDataManager.AddParty(conversationOwnerParty); + RoutingDataManager.AddParty(new PartyWithTimestamps( + botParty.ServiceUrl, botParty.ChannelId, botParty.ChannelAccount, directConversationAccount), false); - result = RoutingDataManager.AddEngagementAndClearPendingRequest(acceptorPartyEngaged, conversationClientParty); - result.ConversationResourceResponse = response; - } - else - { - result.Type = MessageRouterResultType.Error; - result.ErrorMessage = "Failed to create a direct conversation"; + result.ConversationResourceResponse = conversationResourceResponse; } } - catch (Exception e) - { - result.Type = MessageRouterResultType.Error; - result.ErrorMessage = $"Failed to create a direct conversation: {e.Message}"; - } + + result = RoutingDataManager.AddEngagementAndClearPendingRequest(conversationOwnerParty, conversationClientParty); } else { diff --git a/BotMessageRouting/Models/Party.cs b/BotMessageRouting/Models/Party.cs index 42557da..28b8e95 100644 --- a/BotMessageRouting/Models/Party.cs +++ b/BotMessageRouting/Models/Party.cs @@ -28,7 +28,7 @@ public string ChannelId } /// - /// Conversation account - represents a specific user. + /// Channel account - represents a specific user. /// /// Can be null and if so this party is considered to cover (everyone in the given) /// channel/conversation. From aaf121cedc363e21c51fc81337ed4fda69458db0 Mon Sep 17 00:00:00 2001 From: Tomi Paananen Date: Tue, 29 Aug 2017 15:59:03 +0300 Subject: [PATCH 3/7] * Terminology change: Engagement -> Connection * Updated README.md --- .../AzureTableStorageRoutingDataManager.cs | 12 +- .../DataStore/IRoutingDataManager.cs | 47 ++++--- .../DataStore/LocalRoutingDataManager.cs | 118 +++++++++--------- .../MessageRouting/MessageRouterManager.cs | 68 +++++----- .../MessageRouting/MessageRouterResult.cs | 18 +-- BotMessageRouting/Models/Party.cs | 6 +- .../Models/PartyWithTimestamps.cs | 20 +-- BotMessageRouting/Properties/AssemblyInfo.cs | 4 +- BotMessageRouting/Utils/MessagingUtils.cs | 15 +-- README.md | 42 ++++--- 10 files changed, 183 insertions(+), 167 deletions(-) diff --git a/BotMessageRouting/MessageRouting/DataStore/AzureTableStorageRoutingDataManager.cs b/BotMessageRouting/MessageRouting/DataStore/AzureTableStorageRoutingDataManager.cs index 41eaa1c..4dccabe 100644 --- a/BotMessageRouting/MessageRouting/DataStore/AzureTableStorageRoutingDataManager.cs +++ b/BotMessageRouting/MessageRouting/DataStore/AzureTableStorageRoutingDataManager.cs @@ -36,7 +36,7 @@ public bool AddAggregationParty(Party party) throw new NotImplementedException(); } - public MessageRouterResult AddEngagementAndClearPendingRequest(Party conversationOwnerParty, Party conversationClientParty) + public MessageRouterResult ConnectAndClearPendingRequest(Party conversationOwnerParty, Party conversationClientParty) { throw new NotImplementedException(); } @@ -61,7 +61,7 @@ public void DeleteAll() throw new NotImplementedException(); } - public string EngagementsAsString() + public string ConnectionsToString() { throw new NotImplementedException(); } @@ -71,7 +71,7 @@ public Party FindBotPartyByChannelAndConversation(string channelId, Conversation throw new NotImplementedException(); } - public Party FindEngagedPartyByChannel(string channelId, ChannelAccount channelAccount) + public Party FindConnectedPartyByChannel(string channelId, ChannelAccount channelAccount) { throw new NotImplementedException(); } @@ -101,7 +101,7 @@ public IList GetBotParties() throw new NotImplementedException(); } - public Party GetEngagedCounterpart(Party partyWhoseCounterpartToFind) + public Party GetConnectedCounterpart(Party partyWhoseCounterpartToFind) { throw new NotImplementedException(); } @@ -121,7 +121,7 @@ public bool IsAssociatedWithAggregation(Party party) throw new NotImplementedException(); } - public bool IsEngaged(Party party, EngagementProfile engagementProfile) + public bool IsConnected(Party party, ConnectionProfile connectionProfile) { throw new NotImplementedException(); } @@ -131,7 +131,7 @@ public bool RemoveAggregationParty(Party party) throw new NotImplementedException(); } - public IList RemoveEngagement(Party party, EngagementProfile engagementProfile) + public IList Disconnect(Party party, ConnectionProfile connectionProfile) { throw new NotImplementedException(); } diff --git a/BotMessageRouting/MessageRouting/DataStore/IRoutingDataManager.cs b/BotMessageRouting/MessageRouting/DataStore/IRoutingDataManager.cs index 64864e6..f83a6da 100644 --- a/BotMessageRouting/MessageRouting/DataStore/IRoutingDataManager.cs +++ b/BotMessageRouting/MessageRouting/DataStore/IRoutingDataManager.cs @@ -1,18 +1,17 @@ using Microsoft.Bot.Connector; -using System; using System.Collections.Generic; using Underscore.Bot.Models; namespace Underscore.Bot.MessageRouting.DataStore { /// - /// Defines the type of engagement: - /// - None: No engagement + /// Defines the type of the connection: + /// - None: No connection /// - Client: E.g. a customer /// - Owner: E.g. a customer service agent /// - Any: Either a client or an owner /// - public enum EngagementProfile + public enum ConnectionProfile { None, Client, @@ -37,7 +36,7 @@ public interface IRoutingDataManager /// /// The new party to add. /// If true, will try to add the party to the list of users. - /// If false, will try to add it to the list of bot identities. + /// If false, will try to add it to the list of bot identities. True by default. /// True, if the given party was added. False otherwise (was null or already stored). bool AddParty(Party newParty, bool isUser = true); @@ -98,38 +97,38 @@ bool AddParty(string serviceUrl, string channelId, bool RemovePendingRequest(Party party); /// - /// Checks if the given party is engaged in a 1:1 conversation as defined by the engagement - /// profile (e.g. as a customer, as an agent or either one). + /// Checks if the given party is connected in a 1:1 conversation as defined by + /// the connection profile (e.g. as a customer, as an agent or either one). /// /// The party to check. - /// Defines whether to look for clients, owners or both. - /// True, if the party is engaged as defined by the given engagement profile. + /// Defines whether to look for clients, owners or both. + /// True, if the party is connected as defined by the given connection profile. /// False otherwise. - bool IsEngaged(Party party, EngagementProfile engagementProfile); + bool IsConnected(Party party, ConnectionProfile connectionProfile); /// /// Resolves the given party's counterpart in a 1:1 conversation. /// /// The party whose counterpart to resolve. /// The counterpart or null, if not found. - Party GetEngagedCounterpart(Party partyWhoseCounterpartToFind); + Party GetConnectedCounterpart(Party partyWhoseCounterpartToFind); /// - /// Creates a new engagement between the given parties. The method also clears the pending + /// Creates a new connection between the given parties. The method also clears the pending /// request of the client party, if one exists. /// /// The conversation owner party. /// The conversation client (customer) party. - /// The result of the operation. The expected result type, when successful, is EngagementAdded. - MessageRouterResult AddEngagementAndClearPendingRequest(Party conversationOwnerParty, Party conversationClientParty); + /// The result of the operation. The expected result type, when successful, is Connected. + MessageRouterResult ConnectAndClearPendingRequest(Party conversationOwnerParty, Party conversationClientParty); /// - /// Removes an engagement(s) of the given party i.e. ends the 1:1 conversations. + /// Removes connection(s) of the given party i.e. ends the 1:1 conversations. /// - /// The party whose engagements to remove. - /// The engagement profile of the party (owner/client/either). - /// A list of operation results. The expected result types, when successful, are EngagementRemoved. - IList RemoveEngagement(Party party, EngagementProfile engagementProfile); + /// The party whose connections to remove. + /// The connection profile of the party (owner/client/either). + /// A list of operation results. The expected result types, when successful, are Disconnected. + IList Disconnect(Party party, ConnectionProfile connectionProfile); /// /// Deletes all existing routing data permanently. @@ -180,12 +179,12 @@ bool AddParty(string serviceUrl, string channelId, Party FindBotPartyByChannelAndConversation(string channelId, ConversationAccount conversationAccount); /// - /// Tries to find a party engaged in a conversation. + /// Tries to find a party connected in a conversation. /// /// The channel ID. /// The channel account. /// The party matching the given details or null if not found. - Party FindEngagedPartyByChannel(string channelId, ChannelAccount channelAccount); + Party FindConnectedPartyByChannel(string channelId, ChannelAccount channelAccount); /// /// Finds the parties from the given list that match the channel account (and ID) of the given party. @@ -198,9 +197,9 @@ bool AddParty(string serviceUrl, string channelId, #region Methods for debugging #if DEBUG - /// The engagements (parties in conversation) as a string. - /// Will return an empty string, if no engagements exist. - string EngagementsAsString(); + /// The connections (parties in conversation) as a string. + /// Will return an empty string, if no connections exist. + string ConnectionsToString(); string GetLastMessageRouterResults(); diff --git a/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs b/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs index c49461d..112df2a 100644 --- a/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs +++ b/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs @@ -58,11 +58,11 @@ protected List PendingRequests } /// - /// Contains 1:1 associations between parties i.e. parties engaged in a conversation. + /// Contains 1:1 associations between parties i.e. parties connected in a conversation. /// Furthermore, the key party is considered to be the conversation owner e.g. in /// a customer service situation the customer service agent. /// - protected Dictionary EngagedParties + protected Dictionary ConnectedParties { get; set; @@ -85,7 +85,7 @@ public LocalRoutingDataManager() UserParties = new List(); BotParties = new List(); PendingRequests = new List(); - EngagedParties = new Dictionary(); + ConnectedParties = new Dictionary(); #if DEBUG LastMessageRouterResults = new List(); #endif @@ -174,7 +174,7 @@ public virtual IList RemoveParty(Party partyToRemove) messageRouterResults.Add(new MessageRouterResult() { - Type = MessageRouterResultType.EngagementRejected, + Type = MessageRouterResultType.ConnectionRejected, ConversationClientParty = pendingRequestToRemove }); } @@ -182,10 +182,10 @@ public virtual IList RemoveParty(Party partyToRemove) if (wasRemoved) { - // Check if the party exists in EngagedParties + // Check if the party exists in ConnectedParties List keys = new List(); - foreach (var partyPair in EngagedParties) + foreach (var partyPair in ConnectedParties) { if (partyPair.Key.HasMatchingChannelInformation(partyToRemove) || partyPair.Value.HasMatchingChannelInformation(partyToRemove)) @@ -196,7 +196,7 @@ public virtual IList RemoveParty(Party partyToRemove) foreach (Party key in keys) { - messageRouterResults.AddRange(RemoveEngagement(key, EngagementProfile.Owner)); + messageRouterResults.AddRange(Disconnect(key, ConnectionProfile.Owner)); } } @@ -250,7 +250,7 @@ public virtual MessageRouterResult AddPendingRequest(Party party) { if (PendingRequests.Contains(party)) { - result.Type = MessageRouterResultType.EngagementAlreadyInitiated; + result.Type = MessageRouterResultType.TryingToConnect; } else { @@ -263,11 +263,11 @@ public virtual MessageRouterResult AddPendingRequest(Party party) { if (party is PartyWithTimestamps) { - (party as PartyWithTimestamps).RequestMadeTime = DateTime.UtcNow; + (party as PartyWithTimestamps).ConnectionRequestTime = DateTime.UtcNow; } PendingRequests.Add(party); - result.Type = MessageRouterResultType.EngagementInitiated; + result.Type = MessageRouterResultType.Connecting; } } } @@ -284,61 +284,61 @@ public virtual bool RemovePendingRequest(Party party) { if (party is PartyWithTimestamps) { - (party as PartyWithTimestamps).ResetRequestMadeTime(); + (party as PartyWithTimestamps).ResetConnectionRequestTime(); } return PendingRequests.Remove(party); } - public virtual bool IsEngaged(Party party, EngagementProfile engagementProfile) + public virtual bool IsConnected(Party party, ConnectionProfile connectionProfile) { - bool isEngaged = false; + bool isConnected = false; if (party != null) { - switch (engagementProfile) + switch (connectionProfile) { - case EngagementProfile.Client: - isEngaged = EngagedParties.Values.Contains(party); + case ConnectionProfile.Client: + isConnected = ConnectedParties.Values.Contains(party); break; - case EngagementProfile.Owner: - isEngaged = EngagedParties.Keys.Contains(party); + case ConnectionProfile.Owner: + isConnected = ConnectedParties.Keys.Contains(party); break; - case EngagementProfile.Any: - isEngaged = (EngagedParties.Values.Contains(party) || EngagedParties.Keys.Contains(party)); + case ConnectionProfile.Any: + isConnected = (ConnectedParties.Values.Contains(party) || ConnectedParties.Keys.Contains(party)); break; default: break; } } - return isEngaged; + return isConnected; } - public virtual Party GetEngagedCounterpart(Party partyWhoseCounterpartToFind) + public virtual Party GetConnectedCounterpart(Party partyWhoseCounterpartToFind) { Party counterparty = null; - if (IsEngaged(partyWhoseCounterpartToFind, EngagementProfile.Client)) + if (IsConnected(partyWhoseCounterpartToFind, ConnectionProfile.Client)) { - for (int i = 0; i < EngagedParties.Count; ++i) + for (int i = 0; i < ConnectedParties.Count; ++i) { - if (EngagedParties.Values.ElementAt(i).Equals(partyWhoseCounterpartToFind)) + if (ConnectedParties.Values.ElementAt(i).Equals(partyWhoseCounterpartToFind)) { - counterparty = EngagedParties.Keys.ElementAt(i); + counterparty = ConnectedParties.Keys.ElementAt(i); break; } } } - else if (IsEngaged(partyWhoseCounterpartToFind, EngagementProfile.Owner)) + else if (IsConnected(partyWhoseCounterpartToFind, ConnectionProfile.Owner)) { - EngagedParties.TryGetValue(partyWhoseCounterpartToFind, out counterparty); + ConnectedParties.TryGetValue(partyWhoseCounterpartToFind, out counterparty); } return counterparty; } - public virtual MessageRouterResult AddEngagementAndClearPendingRequest(Party conversationOwnerParty, Party conversationClientParty) + public virtual MessageRouterResult ConnectAndClearPendingRequest(Party conversationOwnerParty, Party conversationClientParty) { MessageRouterResult result = new MessageRouterResult() { @@ -350,29 +350,29 @@ public virtual MessageRouterResult AddEngagementAndClearPendingRequest(Party con { try { - EngagedParties.Add(conversationOwnerParty, conversationClientParty); + ConnectedParties.Add(conversationOwnerParty, conversationClientParty); PendingRequests.Remove(conversationClientParty); - DateTime engagementStartedTime = DateTime.UtcNow; + DateTime connectionStartedTime = DateTime.UtcNow; if (conversationClientParty is PartyWithTimestamps) { - (conversationClientParty as PartyWithTimestamps).ResetRequestMadeTime(); - (conversationClientParty as PartyWithTimestamps).EngagementStartedTime = engagementStartedTime; + (conversationClientParty as PartyWithTimestamps).ResetConnectionRequestTime(); + (conversationClientParty as PartyWithTimestamps).ConnectionEstablishedTime = connectionStartedTime; } if (conversationOwnerParty is PartyWithTimestamps) { - (conversationOwnerParty as PartyWithTimestamps).EngagementStartedTime = engagementStartedTime; + (conversationOwnerParty as PartyWithTimestamps).ConnectionEstablishedTime = connectionStartedTime; } - result.Type = MessageRouterResultType.EngagementAdded; + result.Type = MessageRouterResultType.Connected; } catch (ArgumentException e) { result.Type = MessageRouterResultType.Error; result.ErrorMessage = e.Message; - System.Diagnostics.Debug.WriteLine($"Failed to add engagement between parties {conversationOwnerParty} and {conversationClientParty}: {e.Message}"); + System.Diagnostics.Debug.WriteLine($"Failed to connect parties {conversationOwnerParty} and {conversationClientParty}: {e.Message}"); } } else @@ -384,7 +384,7 @@ public virtual MessageRouterResult AddEngagementAndClearPendingRequest(Party con return result; } - public virtual IList RemoveEngagement(Party party, EngagementProfile engagementProfile) + public virtual IList Disconnect(Party party, ConnectionProfile connectionProfile) { IList messageRouterResults = new List(); @@ -392,19 +392,19 @@ public virtual IList RemoveEngagement(Party party, Engageme { List keysToRemove = new List(); - foreach (var partyPair in EngagedParties) + foreach (var partyPair in ConnectedParties) { bool removeThisPair = false; - switch (engagementProfile) + switch (connectionProfile) { - case EngagementProfile.Client: + case ConnectionProfile.Client: removeThisPair = partyPair.Value.Equals(party); break; - case EngagementProfile.Owner: + case ConnectionProfile.Owner: removeThisPair = partyPair.Key.Equals(party); break; - case EngagementProfile.Any: + case ConnectionProfile.Any: removeThisPair = (partyPair.Value.Equals(party) || partyPair.Key.Equals(party)); break; default: @@ -415,7 +415,7 @@ public virtual IList RemoveEngagement(Party party, Engageme { keysToRemove.Add(partyPair.Key); - if (engagementProfile == EngagementProfile.Owner) + if (connectionProfile == ConnectionProfile.Owner) { // Since owner is the key in the dictionary, there can be only one break; @@ -423,7 +423,7 @@ public virtual IList RemoveEngagement(Party party, Engageme } } - messageRouterResults = RemoveEngagements(keysToRemove); + messageRouterResults = RemoveConnections(keysToRemove); } return messageRouterResults; @@ -435,7 +435,7 @@ public virtual void DeleteAll() UserParties.Clear(); BotParties.Clear(); PendingRequests.Clear(); - EngagedParties.Clear(); + ConnectedParties.Clear(); #if DEBUG LastMessageRouterResults.Clear(); #endif @@ -519,13 +519,13 @@ public virtual Party FindBotPartyByChannelAndConversation(string channelId, Conv return botParty; } - public virtual Party FindEngagedPartyByChannel(string channelId, ChannelAccount channelAccount) + public virtual Party FindConnectedPartyByChannel(string channelId, ChannelAccount channelAccount) { Party foundParty = null; try { - foundParty = EngagedParties.Keys.Single(party => + foundParty = ConnectedParties.Keys.Single(party => (party.ChannelId.Equals(channelId) && party.ChannelAccount != null && party.ChannelAccount.Id.Equals(channelAccount.Id))); @@ -533,7 +533,7 @@ public virtual Party FindEngagedPartyByChannel(string channelId, ChannelAccount if (foundParty == null) { // Not found in keys, try the values - foundParty = EngagedParties.Values.First(party => + foundParty = ConnectedParties.Values.First(party => (party.ChannelId.Equals(channelId) && party.ChannelAccount != null && party.ChannelAccount.Id.Equals(channelAccount.Id))); @@ -571,33 +571,33 @@ public virtual IList FindPartiesWithMatchingChannelAccount(Party partyToF } /// - /// Removes the engagements of the given conversation owners. + /// Removes the connections of the given conversation owners. /// - /// The conversation owners whose engagements to remove. - /// The number of engagements removed. - protected virtual IList RemoveEngagements(IList conversationOwnerParties) + /// The conversation owners whose connections to remove. + /// The number of connections removed. + protected virtual IList RemoveConnections(IList conversationOwnerParties) { IList messageRouterResults = new List(); foreach (Party conversationOwnerParty in conversationOwnerParties) { - EngagedParties.TryGetValue(conversationOwnerParty, out Party conversationClientParty); + ConnectedParties.TryGetValue(conversationOwnerParty, out Party conversationClientParty); - if (EngagedParties.Remove(conversationOwnerParty)) + if (ConnectedParties.Remove(conversationOwnerParty)) { if (conversationOwnerParty is PartyWithTimestamps) { - (conversationOwnerParty as PartyWithTimestamps).ResetEngagementStartedTime(); + (conversationOwnerParty as PartyWithTimestamps).ResetConnectionEstablishedTime(); } if (conversationClientParty is PartyWithTimestamps) { - (conversationClientParty as PartyWithTimestamps).ResetEngagementStartedTime(); + (conversationClientParty as PartyWithTimestamps).ResetConnectionEstablishedTime(); } messageRouterResults.Add(new MessageRouterResult() { - Type = MessageRouterResultType.EngagementRemoved, + Type = MessageRouterResultType.Disconnected, ConversationOwnerParty = conversationOwnerParty, ConversationClientParty = conversationClientParty }); @@ -608,11 +608,11 @@ protected virtual IList RemoveEngagements(IList conv } #if DEBUG - public string EngagementsAsString() + public string ConnectionsToString() { string parties = string.Empty; - foreach (KeyValuePair keyValuePair in EngagedParties) + foreach (KeyValuePair keyValuePair in ConnectedParties) { parties += $"{keyValuePair.Key} -> {keyValuePair.Value}\n\r"; } diff --git a/BotMessageRouting/MessageRouting/MessageRouterManager.cs b/BotMessageRouting/MessageRouting/MessageRouterManager.cs index b6bff9b..5a4d827 100644 --- a/BotMessageRouting/MessageRouting/MessageRouterManager.cs +++ b/BotMessageRouting/MessageRouting/MessageRouterManager.cs @@ -1,5 +1,4 @@ using Microsoft.Bot.Connector; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -42,7 +41,8 @@ public MessageRouterManager(IRoutingDataManager routingDataManager) /// The party to send the message to. /// The message activity to send (message content). /// The ResourceResponse instance or null in case of an error. - public async Task SendMessageToPartyByBotAsync(Party partyToMessage, IMessageActivity messageActivity) + public async Task SendMessageToPartyByBotAsync( + Party partyToMessage, IMessageActivity messageActivity) { Party botParty = null; @@ -117,13 +117,13 @@ public async Task BroadcastMessageToAggregationChannelsAsync(string messageText) /// Handles the new activity. /// /// The activity to handle. - /// If true, will try to initiate - /// the engagement (1:1 conversation) automatically, if the sender is not engaged already. + /// If true, will try to initiate + /// the connection (1:1 conversation) automatically, if the sender is not connected already. /// If true, will add the client's name to the beginning of the message. /// If true, will add the owner's (agent) name to the beginning of the message. /// The result of the operation. public async Task HandleActivityAsync( - Activity activity, bool tryToInitiateEngagementIfNotEngaged, + Activity activity, bool tryToRequestConnectionIfNotConnected, bool addClientNameToMessage = true, bool addOwnerNameToMessage = false) { MessageRouterResult result = new MessageRouterResult() @@ -138,10 +138,10 @@ public async Task HandleActivityAsync( if (result.Type == MessageRouterResultType.NoActionTaken) { - // The message was not handled, because the sender is not engaged in a conversation - if (tryToInitiateEngagementIfNotEngaged) + // The message was not handled, because the sender is not connected in a conversation + if (tryToRequestConnectionIfNotConnected) { - result = InitiateEngagement(activity); + result = RequestConnection(activity); } } @@ -170,8 +170,8 @@ public void MakeSurePartiesAreTracked(Party senderParty, Party recipientParty) } /// - /// Checks the given activity for new parties and adds them to the collection, if not - /// already there. + /// Checks the given activity for new parties and adds them to the collection, + /// if not already there. /// /// The activity. public void MakeSurePartiesAreTracked(IActivity activity) @@ -193,12 +193,13 @@ public IList RemoveParty(Party partyToRemove) } /// - /// Tries to initiates the engagement by creating a request on behalf of the sender in the - /// given activity. This method does nothing, if a request for the same user already exists. + /// Tries to initiate a connection (1:1 conversation) by creating a request on behalf of + /// the sender in the given activity. This method does nothing, if a request for the same + /// user already exists. /// /// The activity. /// The result of the operation. - public MessageRouterResult InitiateEngagement(Activity activity) + public MessageRouterResult RequestConnection(Activity activity) { MessageRouterResult messageRouterResult = RoutingDataManager.AddPendingRequest(MessagingUtils.CreateSenderParty(activity)); @@ -207,7 +208,7 @@ public MessageRouterResult InitiateEngagement(Activity activity) } /// - /// Tries to reject the pending engagement request of the given party. + /// Tries to reject the pending connection request of the given party. /// /// The party whose request to reject. /// The party rejecting the request (optional). @@ -227,7 +228,7 @@ public MessageRouterResult RejectPendingRequest(Party partyToReject, Party rejec if (RoutingDataManager.RemovePendingRequest(partyToReject)) { - result.Type = MessageRouterResultType.EngagementRejected; + result.Type = MessageRouterResultType.ConnectionRejected; } else { @@ -240,16 +241,18 @@ public MessageRouterResult RejectPendingRequest(Party partyToReject, Party rejec /// /// Tries to establish 1:1 chat between the two given parties. - /// Note that the conversation owner will have a new separate party in the created engagement. + /// + /// Note that the conversation owner will have a new separate party in the created + /// conversation, if a new direct conversation is created. /// /// The party who owns the conversation (e.g. customer service agent). /// The other party in the conversation. /// If true, will try to create a new direct conversation between /// the bot and the conversation owner (e.g. agent) where the messages from the other (client) party are routed. - /// Note that this will result in the conversation owner having a new separate party in the created engagement + /// Note that this will result in the conversation owner having a new separate party in the created connection /// (for the new direct conversation). /// The result of the operation. - public async Task AddEngagementAsync( + public async Task ConnectAsync( Party conversationOwnerParty, Party conversationClientParty, bool createNewDirectConversation) { if (conversationOwnerParty == null || conversationClientParty == null) @@ -304,7 +307,7 @@ await connectorClient.Conversations.CreateDirectConversationAsync( } } - result = RoutingDataManager.AddEngagementAndClearPendingRequest(conversationOwnerParty, conversationClientParty); + result = RoutingDataManager.ConnectAndClearPendingRequest(conversationOwnerParty, conversationClientParty); } else { @@ -316,23 +319,24 @@ await connectorClient.Conversations.CreateDirectConversationAsync( } /// - /// Ends the engagement where the given party is the conversation owner + /// Ends the 1:1 conversation where the given party is the conversation owner /// (e.g. a customer service agent). /// - /// The owner of the engagement (conversation). - /// The results. If the number of results is more than 0, the operation was successful. - public List EndEngagement(Party conversationOwnerParty) + /// The owner of the connection (conversation). + /// The results. + public List Disconnect(Party conversationOwnerParty) { List messageRouterResults = new List(); - Party ownerInConversation = RoutingDataManager.FindEngagedPartyByChannel( + Party ownerInConversation = RoutingDataManager.FindConnectedPartyByChannel( conversationOwnerParty.ChannelId, conversationOwnerParty.ChannelAccount); - if (ownerInConversation != null && RoutingDataManager.IsEngaged(ownerInConversation, EngagementProfile.Owner)) + if (ownerInConversation != null + && RoutingDataManager.IsConnected(ownerInConversation, ConnectionProfile.Owner)) { - Party otherParty = RoutingDataManager.GetEngagedCounterpart(ownerInConversation); + Party otherParty = RoutingDataManager.GetConnectedCounterpart(ownerInConversation); messageRouterResults.AddRange( - RoutingDataManager.RemoveEngagement(ownerInConversation, EngagementProfile.Owner)); + RoutingDataManager.Disconnect(ownerInConversation, ConnectionProfile.Owner)); } else { @@ -349,7 +353,7 @@ public List EndEngagement(Party conversationOwnerParty) /// /// Handles the incoming message activities. For instance, if it is a message from party - /// engaged in a chat, the message will be forwarded to the counterpart in whatever + /// connected in a 1:1 chat, the message will be forwarded to the counterpart in whatever /// channel that party is on. /// /// The activity to handle. @@ -367,11 +371,11 @@ public async Task HandleMessageAsync( Party senderParty = MessagingUtils.CreateSenderParty(activity); - if (RoutingDataManager.IsEngaged(senderParty, EngagementProfile.Owner)) + if (RoutingDataManager.IsConnected(senderParty, ConnectionProfile.Owner)) { // Sender is an owner of an ongoing conversation - forward the message result.ConversationOwnerParty = senderParty; - Party partyToForwardMessageTo = RoutingDataManager.GetEngagedCounterpart(senderParty); + Party partyToForwardMessageTo = RoutingDataManager.GetConnectedCounterpart(senderParty); if (partyToForwardMessageTo != null) { @@ -397,11 +401,11 @@ public async Task HandleMessageAsync( result.ErrorMessage = "Failed to find the party to forward the message to"; } } - else if (RoutingDataManager.IsEngaged(senderParty, EngagementProfile.Client)) + else if (RoutingDataManager.IsConnected(senderParty, ConnectionProfile.Client)) { // Sender is a participant of an ongoing conversation - forward the message result.ConversationClientParty = senderParty; - Party partyToForwardMessageTo = RoutingDataManager.GetEngagedCounterpart(senderParty); + Party partyToForwardMessageTo = RoutingDataManager.GetConnectedCounterpart(senderParty); if (partyToForwardMessageTo != null) { diff --git a/BotMessageRouting/MessageRouting/MessageRouterResult.cs b/BotMessageRouting/MessageRouting/MessageRouterResult.cs index 6e0e443..7573127 100644 --- a/BotMessageRouting/MessageRouting/MessageRouterResult.cs +++ b/BotMessageRouting/MessageRouting/MessageRouterResult.cs @@ -5,13 +5,13 @@ namespace Underscore.Bot.MessageRouting { public enum MessageRouterResultType { - NoActionTaken, // The result handler should ignore results with this type - OK, // The result handler should ignore results with this type - EngagementInitiated, - EngagementAlreadyInitiated, - EngagementRejected, - EngagementAdded, - EngagementRemoved, + NoActionTaken, // No action taken - The result handler should ignore results with this type + OK, // Action taken, but the result handler should ignore results with this type + Connecting, + TryingToConnect, // Connection attempt already initiated + ConnectionRejected, + Connected, + Disconnected, NoAgentsAvailable, NoAggregationChannel, FailedToForwardMessage, @@ -46,8 +46,8 @@ public Activity Activity /// /// A valid ConversationResourceResponse of the newly created direct conversation /// (between the bot [who will relay messages] and the conversation owner), - /// if the engagement was added and a conversation created successfully - /// (MessageRouterResultType is EngagementAdded). + /// if the connection was added and a conversation created successfully + /// (MessageRouterResultType is Connected). /// public ConversationResourceResponse ConversationResourceResponse { diff --git a/BotMessageRouting/Models/Party.cs b/BotMessageRouting/Models/Party.cs index 28b8e95..64b624a 100644 --- a/BotMessageRouting/Models/Party.cs +++ b/BotMessageRouting/Models/Party.cs @@ -137,9 +137,9 @@ public override int GetHashCode() public override string ToString() { - return "[" + ServiceUrl + "; " + ChannelId + "; " - + ((ChannelAccount == null) ? "(no specific user); " : ("{" + ChannelAccount.Id + "; " + ChannelAccount.Name + "}; ")) - + "{" + ConversationAccount.Id + "; " + ConversationAccount.Name + "}]"; + return $"[{ServiceUrl}; {ChannelId}; " + + ((ChannelAccount == null) ? "(no specific user); " : ($"{{{ChannelAccount.Id}; {ChannelAccount.Name}}}; ")) + + $"{{{ConversationAccount.Id}; {ConversationAccount.Name}}}]"; } } } \ No newline at end of file diff --git a/BotMessageRouting/Models/PartyWithTimestamps.cs b/BotMessageRouting/Models/PartyWithTimestamps.cs index baeef49..89d3881 100644 --- a/BotMessageRouting/Models/PartyWithTimestamps.cs +++ b/BotMessageRouting/Models/PartyWithTimestamps.cs @@ -13,17 +13,17 @@ public class PartyWithTimestamps : Party /// Represents the time when a request was made. /// DateTime.MinValue will indicate that no request is pending. /// - public DateTime RequestMadeTime + public DateTime ConnectionRequestTime { get; set; } /// - /// Represents the time when an engagement (1:1 conversation) was started. - /// DateTime.MinValue will indicate that this party is not engaged in a conversation. + /// Represents the time when the connection (1:1 conversation) was established. + /// DateTime.MinValue will indicate that this party is not connected. /// - public DateTime EngagementStartedTime + public DateTime ConnectionEstablishedTime { get; set; @@ -36,18 +36,18 @@ public PartyWithTimestamps(string serviceUrl, string channelId, ChannelAccount channelAccount, ConversationAccount conversationAccount) : base(serviceUrl, channelId, channelAccount, conversationAccount) { - ResetRequestMadeTime(); - ResetEngagementStartedTime(); + ResetConnectionRequestTime(); + ResetConnectionEstablishedTime(); } - public void ResetRequestMadeTime() + public void ResetConnectionRequestTime() { - RequestMadeTime = DateTime.MinValue; + ConnectionRequestTime = DateTime.MinValue; } - public void ResetEngagementStartedTime() + public void ResetConnectionEstablishedTime() { - EngagementStartedTime = DateTime.MinValue; + ConnectionEstablishedTime = DateTime.MinValue; } } } diff --git a/BotMessageRouting/Properties/AssemblyInfo.cs b/BotMessageRouting/Properties/AssemblyInfo.cs index 4bcecb7..9f402d0 100644 --- a/BotMessageRouting/Properties/AssemblyInfo.cs +++ b/BotMessageRouting/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.9.1.0")] -[assembly: AssemblyFileVersion("0.9.1.0")] +[assembly: AssemblyVersion("0.9.2.0")] +[assembly: AssemblyFileVersion("0.9.2.0")] diff --git a/BotMessageRouting/Utils/MessagingUtils.cs b/BotMessageRouting/Utils/MessagingUtils.cs index fe2d9de..e2c0d67 100644 --- a/BotMessageRouting/Utils/MessagingUtils.cs +++ b/BotMessageRouting/Utils/MessagingUtils.cs @@ -20,11 +20,12 @@ public struct ConnectorClientAndMessageBundle /// Constructs a party instance using the sender (from) of the given activity. /// /// - /// If true, will construct EngageableParty instance instead of Party. True by default. + /// If true, will construct a Party instance with timestamps + /// instead of the regular Party. True by default. /// A newly created Party instance. - public static Party CreateSenderParty(IActivity activity, bool engageable = true) + public static Party CreateSenderParty(IActivity activity, bool withTimestamps = true) { - if (engageable) + if (withTimestamps) { return new PartyWithTimestamps(activity.ServiceUrl, activity.ChannelId, activity.From, activity.Conversation); } @@ -36,11 +37,11 @@ public static Party CreateSenderParty(IActivity activity, bool engageable = true /// Constructs a party instance using the recipient of the given activity. /// /// - /// If true, will construct EngageableParty instance instead of Party. True by default. - /// A newly created Party instance. - public static Party CreateRecipientParty(IActivity activity, bool engageable = true) + /// If true, will construct a Party instance with timestamps + /// instead of the regular Party. True by default. + public static Party CreateRecipientParty(IActivity activity, bool withTimestamps = true) { - if (engageable) + if (withTimestamps) { return new PartyWithTimestamps(activity.ServiceUrl, activity.ChannelId, activity.Recipient, activity.Conversation); } diff --git a/README.md b/README.md index 16d32f5..76efb96 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,21 @@ users on different channels. In addition, it can be used in advanced customer se where the normal routines are handled by a bot, but in case the need arises, the customer can be connected with a human customer service agent. +**For an example on how to take this code into use, see +[Intermediator Bot Sample](https://github.com/tompaana/intermediator-bot-sample).** + Don't worry, if you prefer the Node.js SDK; in that case check out [this sample](https://github.com/palindromed/Bot-HandOff)! -### See also ### +### Possible use cases ### -* [Intermediator Bot Sample](https://github.com/tompaana/intermediator-bot-sample) demonstrating the - use of this core functionality -* [Chatbots as Middlemen blog post](http://tomipaananen.azurewebsites.net/?p=1851) +* Routing messages between users/bots + * See also: [Chatbots as Middlemen blog post](http://tomipaananen.azurewebsites.net/?p=1851) +* Customer service scenarios where (in tricky cases) the customer requires a human customer service agent +* Keeping track of users the bot interacts with +* Sending notifications + * For more information see [this blog post](http://tomipaananen.azurewebsites.net/?p=2231) and + [this sample](https://github.com/tompaana/remote-control-bot-sample)) ## Implementation ## @@ -23,7 +30,7 @@ Don't worry, if you prefer the Node.js SDK; in that case check out | Term | Description | | ---- | ----------- | | Aggregation (channel) | **Only applies if aggregation approach is used!** A channel where the chat requests are sent. The users in the aggregation channel can accept the requests. | -| Engagement | Is created when a request is accepted - the acceptor and the one accepted form an engagement (1:1 chat where the bot relays the messages between the users). | +| Connection | Is created when a request is accepted - the acceptor and the one accepted form a connection (1:1 chat where the bot relays the messages between the users). | | Party | A user/bot in a specific conversation. | | Conversation client | A reqular user e.g. a customer. | | Conversation owner | E.g. a customer service **agent**. | @@ -32,9 +39,10 @@ Don't worry, if you prefer the Node.js SDK; in that case check out #### [IRoutingDataManager](/BotMessageRouting/MessageRouting/DataStore/IRoutingDataManager.cs) #### -An interface for managing the parties (users/bot), aggregation channel details, the list of engaged -parties and pending requests. **Note:** In production this data should be stored in e.g. a table -storage! [LocalRoutingDataManager](/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs) +An interface for managing the parties (users/bot), aggregation channel details, the list of +connected parties and pending requests. **Note:** In production this data should be stored in e.g. +a table storage! +[LocalRoutingDataManager](/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs) is provided for testing, but it provides only an in-memory solution. #### [IMessageRouterResultHandler](/BotMessageRouting/MessageRouting/IMessageRouterResultHandler.cs) #### @@ -51,7 +59,7 @@ localization in (should your bot application require it). **[MessageRouterManager](/BotMessageRouting/MessageRouting/MessageRouterManager.cs)** is the main class of the project. It manages the routing data (using the provided `IRoutingDataManager` -implementation) and executes the actual message mediation between the parties engaged in a +implementation) and executes the actual message mediation between the parties connected in a conversation. #### Properties #### @@ -64,7 +72,7 @@ conversation. * **`HandleActivityAsync`**: In *very simple* cases this is the only method you need to call in your `MessagesController` class. It will track the users (stores their information), forward - messages between users engaged in a conversation automatically. The return value + messages between users connected in a conversation automatically. The return value (`MessageRouterResult`) will indicate whether the message routing logic consumed the activity or not. If the activity was ignored by the message routing logic, you can e.g. forward it to your dialog. @@ -76,13 +84,13 @@ conversation. * `RemoveParty`: Removes all the instances related to the given party from the routing data (since there can be multiple - one for each conversation). Will also remove any pending requests of the party in question as well as end all conversations of this specific user. -* `IntiateEngagement`: Creates a request on behalf of the sender of the activity. +* `RequestConnection`: Creates a request on behalf of the sender of the activity. * `RejectPendingRequest`: Removes the pending request of the given user. -* `AddEngagementAsync`: Establishes an engagement between the given parties. This method should be - called when a chat request is accepted. -* `EndEngagement`: Ends the engagement and severs the connection between the users so that the +* `ConnectAsync`: Establishes a connection between the given parties. This method should be called + when a chat request is accepted (given that requests are necessary). +* `Disconnect`: Ends the conversation and severs the connection between the users so that the messages are no longer relayed. -* `HandleMessageAsync`: Handles the incoming messages: Relays the messages between engaged parties. +* `HandleMessageAsync`: Handles the incoming messages: Relays the messages between connected parties. ### Other classes ### @@ -92,6 +100,10 @@ be a `Party` instance of a user/bot for each conversation (i.e. there can be mul single user/bot). One can think of `Party` as a full address the bot needs in order to send a message to the user in a conversation. The `Party` instances are stored in routing data. +**[PartyWithTimestamps](/BotMessageRouting/Models/PartyWithTimestamps.cs)** - like `Party`, but with +timestamps to mark when a request was made and when a connection was established. Useful for +monitoring waiting times and durations of conversations. + **[MessageRouterResult](/BotMessageRouting/MessageRouting/MessageRouterResult.cs)** is the return value for more complex operations of the `MessageRouterManager` class not unlike custom `EventArgs` implementations, but due to the problems that using actual event handlers can cause, these return From 7d17c12367a2ace022f961b07f8969146324503f Mon Sep 17 00:00:00 2001 From: Tomi Paananen Date: Tue, 29 Aug 2017 16:05:56 +0300 Subject: [PATCH 4/7] Fine-tune of MessageRouterResultType --- .../MessageRouting/DataStore/LocalRoutingDataManager.cs | 4 ++-- BotMessageRouting/MessageRouting/MessageRouterResult.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs b/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs index 112df2a..7997b46 100644 --- a/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs +++ b/BotMessageRouting/MessageRouting/DataStore/LocalRoutingDataManager.cs @@ -250,7 +250,7 @@ public virtual MessageRouterResult AddPendingRequest(Party party) { if (PendingRequests.Contains(party)) { - result.Type = MessageRouterResultType.TryingToConnect; + result.Type = MessageRouterResultType.ConnectionAlreadyRequested; } else { @@ -267,7 +267,7 @@ public virtual MessageRouterResult AddPendingRequest(Party party) } PendingRequests.Add(party); - result.Type = MessageRouterResultType.Connecting; + result.Type = MessageRouterResultType.ConnectionRequested; } } } diff --git a/BotMessageRouting/MessageRouting/MessageRouterResult.cs b/BotMessageRouting/MessageRouting/MessageRouterResult.cs index 7573127..bfe7cb4 100644 --- a/BotMessageRouting/MessageRouting/MessageRouterResult.cs +++ b/BotMessageRouting/MessageRouting/MessageRouterResult.cs @@ -7,8 +7,8 @@ public enum MessageRouterResultType { NoActionTaken, // No action taken - The result handler should ignore results with this type OK, // Action taken, but the result handler should ignore results with this type - Connecting, - TryingToConnect, // Connection attempt already initiated + ConnectionRequested, + ConnectionAlreadyRequested, ConnectionRejected, Connected, Disconnected, From 6a3d1e0d72b8e80e7d9279c9d518c3f0f9cc7843 Mon Sep 17 00:00:00 2001 From: Tomi Paananen Date: Tue, 29 Aug 2017 17:07:25 +0300 Subject: [PATCH 5/7] Updated README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 76efb96..5c5c1fa 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ connected with a human customer service agent. **For an example on how to take this code into use, see [Intermediator Bot Sample](https://github.com/tompaana/intermediator-bot-sample).** +This project is also available as +[NuGet package](https://www.nuget.org/packages/Underscore.Bot.MessageRouting). + Don't worry, if you prefer the Node.js SDK; in that case check out [this sample](https://github.com/palindromed/Bot-HandOff)! From a7b748f9557b04d7111cb6547a4dcb8d57181614 Mon Sep 17 00:00:00 2001 From: Tomi Paananen Date: Tue, 29 Aug 2017 17:16:52 +0300 Subject: [PATCH 6/7] Updated nuspec --- BotMessageRouting/BotMessageRouting.nuspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BotMessageRouting/BotMessageRouting.nuspec b/BotMessageRouting/BotMessageRouting.nuspec index 31c78e3..13911c8 100644 --- a/BotMessageRouting/BotMessageRouting.nuspec +++ b/BotMessageRouting/BotMessageRouting.nuspec @@ -2,16 +2,16 @@ Underscore.Bot.MessageRouting - 0.9.0.0 + 0.9.5.0 Bot Message Routing component - See README on project website + Tomi Paananen et al. (see README on project website) Tomi Paananen https://github.com/tompaana/bot-message-routing/blob/master/LICENSE https://github.com/tompaana/bot-message-routing false Message routing component for chatbots built with Microsoft Bot Framework C# SDK - This is the first release. + This is the second pre-release. Copyright 2017 Microsoft chatbot chatbots bot bots messaging message routing From a0627959c5d6d2fbf0159f363a4482c99c6e2753 Mon Sep 17 00:00:00 2001 From: Tomi Paananen Date: Tue, 29 Aug 2017 17:20:42 +0300 Subject: [PATCH 7/7] Fixed the version number in nuspec. --- BotMessageRouting/BotMessageRouting.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BotMessageRouting/BotMessageRouting.nuspec b/BotMessageRouting/BotMessageRouting.nuspec index 13911c8..0d6a502 100644 --- a/BotMessageRouting/BotMessageRouting.nuspec +++ b/BotMessageRouting/BotMessageRouting.nuspec @@ -2,7 +2,7 @@ Underscore.Bot.MessageRouting - 0.9.5.0 + 0.9.2.0 Bot Message Routing component Tomi Paananen et al. (see README on project website) Tomi Paananen