Skip to content
This repository was archived by the owner on Dec 4, 2023. It is now read-only.

Add 'ExpectReplies' as the list of options for Delivery Mode #937

Merged
merged 2 commits into from
Feb 3, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<InvokeResponse> 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<String, AppCredentials> getCredentialsCache() {
}

/**
* Get the ConnectorClient cache. For unit testing.
* Get the ConnectorClient cache. FOR UNIT TESTING.
*
* @return The ConnectorClient cache.
*/
protected Map<String, ConnectorClient> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Activity> 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<Activity> getBufferedReplyActivities() {
return bufferedReplyActivities;
}

/**
* Sends a message activity to the sender of the incoming activity.
*
@@ -385,21 +396,46 @@ public CompletableFuture<ResourceResponse[]> sendActivities(List<Activity> activ
private CompletableFuture<ResourceResponse[]> sendActivitiesThroughAdapter(
List<Activity> 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);
}

if (sentNonTraceActivity) {
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<ResourceResponse[]> sendActivitiesThroughCallbackPipeline(
Original file line number Diff line number Diff line change
@@ -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<Activity> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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<Activity> 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<Activity> 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<Activity> 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<Activity> withActivities) {
activities = withActivities;
}
}