/// <summary> /// Tries to initiate a connection (1:1 conversation) by creating a request on behalf of /// the given requestor. This method does nothing, if a request for the same user already exists. /// </summary> /// <param name="requestor">The requestor conversation reference.</param> /// <param name="rejectConnectionRequestIfNoAggregationChannel"> /// If true, will reject all requests, if there is no aggregation channel.</param> /// <returns>The result of the operation: /// - ConnectionRequestResultType.Created, /// - ConnectionRequestResultType.AlreadyExists, /// - ConnectionRequestResultType.NotSetup or /// - ConnectionRequestResultType.Error (see the error message for more details). /// </returns> public virtual ConnectionRequestResult CreateConnectionRequest( ConversationReference requestor, bool rejectConnectionRequestIfNoAggregationChannel = false) { if (requestor == null) { throw new ArgumentNullException("Requestor missing"); } ConnectionRequestResult createConnectionRequestResult = null; RoutingDataManager.AddConversationReference(requestor); ConnectionRequest connectionRequest = new ConnectionRequest(requestor); if (RoutingDataManager.IsAssociatedWithAggregation(requestor)) { createConnectionRequestResult = new ConnectionRequestResult() { Type = ConnectionRequestResultType.Error, ErrorMessage = $"The given ConversationReference ({RoutingDataManager.GetChannelAccount(requestor)?.Name}) is associated with aggregation and hence invalid to request a connection" }; } else { createConnectionRequestResult = RoutingDataManager.AddConnectionRequest( connectionRequest, rejectConnectionRequestIfNoAggregationChannel); } return(createConnectionRequestResult); }
/// <summary> /// Creates a new message activity and populates it based on the given arguments. /// </summary> /// <param name="sender">The channel account of the sender.</param> /// <param name="recipient">The conversation reference of the recipient.</param> /// <param name="message">The message content.</param> /// <returns>A newly created message activity.</returns> public static IMessageActivity CreateMessageActivity( ChannelAccount sender, ConversationReference recipient, string message) { IMessageActivity messageActivity = Activity.CreateMessageActivity(); if (sender != null) { messageActivity.From = sender; } if (recipient != null) { if (recipient.Conversation != null) { messageActivity.Conversation = recipient.Conversation; } ChannelAccount recipientChannelAccount = RoutingDataManager.GetChannelAccount(recipient); if (recipientChannelAccount != null) { messageActivity.Recipient = recipientChannelAccount; } } messageActivity.Text = message; return(messageActivity); }
/// <summary> /// Creates a compact card for accepting/rejecting multiple requests. /// </summary> /// <param name="connectionRequests">The connection requests.</param> /// <param name="doAccept">If true, will create an accept card. If false, will create a reject card.</param> /// <param name="botName">The name of the bot (optional).</param> /// <returns>The newly created card.</returns> public static HeroCard CreateMultiConnectionRequestCard( IList <ConnectionRequest> connectionRequests, bool doAccept, string botName = null) { HeroCard card = new HeroCard() { Title = (doAccept ? Strings.AcceptConnectionRequestsCardTitle : Strings.RejectConnectionRequestCardTitle), Subtitle = (doAccept ? Strings.AcceptConnectionRequestsCardInstructions : Strings.RejectConnectionRequestsCardInstructions), }; card.Buttons = new List <CardAction>(); if (!doAccept && connectionRequests.Count > 1) { card.Buttons.Add(new CardAction() { Title = Strings.RejectAll, Type = ActionTypes.ImBack, Value = new Command(Commands.RejectRequest, new string[] { Command.CommandParameterAll }, botName).ToString() }); } foreach (ConnectionRequest connectionRequest in connectionRequests) { ChannelAccount requestorChannelAccount = RoutingDataManager.GetChannelAccount(connectionRequest.Requestor, out bool isBot); if (requestorChannelAccount == null) { throw new ArgumentNullException("The channel account of the requestor is null"); } string requestorChannelAccountName = string.IsNullOrEmpty(requestorChannelAccount.Name) ? StringConstants.NoUserNamePlaceholder : requestorChannelAccount.Name; string requestorChannelId = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(connectionRequest.Requestor.ChannelId); string requestorChannelAccountId = requestorChannelAccount.Id; Command command = Command.CreateAcceptOrRejectConnectionRequestCommand(connectionRequest, doAccept, botName); card.Buttons.Add(new CardAction() { Title = string.Format( Strings.RequestorDetailsItem, requestorChannelAccountName, requestorChannelId, requestorChannelAccountId), Type = ActionTypes.ImBack, Value = command.ToString() }); } return(card); }
/// <summary> /// Routes the message in the given activity, if the sender is connected in a conversation. /// </summary> /// <param name="activity">The activity to handle.</param> /// <param name="addNameToMessage">If true, will add the name of the sender to the beginning of the message.</param> /// <returns>The result of the operation: /// - MessageRouterResultType.NoActionTaken, if no routing rule for the sender is found OR /// - MessageRouterResultType.OK, if the message was routed successfully OR /// - MessageRouterResultType.FailedToForwardMessage in case of an error (see the error message). /// </returns> public virtual async Task <MessageRoutingResult> RouteMessageIfSenderIsConnectedAsync( IMessageActivity activity, bool addNameToMessage = true) { ConversationReference sender = CreateSenderConversationReference(activity); Connection connection = RoutingDataManager.FindConnection(sender); MessageRoutingResult messageRoutingResult = new MessageRoutingResult() { Type = MessageRoutingResultType.NoActionTaken, Connection = connection }; if (connection != null) { ConversationReference recipient = RoutingDataManager.Match(sender, connection.ConversationReference1) ? connection.ConversationReference2 : connection.ConversationReference1; if (recipient != null) { string message = activity.Text; if (addNameToMessage) { string senderName = RoutingDataManager.GetChannelAccount(sender).Name; if (!string.IsNullOrWhiteSpace(senderName)) { message = $"{senderName}: {message}"; } } ResourceResponse resourceResponse = await SendMessageAsync(recipient, message); if (resourceResponse != null) { messageRoutingResult.Type = MessageRoutingResultType.MessageRouted; if (!RoutingDataManager.UpdateTimeSinceLastActivity(connection)) { Logger.Log("Failed to update the time since the last activity property of the connection"); } } else { messageRoutingResult.Type = MessageRoutingResultType.FailedToRouteMessage; messageRoutingResult.ErrorMessage = $"Failed to forward the message to the recipient"; } } else { messageRoutingResult.Type = MessageRoutingResultType.Error; messageRoutingResult.ErrorMessage = "Failed to find the recipient to forward the message to"; } } return(messageRoutingResult); }
public static Command CreateAcceptOrRejectConnectionRequestCommand( ConnectionRequest connectionRequest, bool doAccept, string botName = null) { ChannelAccount requestorChannelAccount = RoutingDataManager.GetChannelAccount(connectionRequest.Requestor); return(new Command( doAccept ? Commands.AcceptRequest : Commands.RejectRequest, new string[] { requestorChannelAccount?.Id, connectionRequest.Requestor.Conversation?.Id }, botName)); }
/// <summary> /// Sends the given message to the given recipient. /// </summary> /// <param name="recipient">The conversation reference of the recipient.</param> /// <param name="messageActivity">The message activity to send.</param> /// <returns>A valid resource response instance, if successful. Null in case of an error.</returns> public virtual async Task <ResourceResponse> SendMessageAsync( ConversationReference recipient, IMessageActivity messageActivity) { if (recipient == null) { Logger.Log("The conversation reference is null"); return(null); } // We need the bot identity in the SAME CHANNEL/CONVERSATION as the RECIPIENT - // Otherwise, the platform (e.g. Slack) will reject the incoming message as it does not // recognize the sender ConversationReference botInstance = RoutingDataManager.FindBotInstanceForRecipient(recipient); if (botInstance == null || botInstance.Bot == null) { Logger.Log("Failed to find the bot instance"); return(null); } messageActivity.From = botInstance.Bot; messageActivity.Recipient = RoutingDataManager.GetChannelAccount(recipient); // Make sure the message activity contains a valid conversation ID if (messageActivity.Conversation == null) { messageActivity.Conversation = recipient.Conversation; } ConnectorClientMessageBundle bundle = new ConnectorClientMessageBundle( recipient.ServiceUrl, messageActivity, _microsoftAppCredentials); ResourceResponse resourceResponse = null; try { resourceResponse = await bundle.ConnectorClient.Conversations.SendToConversationAsync( (Activity)bundle.MessageActivity); } catch (UnauthorizedAccessException e) { Logger.Log($"Failed to send message: {e.Message}"); } catch (Exception e) { Logger.Log($"Failed to send message: {e.Message}"); } return(resourceResponse); }
/// <summary> /// Creates a large connection request card. /// </summary> /// <param name="connectionRequest">The connection request.</param> /// <param name="botName">The name of the bot (optional).</param> /// <returns>A newly created request card.</returns> public static HeroCard CreateConnectionRequestCard( ConnectionRequest connectionRequest, string botName = null) { if (connectionRequest == null || connectionRequest.Requestor == null) { throw new ArgumentNullException("The connection request or the conversation reference of the requestor is null"); } ChannelAccount requestorChannelAccount = RoutingDataManager.GetChannelAccount(connectionRequest.Requestor); if (requestorChannelAccount == null) { throw new ArgumentNullException("The channel account of the requestor is null"); } string requestorChannelAccountName = string.IsNullOrEmpty(requestorChannelAccount.Name) ? StringConstants.NoUserNamePlaceholder : requestorChannelAccount.Name; string requestorChannelId = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(connectionRequest.Requestor.ChannelId); Command acceptCommand = Command.CreateAcceptOrRejectConnectionRequestCommand(connectionRequest, true, botName); Command rejectCommand = Command.CreateAcceptOrRejectConnectionRequestCommand(connectionRequest, false, botName); HeroCard card = new HeroCard() { Title = Strings.ConnectionRequestTitle, Subtitle = string.Format(Strings.RequestorDetailsTitle, requestorChannelAccountName, requestorChannelId), Text = string.Format(Strings.AcceptRejectConnectionHint, acceptCommand.ToString(), rejectCommand.ToString()), Buttons = new List <CardAction>() { new CardAction() { Title = Strings.AcceptButtonTitle, Type = ActionTypes.ImBack, Value = acceptCommand.ToString() }, new CardAction() { Title = Strings.RejectButtonTitle, Type = ActionTypes.ImBack, Value = rejectCommand.ToString() } } }; return(card); }
/// <summary> /// Checks the given activity and determines whether the message was addressed directly to /// the bot or not. /// /// Note: Only mentions are inspected at the moment. /// </summary> /// <param name="messageActivity">The message activity.</param> /// <param name="strict">Use false for channels that do not properly support mentions.</param> /// <returns>True, if the message was address directly to the bot. False otherwise.</returns> public bool WasBotAddressedDirectly(IMessageActivity messageActivity, bool strict = true) { bool botWasMentioned = false; if (strict) { Mention[] mentions = messageActivity.GetMentions(); foreach (Mention mention in mentions) { foreach (ConversationReference bot in _messageRouter.RoutingDataManager.GetBotInstances()) { if (mention.Mentioned.Id.Equals(RoutingDataManager.GetChannelAccount(bot).Id)) { botWasMentioned = true; break; } } } } else { // Here we assume the message starts with the bot name, for instance: // // * "@<BOT NAME>..." // * "<BOT NAME>: ..." string botName = messageActivity.Recipient?.Name; string message = messageActivity.Text?.Trim(); if (!string.IsNullOrEmpty(botName) && !string.IsNullOrEmpty(message) && message.Length > botName.Length) { try { message = message.Remove(botName.Length + 1, message.Length - botName.Length - 1); botWasMentioned = message.Contains(botName); } catch (ArgumentOutOfRangeException e) { System.Diagnostics.Debug.WriteLine($"Failed to check if bot was mentioned: {e.Message}"); } } } return(botWasMentioned); }
/// <summary> /// Tries to resolve the name of the given user/bot instance. /// Will fallback to ID, if no name specified. /// </summary> /// <param name="conversationReference">The conversation reference, whose details to resolve.</param> /// <returns>The name or the ID of the given user/bot instance.</returns> protected virtual string GetNameOrId(ConversationReference conversationReference) { if (conversationReference != null) { ChannelAccount channelAccount = RoutingDataManager.GetChannelAccount(conversationReference); if (channelAccount != null) { if (!string.IsNullOrWhiteSpace(channelAccount.Name)) { return(channelAccount.Name); } if (!string.IsNullOrWhiteSpace(channelAccount.Id)) { return(channelAccount.Id); } } } return(StringConstants.NoNameOrId); }
/// <summary> /// Handles the given connection request result. /// </summary> /// <param name="connectionRequestResult">The result to handle.</param> /// <returns>True, if the result was handled. False, if no action was taken.</returns> protected virtual async Task <bool> HandleConnectionRequestResultAsync( ConnectionRequestResult connectionRequestResult) { ConnectionRequest connectionRequest = connectionRequestResult?.ConnectionRequest; if (connectionRequest == null || connectionRequest.Requestor == null) { System.Diagnostics.Debug.WriteLine("No client to inform about the connection request result"); return(false); } switch (connectionRequestResult.Type) { case ConnectionRequestResultType.Created: foreach (ConversationReference aggregationChannel in _messageRouter.RoutingDataManager.GetAggregationChannels()) { ConversationReference botConversationReference = _messageRouter.RoutingDataManager.FindConversationReference( aggregationChannel.ChannelId, aggregationChannel.Conversation.Id, null, true); if (botConversationReference != null) { IMessageActivity messageActivity = Activity.CreateMessageActivity(); messageActivity.Conversation = aggregationChannel.Conversation; messageActivity.Recipient = RoutingDataManager.GetChannelAccount(aggregationChannel); messageActivity.Attachments = new List <Attachment> { CommandCardFactory.CreateConnectionRequestCard( connectionRequest, RoutingDataManager.GetChannelAccount( botConversationReference)?.Name).ToAttachment() }; await _messageRouter.SendMessageAsync(aggregationChannel, messageActivity); } } await _messageRouter.SendMessageAsync( connectionRequest.Requestor, Strings.NotifyClientWaitForRequestHandling); return(true); case ConnectionRequestResultType.AlreadyExists: await _messageRouter.SendMessageAsync( connectionRequest.Requestor, Strings.NotifyClientDuplicateRequest); return(true); case ConnectionRequestResultType.Rejected: if (connectionRequestResult.Rejecter != null) { await _messageRouter.SendMessageAsync( connectionRequestResult.Rejecter, string.Format(Strings.NotifyOwnerRequestRejected, GetNameOrId(connectionRequest.Requestor))); } await _messageRouter.SendMessageAsync( connectionRequest.Requestor, Strings.NotifyClientRequestRejected); return(true); case ConnectionRequestResultType.NotSetup: await _messageRouter.SendMessageAsync( connectionRequest.Requestor, Strings.NoAgentsAvailable); return(true); case ConnectionRequestResultType.Error: if (connectionRequestResult.Rejecter != null) { await _messageRouter.SendMessageAsync( connectionRequestResult.Rejecter, string.Format(Strings.ConnectionRequestResultErrorWithResult, connectionRequestResult.ErrorMessage)); } return(true); default: break; } return(false); }
/// <summary> /// Tries to establish a connection (1:1 chat) between the two given parties. /// /// Note that the conversation owner will have a new separate conversation reference in the created /// conversation, if a new direct conversation is created. /// </summary> /// <param name="conversationReference1">The conversation reference who owns the conversation (e.g. customer service agent).</param> /// <param name="conversationReference2">The other conversation reference 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) conversation /// reference are routed. /// /// Note that this will result in the conversation owner having a new separate conversation /// reference in the created connection (for the new direct conversation). /// </param> /// <returns> /// The result of the operation: /// - ConnectionResultType.Connected, /// - ConnectionResultType.Error (see the error message for more details). /// </returns> public virtual async Task <ConnectionResult> ConnectAsync( ConversationReference conversationReference1, ConversationReference conversationReference2, bool createNewDirectConversation) { if (conversationReference1 == null || conversationReference2 == null) { throw new ArgumentNullException( $"Neither of the arguments ({nameof(conversationReference1)}, {nameof(conversationReference2)}) can be null"); } ConversationReference botInstance = RoutingDataManager.FindConversationReference( conversationReference1.ChannelId, conversationReference1.Conversation.Id, null, true); if (botInstance == null) { return(new ConnectionResult() { Type = ConnectionResultType.Error, ErrorMessage = "Failed to find the bot instance" }); } ConversationResourceResponse conversationResourceResponse = null; if (createNewDirectConversation) { ChannelAccount conversationReference1ChannelAccount = RoutingDataManager.GetChannelAccount( conversationReference1, out bool conversationReference1IsBot); ConnectorClient connectorClient = new ConnectorClient( new Uri(conversationReference1.ServiceUrl), _microsoftAppCredentials); try { conversationResourceResponse = await connectorClient.Conversations.CreateDirectConversationAsync( botInstance.Bot, conversationReference1ChannelAccount); } catch (Exception e) { Logger.Log($"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); conversationReference1 = new ConversationReference( null, conversationReference1IsBot ? null : conversationReference1ChannelAccount, conversationReference1IsBot ? conversationReference1ChannelAccount : null, directConversationAccount, conversationReference1.ChannelId, conversationReference1.ServiceUrl); RoutingDataManager.AddConversationReference(conversationReference1); RoutingDataManager.AddConversationReference(new ConversationReference( null, null, botInstance.Bot, directConversationAccount, botInstance.ChannelId, botInstance.ServiceUrl)); } } Connection connection = new Connection(conversationReference1, conversationReference2); ConnectionResult connectResult = RoutingDataManager.ConnectAndRemoveConnectionRequest(connection, conversationReference2); connectResult.ConversationResourceResponse = conversationResourceResponse; return(connectResult); }
/// <summary> /// Tries to accept/reject a pending connection request. /// </summary> /// <param name="messageRouter">The message router.</param> /// <param name="messageRouterResultHandler">The message router result handler.</param> /// <param name="sender">The sender party (accepter/rejecter).</param> /// <param name="doAccept">If true, will try to accept the request. If false, will reject.</param> /// <param name="requestorChannelAccountId">The channel account ID of the user/bot whose request to accept/reject.</param> /// <param name="requestorConversationAccountId">The conversation account ID of the user/bot whose request to accept/reject.</param> /// <returns>The result.</returns> public async Task <AbstractMessageRouterResult> AcceptOrRejectRequestAsync( MessageRouter messageRouter, MessageRouterResultHandler messageRouterResultHandler, ConversationReference sender, bool doAccept, ChannelAccount requestorChannelAccountId, ConversationAccount requestorConversationAccountId) { AbstractMessageRouterResult messageRouterResult = new ConnectionRequestResult() { Type = ConnectionRequestResultType.Error }; ConversationReference requestor = new ConversationReference( null, requestorChannelAccountId, null, requestorConversationAccountId); ConnectionRequest connectionRequest = messageRouter.RoutingDataManager.FindConnectionRequest(requestor); if (connectionRequest == null) { // Try bot requestor.Bot = requestor.User; requestor.User = null; connectionRequest = messageRouter.RoutingDataManager.FindConnectionRequest(requestor); } if (connectionRequest != null) { Connection connection = null; if (sender != null) { connection = messageRouter.RoutingDataManager.FindConnection(sender); } ConversationReference senderInConnection = null; ConversationReference counterpart = null; if (connection != null && connection.ConversationReference1 != null) { if (RoutingDataManager.Match(sender, connection.ConversationReference1)) { senderInConnection = connection.ConversationReference1; counterpart = connection.ConversationReference2; } else { senderInConnection = connection.ConversationReference2; counterpart = connection.ConversationReference1; } } if (doAccept) { if (senderInConnection != null) { // The sender (accepter/rejecter) is ALREADY connected to another party if (counterpart != null) { messageRouterResult.ErrorMessage = string.Format( Strings.AlreadyConnectedWithUser, RoutingDataManager.GetChannelAccount(counterpart)?.Name); } else { messageRouterResult.ErrorMessage = Strings.ErrorOccured; } } else { bool createNewDirectConversation = (_noDirectConversationsWithChannels == null || !(_noDirectConversationsWithChannels.Contains(sender.ChannelId.ToLower()))); // Try to accept messageRouterResult = await messageRouter.ConnectAsync( sender, connectionRequest.Requestor, createNewDirectConversation); } } else { // Note: Rejecting is OK even if the sender is alreay connected messageRouterResult = messageRouter.RejectConnectionRequest(connectionRequest.Requestor, sender); } } else { messageRouterResult.ErrorMessage = Strings.FailedToFindPendingRequest; } return(messageRouterResult); }