diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java index 9cae8c24a..a587516c5 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/BotFrameworkAdapter.java @@ -37,6 +37,8 @@ import com.microsoft.bot.schema.ConversationParameters; import com.microsoft.bot.schema.ConversationReference; import com.microsoft.bot.schema.ConversationsResult; +import com.microsoft.bot.schema.DeliveryModes; +import com.microsoft.bot.schema.ExpectedReplies; import com.microsoft.bot.schema.ResourceResponse; import com.microsoft.bot.schema.Serialization; import com.microsoft.bot.schema.TokenExchangeState; @@ -483,10 +485,20 @@ public CompletableFuture processActivity( context.getTurnState().add(CONNECTOR_CLIENT_KEY, connectorClient); return runPipeline(context, callback); }) - - // Handle Invoke scenarios, which deviate from the request/response model in - // that the Bot will return a specific body and return code. .thenCompose(result -> { + // Handle ExpectedReplies scenarios where the all the activities have been + // buffered and sent back at once in an invoke response. + if (DeliveryModes.fromString( + context.getActivity().getDeliveryMode()) == DeliveryModes.EXPECT_REPLIES + ) { + return CompletableFuture.completedFuture(new InvokeResponse( + HttpURLConnection.HTTP_OK, + new ExpectedReplies(context.getBufferedReplyActivities()) + )); + } + + // Handle Invoke scenarios, which deviate from the request/response model in + // that the Bot will return a specific body and return code. if (activity.isType(ActivityTypes.INVOKE)) { Activity invokeResponse = context.getTurnState().get(INVOKE_RESPONSE_KEY); if (invokeResponse == null) { @@ -1581,11 +1593,23 @@ protected Map getCredentialsCache() { } /** - * Get the ConnectorClient cache. For unit testing. + * Get the ConnectorClient cache. FOR UNIT TESTING. * * @return The ConnectorClient cache. */ protected Map getConnectorClientCache() { return Collections.unmodifiableMap(connectorClients); } + + /** + * Inserts a ConnectorClient into the cache. FOR UNIT TESTING ONLY. + * @param serviceUrl The service url + * @param appId The app did + * @param scope The scope + * @param client The ConnectorClient to insert. + */ + protected void addConnectorClientToCache(String serviceUrl, String appId, String scope, ConnectorClient client) { + String key = BotFrameworkAdapter.keyForConnectorClient(serviceUrl, appId, scope); + connectorClients.put(key, client); + } } diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java index 2d53e94c3..e64022c1c 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TurnContextImpl.java @@ -7,6 +7,7 @@ import com.microsoft.bot.schema.Activity; import com.microsoft.bot.schema.ActivityTypes; import com.microsoft.bot.schema.ConversationReference; +import com.microsoft.bot.schema.DeliveryModes; import com.microsoft.bot.schema.InputHints; import com.microsoft.bot.schema.ResourceResponse; import java.util.Locale; @@ -39,6 +40,8 @@ public class TurnContextImpl implements TurnContext, AutoCloseable { */ private final Activity activity; + private List bufferedReplyActivities = new ArrayList<>(); + /** * Response handlers for send activity operations. */ @@ -217,6 +220,14 @@ public void setLocale(String withLocale) { } } + /** + * Gets a list of activities to send when `context.Activity.DeliveryMode == 'expectReplies'. + * @return A list of activities. + */ + public List getBufferedReplyActivities() { + return bufferedReplyActivities; + } + /** * Sends a message activity to the sender of the incoming activity. * @@ -385,12 +396,21 @@ public CompletableFuture sendActivities(List activ private CompletableFuture sendActivitiesThroughAdapter( List activities ) { - return adapter.sendActivities(this, activities).thenApply(responses -> { + if (DeliveryModes.fromString(getActivity().getDeliveryMode()) == DeliveryModes.EXPECT_REPLIES) { + ResourceResponse[] responses = new ResourceResponse[activities.size()]; boolean sentNonTraceActivity = false; for (int index = 0; index < responses.length; index++) { Activity sendActivity = activities.get(index); - sendActivity.setId(responses[index].getId()); + bufferedReplyActivities.add(sendActivity); + + // Ensure the TurnState has the InvokeResponseKey, since this activity + // is not being sent through the adapter, where it would be added to TurnState. + if (activity.isType(ActivityTypes.INVOKE_RESPONSE)) { + getTurnState().add(BotFrameworkAdapter.INVOKE_RESPONSE_KEY, activity); + } + + responses[index] = new ResourceResponse(); sentNonTraceActivity |= !sendActivity.isType(ActivityTypes.TRACE); } @@ -398,8 +418,24 @@ private CompletableFuture sendActivitiesThroughAdapter( responded = true; } - return responses; - }); + return CompletableFuture.completedFuture(responses); + } else { + return adapter.sendActivities(this, activities).thenApply(responses -> { + boolean sentNonTraceActivity = false; + + for (int index = 0; index < responses.length; index++) { + Activity sendActivity = activities.get(index); + sendActivity.setId(responses[index].getId()); + sentNonTraceActivity |= !sendActivity.isType(ActivityTypes.TRACE); + } + + if (sentNonTraceActivity) { + responded = true; + } + + return responses; + }); + } } private CompletableFuture sendActivitiesThroughCallbackPipeline( diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java index 4c68ed00f..58a72a3e7 100644 --- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java +++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/BotFrameworkAdapterTests.java @@ -19,12 +19,18 @@ import com.microsoft.bot.connector.authentication.SimpleChannelProvider; import com.microsoft.bot.connector.authentication.SimpleCredentialProvider; import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; import com.microsoft.bot.schema.CallerIdConstants; import com.microsoft.bot.schema.ConversationAccount; import com.microsoft.bot.schema.ConversationParameters; import com.microsoft.bot.schema.ConversationReference; import com.microsoft.bot.schema.ConversationResourceResponse; +import com.microsoft.bot.schema.DeliveryModes; +import com.microsoft.bot.schema.ExpectedReplies; +import com.microsoft.bot.schema.ResourceResponse; +import java.net.HttpURLConnection; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.Assert; import org.junit.Test; @@ -364,4 +370,95 @@ private static void getConnectorClientAndAssertValues( ); Assert.assertEquals("Unexpected base url", expectedUrl, client.baseUrl()); } + + @Test + public void DeliveryModeExpectReplies() { + BotFrameworkAdapter adapter = new BotFrameworkAdapter(new SimpleCredentialProvider()); + + MockConnectorClient mockConnector = new MockConnectorClient("Windows/3.1", new MockAppCredentials("awesome")); + adapter.addConnectorClientToCache("http://tempuri.org/whatever", null, null, mockConnector); + + BotCallbackHandler callback = turnContext -> { + turnContext.sendActivity(MessageFactory.text("activity 1")).join(); + turnContext.sendActivity(MessageFactory.text("activity 2")).join(); + turnContext.sendActivity(MessageFactory.text("activity 3")).join(); + return CompletableFuture.completedFuture(null); + }; + + Activity inboundActivity = new Activity() {{ + setType(ActivityTypes.MESSAGE); + setChannelId(Channels.EMULATOR); + setServiceUrl("http://tempuri.org/whatever"); + setDeliveryMode(DeliveryModes.EXPECT_REPLIES.toString()); + setText("hello world"); + }}; + + InvokeResponse invokeResponse = adapter.processActivity((String) null, inboundActivity, callback).join(); + + Assert.assertEquals((int) HttpURLConnection.HTTP_OK, invokeResponse.getStatus()); + List activities = ((ExpectedReplies)invokeResponse.getBody()).getActivities(); + Assert.assertEquals(3, activities.size()); + Assert.assertEquals("activity 1", activities.get(0).getText()); + Assert.assertEquals("activity 2", activities.get(1).getText()); + Assert.assertEquals("activity 3", activities.get(2).getText()); + Assert.assertEquals(0, ((MemoryConversations) mockConnector.getConversations()).getSentActivities().size()); + } + + @Test + public void DeliveryModeNormal() { + BotFrameworkAdapter adapter = new BotFrameworkAdapter(new SimpleCredentialProvider()); + + MockConnectorClient mockConnector = new MockConnectorClient("Windows/3.1", new MockAppCredentials("awesome")); + adapter.addConnectorClientToCache("http://tempuri.org/whatever", null, null, mockConnector); + + BotCallbackHandler callback = turnContext -> { + turnContext.sendActivity(MessageFactory.text("activity 1")).join(); + turnContext.sendActivity(MessageFactory.text("activity 2")).join(); + turnContext.sendActivity(MessageFactory.text("activity 3")).join(); + return CompletableFuture.completedFuture(null); + }; + + Activity inboundActivity = new Activity() {{ + setType(ActivityTypes.MESSAGE); + setChannelId(Channels.EMULATOR); + setServiceUrl("http://tempuri.org/whatever"); + setDeliveryMode(DeliveryModes.NORMAL.toString()); + setText("hello world"); + setConversation(new ConversationAccount("conversationId")); + }}; + + InvokeResponse invokeResponse = adapter.processActivity((String) null, inboundActivity, callback).join(); + + Assert.assertNull(invokeResponse); + Assert.assertEquals(3, ((MemoryConversations) mockConnector.getConversations()).getSentActivities().size()); + } + + // should be same as DeliverModes.NORMAL + @Test + public void DeliveryModeNull() { + BotFrameworkAdapter adapter = new BotFrameworkAdapter(new SimpleCredentialProvider()); + + MockConnectorClient mockConnector = new MockConnectorClient("Windows/3.1", new MockAppCredentials("awesome")); + adapter.addConnectorClientToCache("http://tempuri.org/whatever", null, null, mockConnector); + + BotCallbackHandler callback = turnContext -> { + turnContext.sendActivity(MessageFactory.text("activity 1")).join(); + turnContext.sendActivity(MessageFactory.text("activity 2")).join(); + turnContext.sendActivity(MessageFactory.text("activity 3")).join(); + return CompletableFuture.completedFuture(null); + }; + + Activity inboundActivity = new Activity() {{ + setType(ActivityTypes.MESSAGE); + setChannelId(Channels.EMULATOR); + setServiceUrl("http://tempuri.org/whatever"); + setText("hello world"); + setConversation(new ConversationAccount("conversationId")); + }}; + + InvokeResponse invokeResponse = adapter.processActivity((String) null, inboundActivity, callback).join(); + + Assert.assertNull(invokeResponse); + Assert.assertEquals(3, ((MemoryConversations) mockConnector.getConversations()).getSentActivities().size()); + } } diff --git a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/DeliveryModes.java b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/DeliveryModes.java index 52c78f507..b69d9768d 100644 --- a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/DeliveryModes.java +++ b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/DeliveryModes.java @@ -11,14 +11,19 @@ */ public enum DeliveryModes { /** - * Enum value normal. + * The mode value for normal delivery modes. */ NORMAL("normal"), /** - * Enum value notification. + * The mode value for notification delivery modes. */ - NOTIFICATION("notification"); + NOTIFICATION("notification"), + + /** + * The value for expected replies delivery modes. + */ + EXPECT_REPLIES("expectReplies"); /** * The actual serialized value for a DeliveryModes instance. diff --git a/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ExpectedReplies.java b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ExpectedReplies.java new file mode 100644 index 000000000..dd7e2a0fe --- /dev/null +++ b/libraries/bot-schema/src/main/java/com/microsoft/bot/schema/ExpectedReplies.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.schema; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Arrays; +import java.util.List; + +/** + * Replies in response to DeliveryModes.EXPECT_REPLIES. + */ +public class ExpectedReplies { + @JsonProperty(value = "activities") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List activities; + + /** + * Create an instance of ExpectReplies. + */ + public ExpectedReplies() { + + } + + /** + * Create an instance of ExpectReplies. + * @param withActivities The collection of activities that conforms to the + * ExpectedREplies schema. + */ + public ExpectedReplies(List withActivities) { + activities = withActivities; + } + + /** + * Create an instance of ExpectReplies. + * @param withActivities The array of activities that conforms to the + * ExpectedREplies schema. + */ + public ExpectedReplies(Activity... withActivities) { + this(Arrays.asList(withActivities)); + } + + /** + * Gets collection of Activities that conforms to the ExpectedReplies schema. + * @return The collection of activities that conforms to the ExpectedREplies schema. + */ + public List getActivities() { + return activities; + } + + /** + * Sets collection of Activities that conforms to the ExpectedReplies schema. + * @param withActivities The collection of activities that conforms to the + * ExpectedREplies schema. + */ + public void setActivities(List withActivities) { + activities = withActivities; + } +}