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