/// <summary> /// Tries to send the given message activity to the given party using this bot on the same /// channel as the party who the message is sent to. /// </summary> /// <param name="partyToMessage">The party to send the message to.</param> /// <param name="messageActivity">The message activity to send (message content).</param> /// <returns>The ResourceResponse instance or null in case of an error.</returns> public async Task <ResourceResponse> SendMessageToPartyByBotAsync(Party partyToMessage, IMessageActivity messageActivity) { Party botParty = null; if (partyToMessage != null) { // We need the channel account of the bot in the SAME CHANNEL as the RECIPIENT. // The identity of the bot in the channel of the sender is most likely a different one and // thus unusable since it will not be recognized on the recipient's channel. botParty = RoutingDataManager.FindBotPartyByChannelAndConversation( partyToMessage.ChannelId, partyToMessage.ConversationAccount); } if (botParty != null) { messageActivity.From = botParty.ChannelAccount; MessagingUtils.ConnectorClientAndMessageBundle bundle = MessagingUtils.CreateConnectorClientAndMessageActivity( partyToMessage.ServiceUrl, messageActivity); return(await bundle.connectorClient.Conversations.SendToConversationAsync( (Activity)bundle.messageActivity)); } return(null); }
/// <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); }
/// <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> /// Tries to send the given message to the given party using this bot on the same channel /// as the party who the message is sent to. /// </summary> /// <param name="partyToMessage">The party to send the message to.</param> /// <param name="messageText">The message content.</param> /// <returns>The ResourceResponse instance or null in case of an error.</returns> public async Task <ResourceResponse> SendMessageToPartyByBotAsync(Party partyToMessage, string messageText) { Party botParty = null; if (partyToMessage != null) { botParty = RoutingDataManager.FindBotPartyByChannelAndConversation( partyToMessage.ChannelId, partyToMessage.ConversationAccount); } if (botParty != null) { MessagingUtils.ConnectorClientAndMessageBundle bundle = MessagingUtils.CreateConnectorClientAndMessageActivity( partyToMessage, messageText, botParty?.ChannelAccount); return(await bundle.connectorClient.Conversations.SendToConversationAsync( (Activity)bundle.messageActivity)); } return(null); }
/// <summary> /// All messages where the bot was mentioned ("@<bot name>) are checked for possible commands. /// See IBotCommandHandler.cs for more information. /// </summary> public async virtual Task <bool> HandleCommandAsync(Activity activity) { bool wasHandled = false; Activity replyActivity = null; if (WasBotAddressedDirectly(activity) || (!string.IsNullOrEmpty(activity.Text) && activity.Text.StartsWith($"{Commands.CommandKeyword} "))) { string message = MessagingUtils.StripMentionsFromMessage(activity); if (message.StartsWith($"{Commands.CommandKeyword} ")) { message = message.Remove(0, Commands.CommandKeyword.Length + 1); } string messageInLowerCase = message?.ToLower(); if (messageInLowerCase.StartsWith(Commands.CommandAddAggregationChannel)) { // Check if the Aggregation Party already exists Party aggregationParty = new Party( activity.ServiceUrl, activity.ChannelId, /* not a specific user, but a channel/conv */ null, activity.Conversation); // Establish the sender's channel/conversation as an aggreated one if (_routingDataManager.AddAggregationParty(aggregationParty)) { replyActivity = activity.CreateReply( "This channel/conversation is now where the requests are aggregated"); } else { // Aggregation already exists replyActivity = activity.CreateReply( "This channel/conversation is already receiving requests"); } wasHandled = true; } else if (messageInLowerCase.StartsWith(Commands.CommandAcceptRequest) || messageInLowerCase.StartsWith(Commands.CommandRejectRequest)) { // Accept/reject conversation request bool doAccept = messageInLowerCase.StartsWith(Commands.CommandAcceptRequest); Party senderParty = MessagingUtils.CreateSenderParty(activity); if (_routingDataManager.IsAssociatedWithAggregation(senderParty)) { // The party is associated with the aggregation and has the right to accept/reject Party senderInConversation = _routingDataManager.FindEngagedPartyByChannel(senderParty.ChannelId, senderParty.ChannelAccount); if (senderInConversation == null || !_routingDataManager.IsEngaged(senderInConversation, EngagementProfile.Owner)) { if (_routingDataManager.GetPendingRequests().Count > 0) { // The name of the user to accept should be the second word string[] splitMessage = message.Split(' '); if (splitMessage.Count() > 1 && !string.IsNullOrEmpty(splitMessage[1])) { Party partyToAcceptOrReject = null; string errorMessage = ""; try { partyToAcceptOrReject = _routingDataManager.GetPendingRequests().Single( party => (party.ChannelAccount != null && !string.IsNullOrEmpty(party.ChannelAccount.Id) && party.ChannelAccount.Id.Equals(splitMessage[1]))); } catch (InvalidOperationException e) { errorMessage = e.Message; } if (partyToAcceptOrReject != null) { MessageRouterManager messageRouterManager = MessageRouterManager.Instance; if (doAccept) { await messageRouterManager.AddEngagementAsync(senderParty, partyToAcceptOrReject); } else { await messageRouterManager.RejectPendingRequestAsync(partyToAcceptOrReject, senderParty); } } else { replyActivity = activity.CreateReply( $"Could not find a pending request for user {splitMessage[1]}; {errorMessage}"); } } else { replyActivity = activity.CreateReply("User name missing"); } } else { replyActivity = activity.CreateReply("No pending requests"); } } else { Party otherParty = _routingDataManager.GetEngagedCounterpart(senderInConversation); if (otherParty != null) { replyActivity = activity.CreateReply($"You are already engaged in a conversation with {otherParty.ChannelAccount.Name}"); } else { replyActivity = activity.CreateReply("An error occured"); } } wasHandled = true; } } else if (messageInLowerCase.StartsWith(Commands.CommandEndEngagement)) { // End the 1:1 conversation Party senderParty = MessagingUtils.CreateSenderParty(activity); if (await MessageRouterManager.Instance.EndEngagementAsync(senderParty) == false) { replyActivity = activity.CreateReply("Failed to end the engagement"); } wasHandled = true; } /* * NOTE: Either remove these commands or make them unaccessible should you use this * code in production! */ #region Commands for debugging else if (messageInLowerCase.StartsWith(Commands.CommandDeleteAllRoutingData)) { ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl)); await connector.Conversations.ReplyToActivityAsync(activity.CreateReply("Deleting all data...")); _routingDataManager.DeleteAll(); wasHandled = true; } else if (messageInLowerCase.StartsWith(Commands.CommandListAllParties)) { string replyMessage = string.Empty; string parties = string.Empty; foreach (Party userParty in _routingDataManager.GetUserParties()) { parties += userParty.ToString() + "\n"; } if (string.IsNullOrEmpty(parties)) { replyMessage = "No user parties;\n"; } else { replyMessage = "Users:\n" + parties; } parties = string.Empty; foreach (Party botParty in _routingDataManager.GetBotParties()) { parties += botParty.ToString() + "\n"; } if (string.IsNullOrEmpty(parties)) { replyMessage += "No bot parties"; } else { replyMessage += "Bot:\n" + parties; } replyActivity = activity.CreateReply(replyMessage); wasHandled = true; } else if (messageInLowerCase.StartsWith(Commands.CommandListPendingRequests)) { string parties = string.Empty; foreach (Party party in _routingDataManager.GetPendingRequests()) { parties += party.ToString() + "\n"; } if (parties.Length == 0) { parties = "No pending requests"; } replyActivity = activity.CreateReply(parties); wasHandled = true; } else if (messageInLowerCase.StartsWith(Commands.CommandListEngagements)) { string parties = _routingDataManager.EngagementsAsString(); if (string.IsNullOrEmpty(parties)) { replyActivity = activity.CreateReply("No conversations"); } else { replyActivity = activity.CreateReply(parties); } wasHandled = true; } #if DEBUG else if (messageInLowerCase.StartsWith(Commands.CommandListLastMessageRouterResults)) { LocalRoutingDataManager routingDataManager = (MessageRouterManager.Instance.RoutingDataManager as LocalRoutingDataManager); if (routingDataManager != null) { string resultsAsString = routingDataManager.GetLastMessageRouterResults(); replyActivity = activity.CreateReply($"{(string.IsNullOrEmpty(resultsAsString) ? "No results" : resultsAsString)}"); wasHandled = true; } } #endif #endregion Commands for debugging else { replyActivity = activity.CreateReply($"Command \"{messageInLowerCase}\" not recognized"); } } if (replyActivity != null) { ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl)); await connector.Conversations.ReplyToActivityAsync(replyActivity); } return(wasHandled); }
/// <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> /// Checks the given activity for new parties and adds them to the collection, if not /// already there. /// </summary> /// <param name="activity">The activity.</param> public void MakeSurePartiesAreTracked(IActivity activity) { MakeSurePartiesAreTracked( MessagingUtils.CreateSenderParty(activity), MessagingUtils.CreateRecipientParty(activity)); }
/// <summary> /// All messages where the bot was mentioned ("@<bot name>) are checked for possible commands. /// See IBotCommandHandler.cs for more information. /// </summary> public async virtual Task <bool> HandleCommandAsync(Activity activity) { bool wasHandled = false; Activity replyActivity = null; // Sole use of mentions here is unreliable for Skype & Webchat // Therefore just parse the text for any bot id reference if (WasBotAddressedDirectly(activity) || activity.Text.Contains(activity.Recipient.Name) || (!string.IsNullOrEmpty(activity.Text) && activity.Text.StartsWith($"{Commands.CommandKeyword} "))) { //activity.RemoveRecipientMention(); not working here //string message = MessagingUtils.StripMentionsFromMessage(activity); string message = activity.Text.Replace($"@{activity.Recipient.Name}", "").Trim(); if (message.StartsWith($"{Commands.CommandKeyword} ")) { message = message.Remove(0, Commands.CommandKeyword.Length + 1); } switch (message?.ToLower()) { case string s when(s.StartsWith(Commands.CommandEndEngagement)): { // End the 1:1 conversation Party senderParty = MessagingUtils.CreateSenderParty(activity); if (await MessageRouterManager.Instance.EndEngagementAsync(senderParty) == false) { replyActivity = activity.CreateReply("Failed to end the engagement"); } wasHandled = true; break; } case string s when(s.StartsWith(Commands.CommandAddAggregationChannel)): { // Check if the Aggregation Party already exists /* not a specific user, but a channel/conv */ Party aggregationParty = new Party(activity.ServiceUrl, activity.ChannelId, null, activity.Conversation); // Establish the sender's channel/conversation as an aggreated one if not already exists if (_routingDataManager.AddAggregationParty(aggregationParty)) { replyActivity = activity.CreateReply("This channel/conversation is now where the requests are aggregated"); } else { replyActivity = activity.CreateReply("This channel/conversation is already receiving requests"); } wasHandled = true; break; } case string s when(s.StartsWith(Commands.CommandAcceptRequest) || s.StartsWith(Commands.CommandRejectRequest)): { // Accept/reject conversation request bool doAccept = s.StartsWith(Commands.CommandAcceptRequest); Party senderParty = MessagingUtils.CreateSenderParty(activity); if (_routingDataManager.IsAssociatedWithAggregation(senderParty)) { // The party is associated with the aggregation and has the right to accept/reject Party senderInConversation = _routingDataManager.FindEngagedPartyByChannel(senderParty.ChannelId, senderParty.ChannelAccount); replyActivity = await GetEngagementActivity(activity, replyActivity, message, doAccept, senderParty, senderInConversation); wasHandled = true; } break; } case string s when(s.StartsWith(Commands.CommandListOptions)): { replyActivity = await GetAvaialableAgentOptionsCard(activity); wasHandled = true; break; } #region Agent debugging commands // TODO: Remove from production case string s when(s.StartsWith(Commands.CommandDeleteAllRoutingData)): { ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl)); await connector.Conversations.ReplyToActivityAsync(activity.CreateReply("Deleting all data...")); _routingDataManager.DeleteAll(); wasHandled = true; break; } case string s when(s.StartsWith(Commands.CommandListAllParties)): { string replyMessage = string.Empty; string parties = string.Empty; foreach (Party userParty in _routingDataManager.GetUserParties()) { parties += userParty.ToString() + "\n"; } if (string.IsNullOrEmpty(parties)) { replyMessage = "No user parties;\n"; } else { replyMessage = "Users:\n" + parties; } parties = string.Empty; foreach (Party botParty in _routingDataManager.GetBotParties()) { parties += botParty.ToString() + "\n"; } if (string.IsNullOrEmpty(parties)) { replyMessage += "No bot parties"; } else { replyMessage += "Bot:\n" + parties; } replyActivity = activity.CreateReply(replyMessage); wasHandled = true; break; } case string s when(s.StartsWith(Commands.CommandListPendingRequests)): { var attachments = new List <Attachment>(); foreach (Party party in _routingDataManager.GetPendingRequests()) { attachments.Add(MessagingUtils.GetAgentRequestHeroCard(party.ChannelAccount.Name, party.ChannelId, party.ChannelAccount.Id, party)); } replyActivity = activity.CreateReply("No pending requests"); if (attachments.Count > 0) { replyActivity.Text = $"{attachments.Count} Pending requests found:"; replyActivity.AttachmentLayout = AttachmentLayoutTypes.Carousel; replyActivity.Attachments = attachments; } wasHandled = true; break; } case string s when(s.StartsWith(Commands.CommandListEngagements)): { string parties = _routingDataManager.EngagementsAsString(); if (string.IsNullOrEmpty(parties)) { replyActivity = activity.CreateReply("No conversations"); } else { replyActivity = activity.CreateReply(parties); } wasHandled = true; break; } #if DEBUG case string s when(s.StartsWith(Commands.CommandListLastMessageRouterResults)): { LocalRoutingDataManager routingDataManager = (MessageRouterManager.Instance.RoutingDataManager as LocalRoutingDataManager); if (routingDataManager != null) { string resultsAsString = routingDataManager.GetLastMessageRouterResults(); replyActivity = activity.CreateReply($"{(string.IsNullOrEmpty(resultsAsString) ? "No results" : resultsAsString)}"); wasHandled = true; } break; } #endif #endregion default: replyActivity = activity.CreateReply($"Command \"{message}\" not recognized"); break; } if (replyActivity != null) { ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl)); await connector.Conversations.ReplyToActivityAsync(replyActivity); } } return(wasHandled); }