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);
        }
Example #4
0
        /// <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);
        }