public virtual MessageRouterResult AddEngagementAndClearPendingRequest(Party conversationOwnerParty, Party conversationClientParty) { MessageRouterResult result = new MessageRouterResult() { ConversationOwnerParty = conversationOwnerParty, ConversationClientParty = conversationClientParty }; if (conversationOwnerParty != null && conversationClientParty != null) { try { EngagedParties.Add(conversationOwnerParty, conversationClientParty); PendingRequests.Remove(conversationClientParty); result.Type = MessageRouterResultType.EngagementAdded; } 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}"); } } else { result.Type = MessageRouterResultType.Error; result.ErrorMessage = "Either the owner or the client is missing"; } return(result); }
/// <summary> /// Tries to reject the pending engagement request of the given party. /// </summary> /// <param name="partyToReject">The party whose request to reject.</param> /// <param name="rejecterParty">The party rejecting the request (optional).</param> /// <returns>The result of the operation.</returns> public async Task <MessageRouterResult> RejectPendingRequestAsync(Party partyToReject, Party rejecterParty = null) { if (partyToReject == null) { throw new ArgumentNullException($"The party to reject ({nameof(partyToReject)} cannot be null"); } MessageRouterResult result = new MessageRouterResult() { ConversationOwnerParty = rejecterParty, ConversationClientParty = partyToReject }; if (RoutingDataManager.RemovePendingRequest(partyToReject)) { result.Type = MessageRouterResultType.EngagementRejected; } else { result.Type = MessageRouterResultType.Error; result.ErrorMessage = $"Failed to remove the pending request of user \"{partyToReject.ChannelAccount?.Name}\""; } await HandleAndLogMessageRouterResultAsync(result); return(result); }
public virtual MessageRouterResult AddPendingRequest(Party party) { MessageRouterResult result = new MessageRouterResult() { ConversationClientParty = party }; if (party != null) { if (PendingRequests.Contains(party)) { result.Type = MessageRouterResultType.EngagementAlreadyInitiated; } else { PendingRequests.Add(party); result.Type = MessageRouterResultType.EngagementInitiated; } } else { result.Type = MessageRouterResultType.Error; result.ErrorMessage = "The given party instance is null"; } return(result); }
/// <summary> /// 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. /// </summary> /// <param name="conversationOwnerParty">The party who owns the conversation (e.g. customer service agent).</param> /// <param name="conversationClientParty">The other party in the conversation.</param> /// <returns>The result of the operation.</returns> public async Task <MessageRouterResult> AddEngagementAsync( Party conversationOwnerParty, Party conversationClientParty) { if (conversationOwnerParty == null || conversationClientParty == null) { throw new ArgumentNullException( $"Neither of the arguments ({nameof(conversationOwnerParty)}, {nameof(conversationClientParty)}) can be null"); } MessageRouterResult result = new MessageRouterResult() { ConversationOwnerParty = conversationOwnerParty, ConversationClientParty = conversationClientParty }; Party botParty = RoutingDataManager.FindBotPartyByChannelAndConversation( conversationOwnerParty.ChannelId, conversationOwnerParty.ConversationAccount); if (botParty != null) { ConnectorClient connectorClient = new ConnectorClient(new Uri(conversationOwnerParty.ServiceUrl)); ConversationResourceResponse response = await connectorClient.Conversations.CreateDirectConversationAsync( botParty.ChannelAccount, conversationOwnerParty.ChannelAccount); if (response != null && !string.IsNullOrEmpty(response.Id)) { // The conversation account of the conversation owner for this 1:1 chat is different - // thus, we need to create a new party instance ConversationAccount directConversationAccount = new ConversationAccount(id: response.Id); Party acceptorPartyEngaged = new Party( conversationOwnerParty.ServiceUrl, conversationOwnerParty.ChannelId, conversationOwnerParty.ChannelAccount, directConversationAccount); RoutingDataManager.AddParty(acceptorPartyEngaged); RoutingDataManager.AddParty( new Party(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"; } } else { result.Type = MessageRouterResultType.Error; result.ErrorMessage = "Failed to find the bot instance"; } await HandleAndLogMessageRouterResultAsync(result); return(result); }
/// <summary> /// Handles and logs the given message router result. /// </summary> /// <param name="messageRouterResult">The result to handle.</param> /// <returns></returns> private async Task HandleAndLogMessageRouterResultAsync(MessageRouterResult messageRouterResult) { if (messageRouterResult != null) { await ResultHandler.HandleResultAsync(messageRouterResult); AddMessageRouterResultToLog(messageRouterResult); } }
/// <summary> /// 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. /// </summary> /// <param name="activity">The activity.</param> /// <returns>The result of the operation.</returns> public async Task <MessageRouterResult> InitiateEngagementAsync(Activity activity) { MessageRouterResult messageRouterResult = RoutingDataManager.AddPendingRequest(MessagingUtils.CreateSenderParty(activity)); messageRouterResult.Activity = activity; await HandleAndLogMessageRouterResultAsync(messageRouterResult); return(messageRouterResult); }
public void AddMessageRouterResult(MessageRouterResult result) { if (result != null) { if (LastMessageRouterResults.Count > 9) { LastMessageRouterResults.Remove(LastMessageRouterResults.ElementAt(0)); } LastMessageRouterResults.Add(result); } }
/// <summary> /// For debugging. /// Adds the given result to the log. /// </summary> /// <param name="messageRouterResult">The message router result to add to the log.</param> public void AddMessageRouterResultToLog(MessageRouterResult messageRouterResult) { #if DEBUG if (messageRouterResult != null) { if (RoutingDataManager is LocalRoutingDataManager) { (RoutingDataManager as LocalRoutingDataManager).AddMessageRouterResult(messageRouterResult); } System.Diagnostics.Debug.WriteLine($"Message router result: {messageRouterResult.ToString()}"); } #endif }
/// <summary> /// Handles the new activity. /// </summary> /// <param name="activity">The activity to handle.</param> /// <param name="tryToInitiateEngagementIfNotEngaged">If true, will try to initiate /// the engagement (1:1 conversation) automatically, if the sender is not engaged already.</param> /// <param name="addClientNameToMessage">If true, will add the client's name to the beginning of the message.</param> /// <param name="addOwnerNameToMessage">If true, will add the owner's (agent) name to the beginning of the message.</param> /// <returns>The result of the operation.</returns> public async Task <MessageRouterResult> HandleActivityAsync( Activity activity, bool tryToInitiateEngagementIfNotEngaged, bool addClientNameToMessage = true, bool addOwnerNameToMessage = false) { MessageRouterResult result = new MessageRouterResult(); result.Type = MessageRouterResultType.NoActionTaken; // Make sure we have the details of the sender and the receiver (bot) stored MakeSurePartiesAreTracked(activity); // Check for back channel messages // If agent UI is in use, conversation requests are accepted by these messages if (await HandleBackChannelMessageAsync(activity)) { // A back channel message was detected and handled result.Type = MessageRouterResultType.OK; } // Check for possible commands else if (await CommandHandler.HandleCommandAsync(activity)) { // The message contained a command that was handled result.Type = MessageRouterResultType.OK; } else { // 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) { result = await InitiateEngagementAsync(activity); } } } return(result); }
/// <summary> /// From IMessageRouterResultHandler. /// </summary> /// <param name="messageRouterResult">The result to handle.</param> /// <returns></returns> public virtual async Task HandleResultAsync(MessageRouterResult messageRouterResult) { if (messageRouterResult == null) { throw new ArgumentNullException($"The given result ({nameof(messageRouterResult)}) is null"); } string message = ""; MessageRouterManager messageRouterManager; switch (messageRouterResult.Type) { case MessageRouterResultType.NoActionTaken: case MessageRouterResultType.OK: // No need to do anything break; case MessageRouterResultType.EngagementInitiated: case MessageRouterResultType.EngagementAlreadyInitiated: case MessageRouterResultType.EngagementRejected: case MessageRouterResultType.EngagementAdded: case MessageRouterResultType.EngagementRemoved: await HandleEngagementChangedResultAsync(messageRouterResult); break; case MessageRouterResultType.NoAggregationChannel: if (messageRouterResult.Activity != null) { messageRouterManager = MessageRouterManager.Instance; string botName = messageRouterManager.RoutingDataManager.ResolveBotNameInConversation( MessagingUtils.CreateSenderParty(messageRouterResult.Activity)); message = $"{(string.IsNullOrEmpty(messageRouterResult.ErrorMessage) ? "" : $"{messageRouterResult.ErrorMessage}: ")}The message router manager is not initialized; type \""; message += string.IsNullOrEmpty(botName) ? $"{Commands.CommandKeyword} " : $"@{botName} "; message += $"{Commands.CommandAddAggregationChannel}\" to setup the aggregation channel"; await MessagingUtils.ReplyToActivityAsync(messageRouterResult.Activity, message); }
/// <summary> /// From IMessageRouterResultHandler. /// </summary> /// <param name="messageRouterResult">The result to handle.</param> /// <returns></returns> public virtual async Task HandleResultAsync(MessageRouterResult messageRouterResult) { if (messageRouterResult == null) { throw new ArgumentNullException($"The given result ({nameof(messageRouterResult)}) is null"); } if (messageRouterResult.Type == MessageRouterResultType.NoActionTaken || messageRouterResult.Type == MessageRouterResultType.OK) { // No need to do anything } if (messageRouterResult.Type == MessageRouterResultType.EngagementInitiated || messageRouterResult.Type == MessageRouterResultType.EngagementAlreadyInitiated || messageRouterResult.Type == MessageRouterResultType.EngagementRejected || messageRouterResult.Type == MessageRouterResultType.EngagementAdded || messageRouterResult.Type == MessageRouterResultType.EngagementRemoved) { await HandleEngagementChangedResultAsync(messageRouterResult); } else if (messageRouterResult.Type == MessageRouterResultType.NoAggregationChannel) { if (messageRouterResult.Activity != null) { MessageRouterManager messageRouterManager = MessageRouterManager.Instance; string botName = messageRouterManager.RoutingDataManager.ResolveBotNameInConversation( MessagingUtils.CreateSenderParty(messageRouterResult.Activity)); string message = $"{(string.IsNullOrEmpty(messageRouterResult.ErrorMessage)? "" : $"{messageRouterResult.ErrorMessage}: ")}The message router manager is not initialized; type \""; message += string.IsNullOrEmpty(botName) ? $"{Commands.CommandKeyword} " : $"@{botName} "; message += $"{Commands.CommandAddAggregationChannel}\" to setup the aggregation channel"; await MessagingUtils.ReplyToActivityAsync(messageRouterResult.Activity, message); } else { System.Diagnostics.Debug.WriteLine("The activity of the result is null"); } }
/// <summary> /// 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). /// </summary> /// <param name="activity">The activity to check for back channel messages.</param> /// <returns>True, if a back channel message was detected and handled. False otherwise.</returns> public async Task <bool> HandleBackChannelMessageAsync(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 partyId = ((JObject)activity.ChannelData)[BackChannelId][PartyIdPropertyId].ToString(); Party conversationClientParty = Party.FromIdString(partyId); Party conversationOwnerParty = MessagingUtils.CreateSenderParty(activity); messageRouterResult = RoutingDataManager.AddEngagementAndClearPendingRequest( conversationOwnerParty, conversationClientParty); messageRouterResult.Activity = activity; } } else { // No back channel message detected messageRouterResult.Type = MessageRouterResultType.NoActionTaken; } await HandleAndLogMessageRouterResultAsync(messageRouterResult); return(messageRouterResult.Type == MessageRouterResultType.EngagementAdded); }
/// <summary> /// 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 /// channel that party is on. /// </summary> /// <param name="activity">The activity to handle.</param> /// <param name="addClientNameToMessage">If true, will add the client's name to the beginning of the message.</param> /// <param name="addOwnerNameToMessage">If true, will add the owner's (agent) name to the beginning of the message.</param> /// <returns>The result of the operation.</returns> public async Task <MessageRouterResult> HandleMessageAsync( Activity activity, bool addClientNameToMessage = true, bool addOwnerNameToMessage = false) { MessageRouterResult result = new MessageRouterResult() { Type = MessageRouterResultType.NoActionTaken, Activity = activity }; Party senderParty = MessagingUtils.CreateSenderParty(activity); if (RoutingDataManager.IsEngaged(senderParty, EngagementProfile.Owner)) { // Sender is an owner of an ongoing conversation - forward the message result.ConversationOwnerParty = senderParty; Party partyToForwardMessageTo = RoutingDataManager.GetEngagedCounterpart(senderParty); if (partyToForwardMessageTo != null) { result.ConversationClientParty = partyToForwardMessageTo; string message = addOwnerNameToMessage ? $"{senderParty.ChannelAccount.Name}: {activity.Text}" : activity.Text; ResourceResponse resourceResponse = await SendMessageToPartyByBotAsync(partyToForwardMessageTo, activity.Text); if (MessagingUtils.WasSuccessful(resourceResponse)) { result.Type = MessageRouterResultType.OK; } else { result.Type = MessageRouterResultType.FailedToForwardMessage; result.ErrorMessage = $"Failed to forward the message to user {partyToForwardMessageTo}"; } } else { result.Type = MessageRouterResultType.FailedToForwardMessage; result.ErrorMessage = "Failed to find the party to forward the message to"; } } else if (RoutingDataManager.IsEngaged(senderParty, EngagementProfile.Client)) { // Sender is a participant of an ongoing conversation - forward the message result.ConversationClientParty = senderParty; Party partyToForwardMessageTo = RoutingDataManager.GetEngagedCounterpart(senderParty); if (partyToForwardMessageTo != null) { result.ConversationOwnerParty = partyToForwardMessageTo; string message = addClientNameToMessage ? $"{senderParty.ChannelAccount.Name}: {activity.Text}" : activity.Text; await SendMessageToPartyByBotAsync(partyToForwardMessageTo, message); result.Type = MessageRouterResultType.OK; } else { result.Type = MessageRouterResultType.FailedToForwardMessage; result.ErrorMessage = "Failed to find the party to forward the message to"; } } await HandleAndLogMessageRouterResultAsync(result); return(result); }
/// <summary> /// 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. /// </summary> /// <param name="conversationOwnerParty">The party who owns the conversation (e.g. customer service agent).</param> /// <param name="conversationClientParty">The other party in the conversation.</param> /// <returns>The result of the operation.</returns> public async Task <MessageRouterResult> AddEngagementAsync( Party conversationOwnerParty, Party conversationClientParty) { if (conversationOwnerParty == null || conversationClientParty == null) { throw new ArgumentNullException( $"Neither of the arguments ({nameof(conversationOwnerParty)}, {nameof(conversationClientParty)}) can be null"); } MessageRouterResult result = new MessageRouterResult() { ConversationOwnerParty = conversationOwnerParty, ConversationClientParty = conversationClientParty }; Party botParty = RoutingDataManager.FindBotPartyByChannelAndConversation( conversationOwnerParty.ChannelId, conversationOwnerParty.ConversationAccount); if (botParty != null) { ConnectorClient connectorClient = new ConnectorClient(new Uri(conversationOwnerParty.ServiceUrl)); ConversationResourceResponse response = null; try { response = await connectorClient.Conversations.CreateDirectConversationAsync(botParty.ChannelAccount, conversationOwnerParty.ChannelAccount); // ResponseId conversationOwnerParty.ConversationAccount.Id not consistent with each other across channels // Here we need the ConversationAccountId to route messages correctly across channels eg: // Slack they are the same: // response.Id: B6JJQ7939: T6HKNHCP7: D6H04L58R // conversationOwnerParty.ConversationAccount.Id: B6JJQ7939: T6HKNHCP7: D6H04L58R // Skype they are not: // response.Id: 8:daltskin // conversationOwnerParty.ConversationAccount.Id: 29:11MZyI5R2Eak3t7bFjDwXmjQYnSl7aTBEB8zaSMDIEpA if (response != null && !string.IsNullOrEmpty(conversationOwnerParty.ConversationAccount.Id)) { // The conversation account of the conversation owner for this 1:1 chat is different - so create a new party instance ConversationAccount directConversationAccount = new ConversationAccount(id: conversationOwnerParty.ConversationAccount.Id); Party acceptorPartyEngaged = new Party( conversationOwnerParty.ServiceUrl, conversationOwnerParty.ChannelId, conversationOwnerParty.ChannelAccount, directConversationAccount); RoutingDataManager.AddParty(acceptorPartyEngaged); RoutingDataManager.AddParty( new Party(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"; } } catch (Exception exp) { result.Type = MessageRouterResultType.Error; result.ErrorMessage = exp.Message; } } else { result.Type = MessageRouterResultType.Error; result.ErrorMessage = "Failed to find the bot instance"; } await HandleAndLogMessageRouterResultAsync(result); return(result); }