Beispiel #1
0
        /// <summary>
        /// Ends the conversation(s) of the given party.
        /// </summary>
        /// <param name="connectedParty">The party connected in a conversation.</param>
        /// <param name="connectionProfile">The connection profile of the given party.</param>
        /// <returns>A list of operation results:
        /// - MessageRouterResultType.NoActionTaken, if no connection to disconnect was found OR,
        /// - MessageRouterResultType.Disconnected for each disconnection, when successful.
        /// </returns>
        protected virtual List <MessageRouterResult> Disconnect(Party connectedParty, ConnectionProfile connectionProfile)
        {
            List <MessageRouterResult> messageRouterResults = new List <MessageRouterResult>();

            Party partyInConversation = RoutingDataManager.FindConnectedPartyByChannel(
                connectedParty.ChannelId, connectedParty.ChannelAccount);

            if (partyInConversation != null &&
                RoutingDataManager.IsConnected(partyInConversation, connectionProfile))
            {
                messageRouterResults.AddRange(
                    RoutingDataManager.Disconnect(partyInConversation, connectionProfile));
            }
            else
            {
                MessageRouterResult messageRouterResult = new MessageRouterResult()
                {
                    Type         = MessageRouterResultType.Error,
                    ErrorMessage = "No connection to disconnect found"
                };

                if (connectionProfile == ConnectionProfile.Client)
                {
                    messageRouterResult.ConversationClientParty = connectedParty;
                }
                else if (connectionProfile == ConnectionProfile.Owner)
                {
                    messageRouterResult.ConversationOwnerParty = connectedParty;
                }

                messageRouterResults.Add(messageRouterResult);
            }

            return(messageRouterResults);
        }
        /// <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 MessageRouterResult RejectPendingRequest(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}\"";
            }

            return(result);
        }
        /// <summary>
        /// Handles the new activity.
        /// </summary>
        /// <param name="activity">The activity to handle.</param>
        /// <param name="tryToRequestConnectionIfNotConnected">If true, will try to initiate
        /// the connection (1:1 conversation) automatically, if the sender is not connected 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 tryToRequestConnectionIfNotConnected,
            bool addClientNameToMessage = true, bool addOwnerNameToMessage = false)
        {
            MessageRouterResult result = new MessageRouterResult()
            {
                Type = MessageRouterResultType.NoActionTaken
            };

            // Make sure we have the details of the sender and the receiver (bot) stored
            MakeSurePartiesAreTracked(activity);

            result = await HandleMessageAsync(activity, addClientNameToMessage, addOwnerNameToMessage);

            if (result.Type == MessageRouterResultType.NoActionTaken)
            {
                // The message was not handled, because the sender is not connected in a conversation
                if (tryToRequestConnectionIfNotConnected)
                {
                    result = RequestConnection(activity);
                }
            }

            return(result);
        }
        /// <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 MessageRouterResult InitiateEngagement(Activity activity)
        {
            MessageRouterResult messageRouterResult =
                RoutingDataManager.AddPendingRequest(MessagingUtils.CreateSenderParty(activity));

            messageRouterResult.Activity = activity;
            return(messageRouterResult);
        }
Beispiel #5
0
        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="activity">The activity. The SENDER in this activity (From property) is considered the requestor.</param>
        /// <param name="rejectConnectionRequestIfNoAggregationChannel">If true, will reject all requests, if there is no aggregation channel.</param>
        /// <returns>Same as RequestConnection(Party, bool)</returns>
        public virtual MessageRouterResult RequestConnection(
            Activity activity, bool rejectConnectionRequestIfNoAggregationChannel = false)
        {
            MessageRouterResult messageRouterResult =
                RequestConnection(MessagingUtils.CreateSenderParty(activity), rejectConnectionRequestIfNoAggregationChannel);

            messageRouterResult.Activity = activity;
            return(messageRouterResult);
        }
Beispiel #6
0
        /// <summary>
        /// Handles the new activity:
        ///   1. Adds both the sender and the receiver of the given activity into the routing data
        ///      storage (if they do not already exist there).
        ///   2. The message in the given activity is routed to the appropriate receiver (user),
        ///      if the its sender is connected in a conversation with the receiver.
        ///   3. If connection requests are set to happen automatically
        ///      (tryToRequestConnectionIfNotConnected is true) and the sender is not yet
        ///      connected in a conversation, a request is made.
        /// </summary>
        /// <param name="activity">The activity to handle.</param>
        /// <param name="tryToRequestConnectionIfNotConnected">If true, will try to initiate
        /// the connection (1:1 conversation) automatically, if the sender is not connected already.</param>
        /// <param name="rejectConnectionRequestIfNoAggregationChannel">If true and the automatical
        /// connection request is made, will reject all requests, if there is no aggregation channel.</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:
        /// - MessageRouterResultType.NoActionTaken, if the activity was not consumed (no special action taken) OR
        /// - MessageRouterResultType.ConnectionRequested, if a request was successfully made OR
        /// - MessageRouterResultType.ConnectionAlreadyRequested, if a request for the given party already exists OR
        /// - MessageRouterResultType.NoAgentsAvailable, if no aggregation channel exists while one is required OR
        /// - MessageRouterResultType.OK, if the activity was consumed and the message was routed successfully OR
        /// - MessageRouterResultType.FailedToForwardMessage in case a rule to route the message was in place, but the action failed (see the error message) OR
        /// - MessageRouterResultType.Error in case of an error (see the error message).
        /// </returns>
        public virtual async Task <MessageRouterResult> HandleActivityAsync(
            Activity activity,
            bool tryToRequestConnectionIfNotConnected,
            bool rejectConnectionRequestIfNoAggregationChannel,
            bool addClientNameToMessage = true,
            bool addOwnerNameToMessage  = false)
        {
            MakeSurePartiesAreTracked(activity);

            MessageRouterResult messageRouterResult =
                await RouteMessageIfSenderIsConnectedAsync(activity, addClientNameToMessage, addOwnerNameToMessage);

            if (tryToRequestConnectionIfNotConnected &&
                messageRouterResult.Type == MessageRouterResultType.NoActionTaken)
            {
                messageRouterResult = RequestConnection(activity, rejectConnectionRequestIfNoAggregationChannel);
            }

            return(messageRouterResult);
        }
Beispiel #7
0
        /// <summary>
        /// Tries to reject the pending connection 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:
        /// - MessageRouterResultType.ConnectionRejected, if the pending request was removed OR
        /// - MessageRouterResultType.Error in case of an error (see the error message).
        /// </returns>
        public virtual MessageRouterResult RejectPendingRequest(Party partyToReject, Party rejecterParty = null)
        {
            if (partyToReject == null)
            {
                throw new ArgumentNullException($"The party to reject ({nameof(partyToReject)} cannot be null");
            }

            MessageRouterResult messageRouteResult = RoutingDataManager.RemovePendingRequest(partyToReject);

            messageRouteResult.ConversationClientParty = partyToReject;
            messageRouteResult.ConversationOwnerParty  = rejecterParty;

            if (messageRouteResult.Type == MessageRouterResultType.Error)
            {
                messageRouteResult.ErrorMessage =
                    $"Failed to remove the pending request of user \"{partyToReject.ChannelAccount?.Name}\": {messageRouteResult.ErrorMessage}";
            }

            return(messageRouteResult);
        }
        /// <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>The result; if the type of the result is
        /// MessageRouterResultType.EngagementAdded, the operation was successful.</returns>
        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);
        }
        /// <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()
            {
                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 (HandleBackChannelMessage(activity).Type == MessageRouterResultType.EngagementAdded)
            {
                // A back channel message was detected and 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 = InitiateEngagement(activity);
                    }
                }
            }

            return(result);
        }
        /// <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 (resourceResponse != null)
                    {
                        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";
                }
            }

            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));

                try
                {
                    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))
                    {
                        // 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: conversationOwnerParty.ConversationAccount.Id);

                        Party acceptorPartyEngaged = new EngageableParty(
                            conversationOwnerParty.ServiceUrl, conversationOwnerParty.ChannelId,
                            conversationOwnerParty.ChannelAccount, directConversationAccount);

                        RoutingDataManager.AddParty(acceptorPartyEngaged);
                        RoutingDataManager.AddParty(
                            new EngageableParty(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 e)
                {
                    result.Type         = MessageRouterResultType.Error;
                    result.ErrorMessage = $"Failed to create a direct conversation: {e.Message}";
                }
            }
            else
            {
                result.Type         = MessageRouterResultType.Error;
                result.ErrorMessage = "Failed to find the bot instance";
            }

            return(result);
        }
Beispiel #12
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
        /// conversation, if a new direct conversation is created.
        /// </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>
        /// <param name="createNewDirectConversation">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 connection
        /// (for the new direct conversation).</param>
        /// <returns>
        /// The result of the operation:
        /// - MessageRouterResultType.Connected, if successfully connected OR
        /// - MessageRouterResultType.Error in case of an error (see the error message).
        ///
        /// The result will also contain the connected parties and note that the client's identity
        /// will have changed, if a new direct conversation was created!
        /// </returns>
        public virtual async Task <MessageRouterResult> ConnectAsync(
            Party conversationOwnerParty, Party conversationClientParty, bool createNewDirectConversation)
        {
            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)
            {
                if (createNewDirectConversation)
                {
                    ConnectorClient connectorClient = new ConnectorClient(new Uri(conversationOwnerParty.ServiceUrl));
                    ConversationResourceResponse conversationResourceResponse = null;

                    try
                    {
                        conversationResourceResponse =
                            await connectorClient.Conversations.CreateDirectConversationAsync(
                                botParty.ChannelAccount, conversationOwnerParty.ChannelAccount);
                    }
                    catch (Exception e)
                    {
                        System.Diagnostics.Debug.WriteLine($"Failed to create a direct conversation: {e.Message}");
                        // 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 re-create the conversation owner instance
                        ConversationAccount directConversationAccount =
                            new ConversationAccount(id: conversationResourceResponse.Id);

                        conversationOwnerParty = new Party(
                            conversationOwnerParty.ServiceUrl, conversationOwnerParty.ChannelId,
                            conversationOwnerParty.ChannelAccount, directConversationAccount);

                        RoutingDataManager.AddParty(conversationOwnerParty);
                        RoutingDataManager.AddParty(new Party(
                                                        botParty.ServiceUrl, botParty.ChannelId, botParty.ChannelAccount, directConversationAccount), false);

                        result.ConversationResourceResponse = conversationResourceResponse;
                    }
                }

                result = RoutingDataManager.ConnectAndClearPendingRequest(conversationOwnerParty, conversationClientParty);
            }
            else
            {
                result.Type         = MessageRouterResultType.Error;
                result.ErrorMessage = "Failed to find the bot instance";
            }

            return(result);
        }