diff --git a/etc/bot-checkstyle.xml b/etc/bot-checkstyle.xml
index da9b3f275..963f7ccf5 100644
--- a/etc/bot-checkstyle.xml
+++ b/etc/bot-checkstyle.xml
@@ -86,6 +86,9 @@
+
+
+
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ActivityHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ActivityHandler.java
index 60987f678..58b32a75f 100644
--- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ActivityHandler.java
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ActivityHandler.java
@@ -87,6 +87,9 @@ public CompletableFuture onTurn(TurnContext turnContext) {
case ActivityTypes.INSTALLATION_UPDATE:
return onInstallationUpdate(turnContext);
+ case ActivityTypes.END_OF_CONVERSATION:
+ return onEndOfConversationActivity(turnContext);
+
case ActivityTypes.TYPING:
return onTypingActivity(turnContext);
@@ -565,6 +568,17 @@ protected CompletableFuture onInstallationUpdateRemove(TurnContext turnCon
return CompletableFuture.completedFuture(null);
}
+ /**
+ * Override this in a derived class to provide logic specific to
+ * ActivityTypes.END_OF_CONVERSATION activities.
+ *
+ * @param turnContext The context object for this turn.
+ * @return A task that represents the work queued to execute.
+ */
+ protected CompletableFuture onEndOfConversationActivity(TurnContext turnContext) {
+ return CompletableFuture.completedFuture(null);
+ }
+
/**
* Override this in a derived class to provide logic specific to
* ActivityTypes.Typing activities.
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 60e0e8f44..eb8ed6491 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
@@ -26,6 +26,7 @@
import com.microsoft.bot.connector.authentication.MicrosoftAppCredentials;
import com.microsoft.bot.connector.authentication.MicrosoftGovernmentAppCredentials;
import com.microsoft.bot.connector.authentication.SimpleCredentialProvider;
+import com.microsoft.bot.connector.authentication.SkillValidation;
import com.microsoft.bot.connector.rest.RestConnectorClient;
import com.microsoft.bot.connector.rest.RestOAuthClient;
import com.microsoft.bot.schema.AadResourceUrls;
@@ -438,7 +439,10 @@ public CompletableFuture processActivity(ClaimsIdentity identity
// The OAuthScope is also stored on the TurnState to get the correct
// AppCredentials if fetching a token is required.
- String scope = getBotFrameworkOAuthScope();
+ String scope = SkillValidation.isSkillClaim(identity.claims())
+ ? String.format("%s/.default", JwtTokenValidation.getAppIdFromClaims(identity.claims()))
+ : getBotFrameworkOAuthScope();
+
context.getTurnState().add(OAUTH_SCOPE_KEY, scope);
pipelineResult = createConnectorClient(activity.getServiceUrl(), identity, scope)
@@ -493,6 +497,13 @@ private CompletableFuture generateCallerId(ClaimsIdentity claimsIdentity
return null;
}
+ // Is the activity from another bot?
+ if (SkillValidation.isSkillClaim(claimsIdentity.claims())) {
+ return String.format("%s%s",
+ CallerIdConstants.BOT_TO_BOT_PREFIX,
+ JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()));
+ }
+
// Is the activity from Public Azure?
if (channelProvider == null || channelProvider.isPublicAzure()) {
return CallerIdConstants.PUBLIC_AZURE_CHANNEL;
@@ -1133,7 +1144,13 @@ public CompletableFuture createConnectorClient(String serviceUr
}
if (botAppIdClaim != null) {
- String scope = getBotFrameworkOAuthScope();
+ String scope = audience;
+
+ if (StringUtils.isBlank(audience)) {
+ scope = SkillValidation.isSkillClaim(claimsIdentity.claims())
+ ? String.format("%s/.default", JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()))
+ : getBotFrameworkOAuthScope();
+ }
return getAppCredentials(botAppIdClaim, scope)
.thenCompose(credentials -> getOrCreateConnectorClient(serviceUrl, credentials));
@@ -1236,8 +1253,8 @@ private CompletableFuture getAppCredentials(String appId, String
protected CompletableFuture buildAppCredentials(String appId, String scope) {
return credentialProvider.getAppPassword(appId).thenApply(appPassword -> {
AppCredentials credentials = channelProvider != null && channelProvider.isGovernment()
- ? new MicrosoftGovernmentAppCredentials(appId, appPassword, scope)
- : new MicrosoftAppCredentials(appId, appPassword);
+ ? new MicrosoftGovernmentAppCredentials(appId, appPassword, null, scope)
+ : new MicrosoftAppCredentials(appId, appPassword, null, scope);
return credentials;
});
}
@@ -1368,12 +1385,13 @@ public CompletableFuture getUserToken(TurnContext context, AppCre
));
}
- OAuthClient client = createOAuthAPIClient(context, oAuthAppCredentials).join();
- return client.getUserToken().getToken(
+ return createOAuthAPIClient(context, oAuthAppCredentials).thenCompose(client -> {
+ return client.getUserToken().getToken(
context.getActivity().getFrom().getId(),
connectionName,
context.getActivity().getChannelId(),
magicCode);
+ });
}
/**
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java
new file mode 100644
index 000000000..cb3e74ddd
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ChannelServiceHandler.java
@@ -0,0 +1,632 @@
+package com.microsoft.bot.builder;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import com.microsoft.bot.connector.Async;
+import com.microsoft.bot.connector.authentication.AuthenticationConfiguration;
+import com.microsoft.bot.connector.authentication.AuthenticationException;
+import com.microsoft.bot.connector.authentication.ChannelProvider;
+import com.microsoft.bot.connector.authentication.ClaimsIdentity;
+import com.microsoft.bot.connector.authentication.CredentialProvider;
+import com.microsoft.bot.connector.authentication.JwtTokenValidation;
+import com.microsoft.bot.connector.authentication.SkillValidation;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.AttachmentData;
+import com.microsoft.bot.schema.ChannelAccount;
+import com.microsoft.bot.schema.ConversationParameters;
+import com.microsoft.bot.schema.ConversationResourceResponse;
+import com.microsoft.bot.schema.ConversationsResult;
+import com.microsoft.bot.schema.PagedMembersResult;
+import com.microsoft.bot.schema.ResourceResponse;
+import com.microsoft.bot.schema.Transcript;
+
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * A class to help with the implementation of the Bot Framework protocol.
+ */
+public class ChannelServiceHandler {
+
+ private ChannelProvider channelProvider;
+
+ private final AuthenticationConfiguration authConfiguration;
+ private final CredentialProvider credentialProvider;
+
+ /**
+ * Initializes a new instance of the {@link ChannelServiceHandler} class,
+ * using a credential provider.
+ *
+ * @param credentialProvider The credential provider.
+ * @param authConfiguration The authentication configuration.
+ * @param channelProvider The channel provider.
+ */
+ public ChannelServiceHandler(
+ CredentialProvider credentialProvider,
+ AuthenticationConfiguration authConfiguration,
+ ChannelProvider channelProvider) {
+
+ if (credentialProvider == null) {
+ throw new IllegalArgumentException("credentialprovider cannot be nul");
+ }
+
+ if (authConfiguration == null) {
+ throw new IllegalArgumentException("authConfiguration cannot be nul");
+ }
+
+ this.credentialProvider = credentialProvider;
+ this.authConfiguration = authConfiguration;
+ this.channelProvider = channelProvider;
+ }
+
+ /**
+ * Sends an activity to the end of a conversation.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param activity The activity to send.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture handleSendToConversation(
+ String authHeader,
+ String conversationId,
+ Activity activity) {
+
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onSendToConversation(claimsIdentity, conversationId, activity);
+ });
+ }
+
+ /**
+ * Sends a reply to an activity.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param activityId The activity Id the reply is to.
+ * @param activity The activity to send.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture handleReplyToActivity(
+ String authHeader,
+ String conversationId,
+ String activityId,
+ Activity activity) {
+
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onReplyToActivity(claimsIdentity, conversationId, activityId, activity);
+ });
+ }
+
+ /**
+ * Edits a previously sent existing activity.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param activityId The activity Id to update.
+ * @param activity The replacement activity.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture handleUpdateActivity(
+ String authHeader,
+ String conversationId,
+ String activityId,
+ Activity activity) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onUpdateActivity(claimsIdentity, conversationId, activityId, activity);
+ });
+ }
+
+ /**
+ * Deletes an existing activity.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param activityId The activity Id.
+ *
+ * @return A {@link CompletableFuture} representing the result of
+ * the asynchronous operation.
+ */
+ public CompletableFuture handleDeleteActivity(String authHeader, String conversationId, String activityId) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onDeleteActivity(claimsIdentity, conversationId, activityId);
+ });
+ }
+
+ /**
+ * Enumerates the members of an activity.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param activityId The activity Id.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture> handleGetActivityMembers(
+ String authHeader,
+ String conversationId,
+ String activityId) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onGetActivityMembers(claimsIdentity, conversationId, activityId);
+ });
+ }
+
+ /**
+ * Create a new Conversation.
+ *
+ * @param authHeader The authentication header.
+ * @param parameters Parameters to create the conversation from.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture handleCreateConversation(
+ String authHeader,
+ ConversationParameters parameters) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onCreateConversation(claimsIdentity, parameters);
+ });
+ }
+
+ /**
+ * Lists the Conversations in which the bot has participated.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param continuationToken A skip or continuation token.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture handleGetConversations(
+ String authHeader,
+ String conversationId,
+ String continuationToken) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onGetConversations(claimsIdentity, conversationId, continuationToken);
+ });
+ }
+
+ /**
+ * Enumerates the members of a conversation.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture> handleGetConversationMembers(
+ String authHeader,
+ String conversationId) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onGetConversationMembers(claimsIdentity, conversationId);
+ });
+ }
+
+ /**
+ * Enumerates the members of a conversation one page at a time.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param pageSize Suggested page size.
+ * @param continuationToken A continuation token.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture handleGetConversationPagedMembers(
+ String authHeader,
+ String conversationId,
+ Integer pageSize,
+ String continuationToken) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onGetConversationPagedMembers(claimsIdentity, conversationId, pageSize, continuationToken);
+ });
+ }
+
+ /**
+ * Deletes a member from a conversation.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param memberId Id of the member to delete from this
+ * conversation.
+ *
+ * @return A {@link CompletableFuture} representing the
+ * asynchronous operation.
+ */
+ public CompletableFuture handleDeleteConversationMember(
+ String authHeader,
+ String conversationId,
+ String memberId) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onDeleteConversationMember(claimsIdentity, conversationId, memberId);
+ });
+ }
+
+ /**
+ * Uploads the historic activities of the conversation.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param transcript Transcript of activities.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture handleSendConversationHistory(
+ String authHeader,
+ String conversationId,
+ Transcript transcript) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onSendConversationHistory(claimsIdentity, conversationId, transcript);
+ });
+ }
+
+ /**
+ * Stores data in a compliant store when dealing with enterprises.
+ *
+ * @param authHeader The authentication header.
+ * @param conversationId The conversation Id.
+ * @param attachmentUpload Attachment data.
+ *
+ * @return A {@link CompletableFuture{TResult}} representing the
+ * result of the asynchronous operation.
+ */
+ public CompletableFuture handleUploadAttachment(
+ String authHeader,
+ String conversationId,
+ AttachmentData attachmentUpload) {
+ return authenticate(authHeader).thenCompose(claimsIdentity -> {
+ return onUploadAttachment(claimsIdentity, conversationId, attachmentUpload);
+ });
+ }
+
+ /**
+ * SendToConversation() API for Skill.
+ *
+ * This method allows you to send an activity to the end of a conversation.
+ * This is slightly different from ReplyToActivity(). *
+ * SendToConversation(conversationId) - will append the activity to the end
+ * of the conversation according to the timestamp or semantics of the
+ * channel. * ReplyToActivity(conversationId,ActivityId) - adds the
+ * activity as a reply to another activity, if the channel supports it. If
+ * the channel does not support nested replies, ReplyToActivity falls back
+ * to SendToConversation. Use ReplyToActivity when replying to a specific
+ * activity in the conversation. Use SendToConversation in all other cases.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId conversationId.
+ * @param activity Activity to send.
+ *
+ * @return task for a resource response.
+ */
+ protected CompletableFuture onSendToConversation(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ Activity activity) {
+ throw new NotImplementedException("onSendToConversation is not implemented");
+ }
+
+ /**
+ * OnReplyToActivity() API.
+ *
+ * Override this method allows to reply to an Activity. This is slightly
+ * different from SendToConversation(). *
+ * SendToConversation(conversationId) - will append the activity to the end
+ * of the conversation according to the timestamp or semantics of the
+ * channel. * ReplyToActivity(conversationId,ActivityId) - adds the
+ * activity as a reply to another activity, if the channel supports it. If
+ * the channel does not support nested replies, ReplyToActivity falls back
+ * to SendToConversation. Use ReplyToActivity when replying to a specific
+ * activity in the conversation. Use SendToConversation in all other cases.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ * @param activityId activityId the reply is to (OPTONAL).
+ * @param activity Activity to send.
+ *
+ * @return task for a resource response.
+ */
+ protected CompletableFuture onReplyToActivity(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String activityId,
+ Activity activity) {
+ throw new NotImplementedException("onReplyToActivity is not implemented");
+ }
+
+ /**
+ * OnUpdateActivity() API.
+ *
+ * Override this method to edit a previously sent existing activity. Some
+ * channels allow you to edit an existing activity to reflect the new state
+ * of a bot conversation. For example, you can remove buttons after someone
+ * has clicked "Approve" button.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ * @param activityId activityId to update.
+ * @param activity replacement Activity.
+ *
+ * @return task for a resource response.
+ */
+ protected CompletableFuture onUpdateActivity(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String activityId,
+ Activity activity) {
+ throw new NotImplementedException("onUpdateActivity is not implemented");
+ }
+
+ /**
+ * OnDeleteActivity() API.
+ *
+ * Override this method to Delete an existing activity. Some channels allow
+ * you to delete an existing activity, and if successful this method will
+ * remove the specified activity.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ * @param activityId activityId to delete.
+ *
+ * @return task for a resource response.
+ */
+ protected CompletableFuture onDeleteActivity(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String activityId) {
+ throw new NotImplementedException("onDeleteActivity is not implemented");
+ }
+
+ /**
+ * OnGetActivityMembers() API.
+ *
+ * Override this method to enumerate the members of an activity. This REST
+ * API takes a ConversationId and a ActivityId, returning an array of
+ * ChannelAccount Objects representing the members of the particular
+ * activity in the conversation.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ * @param activityId Activity D.
+ *
+ * @return task with result.
+ */
+ protected CompletableFuture> onGetActivityMembers(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String activityId) {
+ throw new NotImplementedException("onGetActivityMembers is not implemented");
+ }
+
+ /**
+ * CreateConversation() API.
+ *
+ * Override this method to create a new Conversation. POST to this method
+ * with a * Bot being the bot creating the conversation * IsGroup set to
+ * true if this is not a direct message (default instanceof false) * Array
+ * containing the members to include in the conversation The return value
+ * is a ResourceResponse which contains a conversation D which is suitable
+ * for use in the message payload and REST API URIs. Most channels only
+ * support the semantics of bots initiating a direct message conversation.
+ * An example of how to do that would be: var resource =
+ * connector.getconversations().CreateConversation(new
+ * ConversationParameters(){ Bot = bot, members = new ChannelAccount[] {
+ * new ChannelAccount("user1") } );
+ * connect.getConversations().OnSendToConversation(resource.getId(), new
+ * Activity() ... ) ; end.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param parameters Parameters to create the conversation
+ * from.
+ *
+ * @return task for a conversation resource response.
+ */
+ protected CompletableFuture onCreateConversation(
+ ClaimsIdentity claimsIdentity,
+ ConversationParameters parameters) {
+ throw new NotImplementedException("onCreateConversation is not implemented");
+ }
+
+ /**
+ * OnGetConversations() API for Skill.
+ *
+ * Override this method to list the Conversations in which this bot has
+ * participated. GET from this method with a skip token The return value is
+ * a ConversationsResult, which contains an array of ConversationMembers
+ * and a skip token. If the skip token is not empty, then there are further
+ * values to be returned. Call this method again with the returned token to
+ * get more values. Each ConversationMembers Object contains the D of the
+ * conversation and an array of ChannelAccounts that describe the members
+ * of the conversation.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId conversationId.
+ * @param continuationToken skip or continuation token.
+ *
+ * @return task for ConversationsResult.
+ */
+ protected CompletableFuture onGetConversations(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String continuationToken) {
+ throw new NotImplementedException("onGetConversationMembers is not implemented");
+ }
+
+ /**
+ * GetConversationMembers() API for Skill.
+ *
+ * Override this method to enumerate the members of a conversation. This
+ * REST API takes a ConversationId and returns an array of ChannelAccount
+ * Objects representing the members of the conversation.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ *
+ * @return task for a response.
+ */
+ protected CompletableFuture> onGetConversationMembers(
+ ClaimsIdentity claimsIdentity,
+ String conversationId) {
+ throw new NotImplementedException("onGetConversationMembers is not implemented");
+ }
+
+ /**
+ * GetConversationPagedMembers() API for Skill.
+ *
+ * Override this method to enumerate the members of a conversation one page
+ * at a time. This REST API takes a ConversationId. Optionally a pageSize
+ * and/or continuationToken can be provided. It returns a
+ * PagedMembersResult, which contains an array of ChannelAccounts
+ * representing the members of the conversation and a continuation token
+ * that can be used to get more values. One page of ChannelAccounts records
+ * are returned with each call. The number of records in a page may vary
+ * between channels and calls. The pageSize parameter can be used as a
+ * suggestion. If there are no additional results the response will not
+ * contain a continuation token. If there are no members in the
+ * conversation the Members will be empty or not present in the response. A
+ * response to a request that has a continuation token from a prior request
+ * may rarely return members from a previous request.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ * @param pageSize Suggested page size.
+ * @param continuationToken Continuation Token.
+ *
+ * @return task for a response.
+ */
+ protected CompletableFuture onGetConversationPagedMembers(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ Integer pageSize,
+ String continuationToken) {
+ throw new NotImplementedException("onGetConversationPagedMembers is not implemented");
+ }
+
+ /**
+ * DeleteConversationMember() API for Skill.
+ *
+ * Override this method to deletes a member from a conversation. This REST
+ * API takes a ConversationId and a memberId (of type String) and removes
+ * that member from the conversation. If that member was the last member of
+ * the conversation, the conversation will also be deleted.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ * @param memberId D of the member to delete from this
+ * conversation.
+ *
+ * @return task.
+ */
+ protected CompletableFuture onDeleteConversationMember(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String memberId) {
+ throw new NotImplementedException("onDeleteConversationMember is not implemented");
+ }
+
+ /**
+ * SendConversationHistory() API for Skill.
+ *
+ * Override this method to this method allows you to upload the historic
+ * activities to the conversation. Sender must ensure that the historic
+ * activities have unique ids and appropriate timestamps. The ids are used
+ * by the client to deal with duplicate activities and the timestamps are
+ * used by the client to render the activities in the right order.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ * @param transcript Transcript of activities.
+ *
+ * @return task for a resource response.
+ */
+ protected CompletableFuture onSendConversationHistory(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ Transcript transcript) {
+ throw new NotImplementedException("onSendConversationHistory is not implemented");
+ }
+
+ /**
+ * UploadAttachment() API.
+ *
+ * Override this method to store data in a compliant store when dealing
+ * with enterprises. The response is a ResourceResponse which contains an
+ * AttachmentId which is suitable for using with the attachments API.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation D.
+ * @param attachmentUpload Attachment data.
+ *
+ * @return task with result.
+ */
+ protected CompletableFuture onUploadAttachment(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ AttachmentData attachmentUpload) {
+ throw new NotImplementedException("onUploadAttachment is not implemented");
+ }
+
+ /**
+ * Helper to authenticate the header.
+ *
+ * This code is very similar to the code in
+ * {@link JwtTokenValidation#authenticateRequest(Activity, String,
+ * CredentialProvider, ChannelProvider, AuthenticationConfiguration,
+ * HttpClient)} , we should move this code somewhere in that library when
+ * we refactor auth, for now we keep it private to avoid adding more public
+ * static functions that we will need to deprecate later.
+ */
+ private CompletableFuture authenticate(String authHeader) {
+ if (StringUtils.isEmpty(authHeader)) {
+ return credentialProvider.isAuthenticationDisabled().thenCompose(isAuthDisabled -> {
+ if (!isAuthDisabled) {
+ return Async.completeExceptionally(
+ // No auth header. Auth is required. Request is not authorized.
+ new AuthenticationException("No auth header, Auth is required. Request is not authorized")
+ );
+ }
+
+ // In the scenario where auth is disabled, we still want to have the
+ // IsAuthenticated flag set in the ClaimsIdentity.
+ // To do this requires adding in an empty claim.
+ // Since ChannelServiceHandler calls are always a skill callback call, we set the skill claim too.
+ return CompletableFuture.completedFuture(SkillValidation.createAnonymousSkillClaim());
+ });
+ }
+
+ // Validate the header and extract claims.
+ return JwtTokenValidation.validateAuthHeader(
+ authHeader, credentialProvider, getChannelProvider(), "unknown", null, authConfiguration);
+ }
+ /**
+ * Gets the channel provider that implements {@link ChannelProvider} .
+ * @return the ChannelProvider value as a getChannelProvider().
+ */
+ protected ChannelProvider getChannelProvider() {
+ return this.channelProvider;
+ }
+
+}
+
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java
index 7a4af8d6b..483c23de4 100644
--- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/InvokeResponse.java
@@ -10,6 +10,7 @@
* Serialized content from the Body property.
*/
public class InvokeResponse {
+
/**
* The POST that is generated in response to the incoming Invoke Activity will
* have the HTTP Status code specified by this field.
@@ -23,7 +24,7 @@ public class InvokeResponse {
/**
* Initializes new instance of InvokeResponse.
- *
+ *
* @param withStatus The invoke response status.
* @param withBody The invoke response body.
*/
@@ -34,7 +35,7 @@ public InvokeResponse(int withStatus, Object withBody) {
/**
* Gets the HTTP status code for the response.
- *
+ *
* @return The HTTP status code.
*/
public int getStatus() {
@@ -43,7 +44,7 @@ public int getStatus() {
/**
* Sets the HTTP status code for the response.
- *
+ *
* @param withStatus The HTTP status code.
*/
public void setStatus(int withStatus) {
@@ -52,7 +53,7 @@ public void setStatus(int withStatus) {
/**
* Gets the body content for the response.
- *
+ *
* @return The body content.
*/
public Object getBody() {
@@ -61,10 +62,20 @@ public Object getBody() {
/**
* Sets the body content for the response.
- *
+ *
* @param withBody The body content.
*/
public void setBody(Object withBody) {
body = withBody;
}
+
+ /**
+ * Returns if the status of the request was successful.
+ * @return True if the status code is successful, false if not.
+ */
+ @SuppressWarnings("MagicNumber")
+ public boolean getIsSuccessStatusCode() {
+ return status >= 200 && status <= 299;
+ }
+
}
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ShowTypingMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ShowTypingMiddleware.java
index bf490d079..500bc12a2 100644
--- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ShowTypingMiddleware.java
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/ShowTypingMiddleware.java
@@ -4,6 +4,8 @@
package com.microsoft.bot.builder;
import com.microsoft.bot.connector.ExecutorFactory;
+import com.microsoft.bot.connector.authentication.ClaimsIdentity;
+import com.microsoft.bot.connector.authentication.SkillValidation;
import com.microsoft.bot.schema.Activity;
import com.microsoft.bot.schema.ActivityTypes;
import com.microsoft.bot.schema.ConversationReference;
@@ -73,7 +75,7 @@ public ShowTypingMiddleware(long withDelay, long withPeriod) throws IllegalArgum
*/
@Override
public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next) {
- if (!turnContext.getActivity().isType(ActivityTypes.MESSAGE)) {
+ if (!turnContext.getActivity().isType(ActivityTypes.MESSAGE) || isSkillBot(turnContext)) {
return next.next();
}
@@ -83,6 +85,16 @@ public CompletableFuture onTurn(TurnContext turnContext, NextDelegate next
return next.next().thenAccept(result -> sendFuture.cancel(true));
}
+ private static Boolean isSkillBot(TurnContext turnContext) {
+ Object identity = turnContext.getTurnState().get(BotAdapter.BOT_IDENTITY_KEY);
+ if (identity instanceof ClaimsIdentity) {
+ ClaimsIdentity claimsIdentity = (ClaimsIdentity) identity;
+ return SkillValidation.isSkillClaim(claimsIdentity.claims());
+ } else {
+ return false;
+ }
+ }
+
private static CompletableFuture sendTyping(
TurnContext turnContext,
long delay,
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java
new file mode 100644
index 000000000..7ac36c695
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TypedInvokeResponse.java
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.builder;
+
+/**
+ * Tuple class containing an HTTP Status Code and a JSON Serializable object.
+ * The HTTP Status code is, in the invoke activity scenario, what will be set in
+ * the resulting POST. The Body of the resulting POST will be the JSON
+ * Serialized content from the Body property.
+ * @param The type for the body of the TypedInvokeResponse.
+ */
+public class TypedInvokeResponse extends InvokeResponse {
+
+ /**
+ * Initializes new instance of InvokeResponse.
+ *
+ * @param withStatus The invoke response status.
+ * @param withBody The invoke response body.
+ */
+ public TypedInvokeResponse(int withStatus, T withBody) {
+ super(withStatus, withBody);
+ }
+
+ /**
+ * Sets the body with a typed value.
+ * @param withBody the typed value to set the body to.
+ */
+ public void setTypedBody(T withBody) {
+ super.setBody(withBody);
+ }
+
+ /**
+ * Gets the body content for the response.
+ *
+ * @return The body content.
+ */
+ public T getTypedBody() {
+ return (T) super.getBody();
+ }
+}
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java
new file mode 100644
index 000000000..0db9994bd
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkClient.java
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.builder.skills;
+
+import java.net.URI;
+import java.util.concurrent.CompletableFuture;
+
+import com.microsoft.bot.builder.TypedInvokeResponse;
+import com.microsoft.bot.schema.Activity;
+
+/**
+ * A Bot Framework client.
+ */
+public abstract class BotFrameworkClient {
+
+ // /**
+ // * Forwards an activity to a skill (bot).
+ // *
+ // * NOTE: Forwarding an activity to a skill will flush UserState and
+ // * ConversationState changes so that skill has accurate state.
+ // *
+ // * @param fromBotId The MicrosoftAppId of the bot sending the
+ // * activity.
+ // * @param toBotId The MicrosoftAppId of the bot receiving
+ // * the activity.
+ // * @param toUrl The URL of the bot receiving the activity.
+ // * @param serviceUrl The callback Url for the skill host.
+ // * @param conversationId A conversation ID to use for the
+ // * conversation with the skill.
+ // * @param activity The {@link Activity} to send to forward.
+ // *
+ // * @return task with optional invokeResponse.
+ // */
+ // public abstract CompletableFuture postActivity(
+ // String fromBotId,
+ // String toBotId,
+ // URI toUrl,
+ // URI serviceUrl,
+ // String conversationId,
+ // Activity activity);
+
+ /**
+ * Forwards an activity to a skill (bot).
+ *
+ * NOTE: Forwarding an activity to a skill will flush UserState and
+ * ConversationState changes so that skill has accurate state.
+ *
+ * @param fromBotId The MicrosoftAppId of the bot sending the
+ * activity.
+ * @param toBotId The MicrosoftAppId of the bot receiving
+ * the activity.
+ * @param toUri The URL of the bot receiving the activity.
+ * @param serviceUri The callback Url for the skill host.
+ * @param conversationId A conversation ID to use for the
+ * conversation with the skill.
+ * @param activity The {@link Activity} to send to forward.
+ * @param type The type for the response body to contain, can't really use due to type erasure
+ * in Java.
+ * @param The type for the TypedInvokeResponse body to contain.
+ *
+ * @return task with optional invokeResponse.
+ */
+ public abstract CompletableFuture> postActivity(
+ String fromBotId,
+ String toBotId,
+ URI toUri,
+ URI serviceUri,
+ String conversationId,
+ Activity activity,
+ Class type);
+}
+
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkSkill.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkSkill.java
new file mode 100644
index 000000000..99f4f97f1
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/BotFrameworkSkill.java
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.builder.skills;
+
+import java.net.URI;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Registration for a BotFrameworkHttpProtocol super. Skill endpoint.
+ */
+public class BotFrameworkSkill {
+
+ @JsonProperty(value = "id")
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private String id;
+
+ @JsonProperty(value = "appId")
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private String appId;
+
+ @JsonProperty(value = "skillEndpoint")
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private URI skillEndpoint;
+
+ /**
+ * Gets Id of the skill.
+ * @return the Id value as a String.
+ */
+ public String getId() {
+ return this.id;
+ }
+
+ /**
+ * Sets Id of the skill.
+ * @param withId The Id value.
+ */
+ public void setId(String withId) {
+ this.id = withId;
+ }
+ /**
+ * Gets appId of the skill.
+ * @return the AppId value as a String.
+ */
+ public String getAppId() {
+ return this.appId;
+ }
+
+ /**
+ * Sets appId of the skill.
+ * @param withAppId The AppId value.
+ */
+ public void setAppId(String withAppId) {
+ this.appId = withAppId;
+ }
+ /**
+ * Gets /api/messages endpoint for the skill.
+ * @return the SkillEndpoint value as a Uri.
+ */
+ public URI getSkillEndpoint() {
+ return this.skillEndpoint;
+ }
+
+ /**
+ * Sets /api/messages endpoint for the skill.
+ * @param withSkillEndpoint The SkillEndpoint value.
+ */
+ public void setSkillEndpoint(URI withSkillEndpoint) {
+ this.skillEndpoint = withSkillEndpoint;
+ }
+}
+
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryBase.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryBase.java
new file mode 100644
index 000000000..8e912ba73
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryBase.java
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.builder.skills;
+
+import java.util.concurrent.CompletableFuture;
+
+import com.microsoft.bot.schema.ConversationReference;
+
+import org.apache.commons.lang3.NotImplementedException;
+
+/**
+ * Defines the interface of a factory that is used to create unique conversation
+ * IDs for skill conversations.
+ */
+public abstract class SkillConversationIdFactoryBase {
+
+ /**
+ * Creates a conversation ID for a skill conversation super. on the
+ * caller's {@link ConversationReference} .
+ *
+ * @param conversationReference The skill's caller {@link ConversationReference} .
+ *
+ * @return A unique conversation ID used to communicate with the
+ * skill.
+ *
+ * It should be possible to use the returned String on a request URL and it
+ * should not contain special characters.
+ */
+ public CompletableFuture createSkillConversationId(ConversationReference conversationReference) {
+ throw new NotImplementedException("createSkillConversationId");
+ }
+
+ /**
+ * Creates a conversation id for a skill conversation.
+ *
+ * @param options A {@link SkillConversationIdFactoryOptions}
+ * instance containing parameters for creating the conversation ID.
+ *
+ * @return A unique conversation ID used to communicate with the skill.
+ *
+ * It should be possible to use the returned String on a request URL and it
+ * should not contain special characters.
+ */
+ public CompletableFuture createSkillConversationId(SkillConversationIdFactoryOptions options) {
+ throw new NotImplementedException("createSkillConversationId");
+ }
+
+ /**
+ * Gets the {@link ConversationReference} created using
+ * {@link
+ * CreateSkillConversationId(Microsoft#getBot()#getSchema()#getConversatio
+ * Reference(),System#getThreading()#getCancellationToken())} for a
+ * skillConversationId.
+ *
+ * @param skillConversationId A skill conversationId created using {@link
+ * CreateSkillConversationId(Microsoft#getBot()#getSchema()#getConversatio
+ * Reference(),System#getThreading()#getCancellationToken())} .
+ *
+ * @return The caller's {@link ConversationReference} for a skillConversationId. null if not found.
+ */
+ public CompletableFuture getConversationReference(String skillConversationId) {
+ throw new NotImplementedException("getConversationReference");
+ }
+
+ /**
+ * Gets the {@link SkillConversationReference} used during {@link
+ * CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT
+ * reading()#getCancellationToken())} for a skillConversationId.
+ *
+ * @param skillConversationId A skill conversationId created using {@link
+ * CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT
+ * reading()#getCancellationToken())} .
+ *
+ * @return The caller's {@link ConversationReference} for a skillConversationId, with originatingAudience.
+ * Null if not found.
+ */
+ public CompletableFuture getSkillConversationReference(String skillConversationId) {
+ throw new NotImplementedException("getSkillConversationReference");
+ }
+
+ /**
+ * Deletes a {@link ConversationReference} .
+ *
+ * @param skillConversationId A skill conversationId created using {@link
+ * CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT
+ * reading()#getCancellationToken())} .
+ *
+ * @return A {@link CompletableFuture} representing the asynchronous operation.
+ */
+ public abstract CompletableFuture deleteConversationReference(String skillConversationId);
+}
+
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryOptions.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryOptions.java
new file mode 100644
index 000000000..c0ffde7ef
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactoryOptions.java
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.builder.skills;
+
+import com.microsoft.bot.schema.Activity;
+
+/**
+ * A class defining the parameters used in
+ * {@link SkillConversationIdFactoryBase#createSkillConversationId(SkillConversationI
+ * FactoryOptions,System#getThreading()#getCancellationToken())} .
+ */
+public class SkillConversationIdFactoryOptions {
+
+ private String fromBotOAuthScope;
+
+ private String fromBotId;
+
+ private Activity activity;
+
+ private BotFrameworkSkill botFrameworkSkill;
+
+ /**
+ * Gets the oauth audience scope, used during token retrieval
+ * (either https://api.getbotframework().com or bot app id).
+ * @return the FromBotOAuthScope value as a String.
+ */
+ public String getFromBotOAuthScope() {
+ return this.fromBotOAuthScope;
+ }
+
+ /**
+ * Sets the oauth audience scope, used during token retrieval
+ * (either https://api.getbotframework().com or bot app id).
+ * @param withFromBotOAuthScope The FromBotOAuthScope value.
+ */
+ public void setFromBotOAuthScope(String withFromBotOAuthScope) {
+ this.fromBotOAuthScope = withFromBotOAuthScope;
+ }
+
+ /**
+ * Gets the id of the parent bot that is messaging the skill.
+ * @return the FromBotId value as a String.
+ */
+ public String getFromBotId() {
+ return this.fromBotId;
+ }
+
+ /**
+ * Sets the id of the parent bot that is messaging the skill.
+ * @param withFromBotId The FromBotId value.
+ */
+ public void setFromBotId(String withFromBotId) {
+ this.fromBotId = withFromBotId;
+ }
+
+ /**
+ * Gets the activity which will be sent to the skill.
+ * @return the Activity value as a getActivity().
+ */
+ public Activity getActivity() {
+ return this.activity;
+ }
+
+ /**
+ * Sets the activity which will be sent to the skill.
+ * @param withActivity The Activity value.
+ */
+ public void setActivity(Activity withActivity) {
+ this.activity = withActivity;
+ }
+ /**
+ * Gets the skill to create the conversation Id for.
+ * @return the BotFrameworkSkill value as a getBotFrameworkSkill().
+ */
+ public BotFrameworkSkill getBotFrameworkSkill() {
+ return this.botFrameworkSkill;
+ }
+
+ /**
+ * Sets the skill to create the conversation Id for.
+ * @param withBotFrameworkSkill The BotFrameworkSkill value.
+ */
+ public void setBotFrameworkSkill(BotFrameworkSkill withBotFrameworkSkill) {
+ this.botFrameworkSkill = withBotFrameworkSkill;
+ }
+}
+
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationReference.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationReference.java
new file mode 100644
index 000000000..5e538fbe7
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationReference.java
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.builder.skills;
+
+import com.microsoft.bot.schema.ConversationReference;
+
+/**
+ * A conversation reference type for skills.
+ */
+public class SkillConversationReference {
+
+ private ConversationReference conversationReference;
+
+ private String oAuthScope;
+
+ /**
+ * Gets the conversation reference.
+ * @return the ConversationReference value as a getConversationReference().
+ */
+ public ConversationReference getConversationReference() {
+ return this.conversationReference;
+ }
+
+ /**
+ * Sets the conversation reference.
+ * @param withConversationReference The ConversationReference value.
+ */
+ public void setConversationReference(ConversationReference withConversationReference) {
+ this.conversationReference = withConversationReference;
+ }
+
+ /**
+ * Gets the OAuth scope.
+ * @return the OAuthScope value as a String.
+ */
+ public String getOAuthScope() {
+ return this.oAuthScope;
+ }
+
+ /**
+ * Sets the OAuth scope.
+ * @param withOAuthScope The OAuthScope value.
+ */
+ public void setOAuthScope(String withOAuthScope) {
+ this.oAuthScope = withOAuthScope;
+ }
+
+}
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java
new file mode 100644
index 000000000..e2107182f
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillHandler.java
@@ -0,0 +1,340 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.builder.skills;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.microsoft.bot.builder.Bot;
+import com.microsoft.bot.builder.BotAdapter;
+import com.microsoft.bot.builder.BotCallbackHandler;
+import com.microsoft.bot.builder.ChannelServiceHandler;
+import com.microsoft.bot.builder.TurnContext;
+import com.microsoft.bot.connector.authentication.AuthenticationConfiguration;
+import com.microsoft.bot.connector.authentication.AuthenticationConstants;
+import com.microsoft.bot.connector.authentication.ChannelProvider;
+import com.microsoft.bot.connector.authentication.ClaimsIdentity;
+import com.microsoft.bot.connector.authentication.CredentialProvider;
+import com.microsoft.bot.connector.authentication.GovernmentAuthenticationConstants;
+import com.microsoft.bot.connector.authentication.JwtTokenValidation;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.ActivityTypes;
+import com.microsoft.bot.schema.CallerIdConstants;
+import com.microsoft.bot.schema.ConversationReference;
+import com.microsoft.bot.schema.ResourceResponse;
+
+import org.apache.commons.lang3.NotImplementedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Bot Framework Handler for skills.
+ */
+public class SkillHandler extends ChannelServiceHandler {
+
+ /**
+ * The skill conversation reference.
+ */
+ public static final String SKILL_CONVERSATION_REFERENCE_KEY =
+ "com.microsoft.bot.builder.skills.SkillConversationReference";
+
+ private final BotAdapter adapter;
+ private final Bot bot;
+ private final SkillConversationIdFactoryBase conversationIdFactory;
+
+ /**
+ * The slf4j Logger to use. Note that slf4j is configured by providing Log4j
+ * dependencies in the POM, and corresponding Log4j configuration in the
+ * 'resources' folder.
+ */
+ private Logger logger = LoggerFactory.getLogger(SkillHandler.class);
+
+ /**
+ * Initializes a new instance of the {@link SkillHandler} class, using a
+ * credential provider.
+ *
+ * @param adapter An instance of the {@link BotAdapter} that will handle the request.
+ * @param bot The {@link IBot} instance.
+ * @param conversationIdFactory A {@link SkillConversationIdFactoryBase} to unpack the conversation ID and
+ * map it to the calling bot.
+ * @param credentialProvider The credential provider.
+ * @param authConfig The authentication configuration.
+ * @param channelProvider The channel provider.
+ *
+ * Use a {@link MiddlewareSet} Object to add multiple middleware components
+ * in the constructor. Use the Use({@link Middleware} ) method to add
+ * additional middleware to the adapter after construction.
+ */
+ public SkillHandler(
+ BotAdapter adapter,
+ Bot bot,
+ SkillConversationIdFactoryBase conversationIdFactory,
+ CredentialProvider credentialProvider,
+ AuthenticationConfiguration authConfig,
+ ChannelProvider channelProvider
+ ) {
+
+ super(credentialProvider, authConfig, channelProvider);
+
+ if (adapter == null) {
+ throw new IllegalArgumentException("adapter cannot be null");
+ }
+
+ if (bot == null) {
+ throw new IllegalArgumentException("bot cannot be null");
+ }
+
+ if (conversationIdFactory == null) {
+ throw new IllegalArgumentException("conversationIdFactory cannot be null");
+ }
+
+ this.adapter = adapter;
+ this.bot = bot;
+ this.conversationIdFactory = conversationIdFactory;
+ }
+
+ /**
+ * SendToConversation() API for Skill.
+ *
+ * This method allows you to send an activity to the end of a conversation.
+ * This is slightly different from ReplyToActivity(). *
+ * SendToConversation(conversationId) - will append the activity to the end
+ * of the conversation according to the timestamp or semantics of the
+ * channel. * ReplyToActivity(conversationId,ActivityId) - adds the
+ * activity as a reply to another activity, if the channel supports it. If
+ * the channel does not support nested replies, ReplyToActivity falls back
+ * to SendToConversation. Use ReplyToActivity when replying to a specific
+ * activity in the conversation. Use SendToConversation in all other cases.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId conversationId.
+ * @param activity Activity to send.
+ *
+ * @return task for a resource response.
+ */
+ @Override
+ protected CompletableFuture onSendToConversation(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ Activity activity) {
+ return processActivity(claimsIdentity, conversationId, null, activity);
+ }
+
+ /**
+ * ReplyToActivity() API for Skill.
+ *
+ * This method allows you to reply to an activity. This is slightly
+ * different from SendToConversation(). *
+ * SendToConversation(conversationId) - will append the activity to the end
+ * of the conversation according to the timestamp or semantics of the
+ * channel. * ReplyToActivity(conversationId,ActivityId) - adds the
+ * activity as a reply to another activity, if the channel supports it. If
+ * the channel does not support nested replies, ReplyToActivity falls back
+ * to SendToConversation. Use ReplyToActivity when replying to a specific
+ * activity in the conversation. Use SendToConversation in all other cases.
+ *
+ * @param claimsIdentity claimsIdentity for the bot, should have
+ * AudienceClaim, AppIdClaim and ServiceUrlClaim.
+ * @param conversationId Conversation ID.
+ * @param activityId activityId the reply is to (OPTIONAL).
+ * @param activity Activity to send.
+ *
+ * @return task for a resource response.
+ */
+ @Override
+ protected CompletableFuture onReplyToActivity(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String activityId,
+ Activity activity) {
+ return processActivity(claimsIdentity, conversationId, activityId, activity);
+ }
+
+ /**
+ */
+ @Override
+ protected CompletableFuture onDeleteActivity(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String activityId) {
+
+ SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join();
+
+ BotCallbackHandler callback = turnContext -> {
+ turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference);
+ return turnContext.deleteActivity(activityId);
+ };
+
+ return adapter.continueConversation(claimsIdentity,
+ skillConversationReference.getConversationReference(),
+ skillConversationReference.getOAuthScope(),
+ callback);
+ }
+
+ /**
+ */
+ @Override
+ protected CompletableFuture onUpdateActivity(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String activityId,
+ Activity activity) {
+ SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join();
+
+ AtomicReference resourceResponse = new AtomicReference();
+
+ BotCallbackHandler callback = turnContext -> {
+ turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference);
+ activity.applyConversationReference(skillConversationReference.getConversationReference());
+ turnContext.getActivity().setId(activityId);
+ String callerId = String.format("%s%s",
+ CallerIdConstants.BOT_TO_BOT_PREFIX,
+ JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()));
+ turnContext.getActivity().setCallerId(callerId);
+ resourceResponse.set(turnContext.updateActivity(activity).join());
+ return CompletableFuture.completedFuture(null);
+ };
+
+ adapter.continueConversation(claimsIdentity,
+ skillConversationReference.getConversationReference(),
+ skillConversationReference.getOAuthScope(),
+ callback);
+
+ if (resourceResponse.get() != null) {
+ return CompletableFuture.completedFuture(resourceResponse.get());
+ } else {
+ return CompletableFuture.completedFuture(new ResourceResponse(UUID.randomUUID().toString()));
+ }
+ }
+
+ private static void applyEoCToTurnContextActivity(TurnContext turnContext, Activity endOfConversationActivity) {
+ // transform the turnContext.Activity to be the EndOfConversation.
+ turnContext.getActivity().setType(endOfConversationActivity.getType());
+ turnContext.getActivity().setText(endOfConversationActivity.getText());
+ turnContext.getActivity().setCode(endOfConversationActivity.getCode());
+
+ turnContext.getActivity().setReplyToId(endOfConversationActivity.getReplyToId());
+ turnContext.getActivity().setValue(endOfConversationActivity.getValue());
+ turnContext.getActivity().setEntities(endOfConversationActivity.getEntities());
+ turnContext.getActivity().setLocale(endOfConversationActivity.getLocale());
+ turnContext.getActivity().setLocalTimestamp(endOfConversationActivity.getLocalTimestamp());
+ turnContext.getActivity().setTimestamp(endOfConversationActivity.getTimestamp());
+ turnContext.getActivity().setChannelData(endOfConversationActivity.getChannelData());
+ for (Map.Entry entry : endOfConversationActivity.getProperties().entrySet()) {
+ turnContext.getActivity().setProperties(entry.getKey(), entry.getValue());
+ }
+ }
+
+ private static void applyEventToTurnContextActivity(TurnContext turnContext, Activity eventActivity) {
+ // transform the turnContext.Activity to be the EventActivity.
+ turnContext.getActivity().setType(eventActivity.getType());
+ turnContext.getActivity().setName(eventActivity.getName());
+ turnContext.getActivity().setValue(eventActivity.getValue());
+ turnContext.getActivity().setRelatesTo(eventActivity.getRelatesTo());
+
+ turnContext.getActivity().setReplyToId(eventActivity.getReplyToId());
+ turnContext.getActivity().setValue(eventActivity.getValue());
+ turnContext.getActivity().setEntities(eventActivity.getEntities());
+ turnContext.getActivity().setLocale(eventActivity.getLocale());
+ turnContext.getActivity().setLocalTimestamp(eventActivity.getLocalTimestamp());
+ turnContext.getActivity().setTimestamp(eventActivity.getTimestamp());
+ turnContext.getActivity().setChannelData(eventActivity.getChannelData());
+ for (Map.Entry entry : eventActivity.getProperties().entrySet()) {
+ turnContext.getActivity().setProperties(entry.getKey(), entry.getValue());
+ }
+ }
+
+ private CompletableFuture getSkillConversationReference(String conversationId) {
+
+ SkillConversationReference skillConversationReference;
+ try {
+ skillConversationReference = conversationIdFactory.getSkillConversationReference(conversationId).join();
+ } catch (NotImplementedException ex) {
+ if (logger != null) {
+ logger.warn("Got NotImplementedException when trying to call "
+ + "GetSkillConversationReference() on the ConversationIdFactory,"
+ + " attempting to use deprecated GetConversationReference() method instead.");
+ }
+
+ // Attempt to get SkillConversationReference using deprecated method.
+ // this catch should be removed once we remove the deprecated method.
+ // We need to use the deprecated method for backward compatibility.
+ ConversationReference conversationReference =
+ conversationIdFactory.getConversationReference(conversationId).join();
+ skillConversationReference = new SkillConversationReference();
+ skillConversationReference.setConversationReference(conversationReference);
+ if (getChannelProvider() != null && getChannelProvider().isGovernment()) {
+ skillConversationReference.setOAuthScope(
+ GovernmentAuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE);
+ } else {
+ skillConversationReference.setOAuthScope(
+ AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE);
+ }
+ }
+
+ if (skillConversationReference == null) {
+ if (logger != null) {
+ logger.warn(
+ String.format("Unable to get skill conversation reference for conversationId %s.", conversationId)
+ );
+ }
+ throw new RuntimeException("Key not found");
+ }
+
+ return CompletableFuture.completedFuture(skillConversationReference);
+ }
+
+ private CompletableFuture processActivity(
+ ClaimsIdentity claimsIdentity,
+ String conversationId,
+ String replyToActivityId,
+ Activity activity) {
+
+ SkillConversationReference skillConversationReference = getSkillConversationReference(conversationId).join();
+
+ AtomicReference resourceResponse = new AtomicReference();
+
+ BotCallbackHandler callback = turnContext -> {
+ turnContext.getTurnState().add(SKILL_CONVERSATION_REFERENCE_KEY, skillConversationReference);
+ activity.applyConversationReference(skillConversationReference.getConversationReference());
+ turnContext.getActivity().setId(replyToActivityId);
+ String callerId = String.format("%s%s",
+ CallerIdConstants.BOT_TO_BOT_PREFIX,
+ JwtTokenValidation.getAppIdFromClaims(claimsIdentity.claims()));
+ turnContext.getActivity().setCallerId(callerId);
+
+ switch (activity.getType()) {
+ case ActivityTypes.END_OF_CONVERSATION:
+ conversationIdFactory.deleteConversationReference(conversationId).join();
+ applyEoCToTurnContextActivity(turnContext, activity);
+ bot.onTurn(turnContext).join();
+ break;
+ case ActivityTypes.EVENT:
+ applyEventToTurnContextActivity(turnContext, activity);
+ bot.onTurn(turnContext).join();
+ break;
+ default:
+ resourceResponse.set(turnContext.sendActivity(activity).join());
+ break;
+ }
+ return CompletableFuture.completedFuture(null);
+ };
+
+ adapter.continueConversation(claimsIdentity,
+ skillConversationReference.getConversationReference(),
+ skillConversationReference.getOAuthScope(),
+ callback).join();
+
+ if (resourceResponse.get() != null) {
+ return CompletableFuture.completedFuture(resourceResponse.get());
+ } else {
+ return CompletableFuture.completedFuture(new ResourceResponse(UUID.randomUUID().toString()));
+ }
+ }
+}
+
diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/package-info.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/package-info.java
new file mode 100644
index 000000000..4bf1c6ad6
--- /dev/null
+++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/package-info.java
@@ -0,0 +1,8 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for
+// license information.
+
+/**
+ * This package contains the classes for Bot-Builder.
+ */
+package com.microsoft.bot.builder.skills;
diff --git a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java
index 2226517c6..6137eb730 100644
--- a/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java
+++ b/libraries/bot-builder/src/test/java/com/microsoft/bot/builder/adapters/TestAdapter.java
@@ -9,7 +9,7 @@
import com.microsoft.bot.connector.authentication.AppCredentials;
import com.microsoft.bot.schema.*;
import org.apache.commons.lang3.StringUtils;
-
+import org.junit.rules.ExpectedException;
import java.time.OffsetDateTime;
import java.time.ZoneId;
@@ -271,7 +271,7 @@ public CompletableFuture sendActivities(TurnContext context,
Thread.sleep(delayMs);
} catch (InterruptedException e) {
}
- } else if (activity.getType() == ActivityTypes.TRACE) {
+ } else if (activity.getType().equals(ActivityTypes.TRACE)) {
if (sendTraceActivity) {
synchronized (botReplies) {
botReplies.add(activity);
@@ -506,8 +506,8 @@ public void addExchangeableToken(String connectionName,
String channelId,
String userId,
String exchangableItem,
- String token)
- {
+ String token
+ ) {
ExchangableTokenKey key = new ExchangableTokenKey();
key.setConnectionName(connectionName);
key.setChannelId(channelId);
@@ -521,6 +521,23 @@ public void addExchangeableToken(String connectionName,
}
}
+ public void throwOnExchangeRequest(String connectionName,
+ String channelId,
+ String userId,
+ String exchangableItem) {
+ ExchangableTokenKey key = new ExchangableTokenKey();
+ key.setConnectionName(connectionName);
+ key.setChannelId(channelId);
+ key.setUserId(userId);
+ key.setExchangableItem(exchangableItem);
+
+ if (exchangableToken.containsKey(key)) {
+ exchangableToken.replace(key, exceptionExpected);
+ } else {
+ exchangableToken.put(key, exceptionExpected);
+ }
+ }
+
@Override
public CompletableFuture signOutUser(TurnContext turnContext, AppCredentials oAuthAppCredentials,
String connectionName, String userId) {
diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java
index 327cedc11..2754253ea 100644
--- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java
+++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/AuthenticationConfiguration.java
@@ -10,12 +10,31 @@
* General configuration settings for authentication.
*/
public class AuthenticationConfiguration {
+
+ private ClaimsValidator claimsValidator = null;
+
/**
* Required endorsements for auth.
- *
+ *
* @return A List of endorsements.
*/
public List requiredEndorsements() {
return new ArrayList();
}
+
+ /**
+ * Access to the ClaimsValidator used to validate the identity claims.
+ * @return the ClaimsValidator value if set.
+ */
+ public ClaimsValidator getClaimsValidator() {
+ return claimsValidator;
+ }
+
+ /**
+ * Access to the ClaimsValidator used to validate the identity claims.
+ * @param withClaimsValidator the value to set the ClaimsValidator to.
+ */
+ public void setClaimsValidator(ClaimsValidator withClaimsValidator) {
+ claimsValidator = withClaimsValidator;
+ }
}
diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsValidator.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsValidator.java
new file mode 100644
index 000000000..42692da1a
--- /dev/null
+++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/ClaimsValidator.java
@@ -0,0 +1,20 @@
+package com.microsoft.bot.connector.authentication;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * An abstract class used to validate identity.
+ */
+public abstract class ClaimsValidator {
+
+ /**
+ * Validates a Map of claims and should throw an exception if the
+ * validation fails.
+ *
+ * @param claims The Map of claims to validate.
+ *
+ * @return true if the validation is successful, false if not.
+ */
+ public abstract CompletableFuture validateClaims(Map claims);
+}
diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java
index 4d6398b78..f81a70ca6 100644
--- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java
+++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/JwtTokenValidation.java
@@ -147,6 +147,40 @@ public static CompletableFuture validateAuthHeader(
new IllegalArgumentException("No authHeader present. Auth is required."));
}
+ return authenticateToken(authHeader, credentials, channelProvider, channelId, serviceUrl, authConfig)
+ .thenApply(identity -> {
+ validateClaims(authConfig, identity.claims());
+ return identity;
+ }
+ );
+ }
+
+ private static CompletableFuture validateClaims(
+ AuthenticationConfiguration authConfig,
+ Map claims
+ ) {
+ if (authConfig.getClaimsValidator() != null) {
+ return authConfig.getClaimsValidator().validateClaims(claims);
+ } else if (SkillValidation.isSkillClaim(claims)) {
+ return Async.completeExceptionally(
+ new RuntimeException("ClaimValidator is required for validation of Skill Host calls")
+ );
+ }
+ return CompletableFuture.completedFuture(null);
+ }
+
+ private static CompletableFuture authenticateToken(
+ String authHeader,
+ CredentialProvider credentials,
+ ChannelProvider channelProvider,
+ String channelId,
+ String serviceUrl,
+ AuthenticationConfiguration authConfig
+ ) {
+ if (SkillValidation.isSkillToken(authHeader)) {
+ return SkillValidation.authenticateChannelToken(
+ authHeader, credentials, channelProvider, channelId, authConfig);
+ }
boolean usingEmulator = EmulatorValidation.isTokenFromEmulator(authHeader);
if (usingEmulator) {
return EmulatorValidation
@@ -168,6 +202,7 @@ public static CompletableFuture validateAuthHeader(
authHeader, credentials, channelProvider, serviceUrl, channelId, authConfig
);
}
+
}
/**
diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java
index 619689313..88190f19b 100644
--- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java
+++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/MicrosoftGovernmentAppCredentials.java
@@ -42,9 +42,26 @@ public MicrosoftGovernmentAppCredentials(String appId, String password, String o
);
}
+ /**
+ * Initializes a new instance of the MicrosoftGovernmentAppCredentials class.
+ *
+ * @param withAppId The Microsoft app ID.
+ * @param withAppPassword The Microsoft app password.
+ * @param withChannelAuthTenant Optional. The oauth token tenant.
+ * @param withOAuthScope The scope for the token.
+ */
+ public MicrosoftGovernmentAppCredentials(
+ String withAppId,
+ String withAppPassword,
+ String withChannelAuthTenant,
+ String withOAuthScope
+ ) {
+ super(withAppId, withAppPassword, withChannelAuthTenant, withOAuthScope);
+ }
+
/**
* An empty set of credentials.
- *
+ *
* @return An empty Gov credentials.
*/
public static MicrosoftGovernmentAppCredentials empty() {
diff --git a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SkillValidation.java b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SkillValidation.java
index 33891fe5a..cbf1885be 100644
--- a/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SkillValidation.java
+++ b/libraries/bot-connector/src/main/java/com/microsoft/bot/connector/authentication/SkillValidation.java
@@ -52,44 +52,71 @@ private SkillValidation() {
true, Duration.ofMinutes(5), true);
/**
- * Checks if the given list of claims represents a skill. A skill claim should
- * contain: An AuthenticationConstants.VersionClaim" claim. An
- * AuthenticationConstants.AudienceClaim claim. An
- * AuthenticationConstants.AppIdClaim claim (v1) or an a
- * AuthenticationConstants.AuthorizedParty claim (v2). And the appId claim
- * should be different than the audience claim. When a channel (webchat, teams,
- * etc.) invokes a bot, the
- * is set to
- * but when a bot calls another bot, the audience claim is set to the appId of
- * the bot being invoked. The protocol supports v1 and v2 tokens: For v1 tokens,
- * the AuthenticationConstants.AppIdClaim is present and set to the app Id of
- * the calling bot. For v2 tokens, the AuthenticationConstants.AuthorizedParty
- * is present and set to the app Id of the calling bot.
+ * Determines if a given Auth header is from from a skill to bot or bot to skill
+ * request.
+ *
+ * @param authHeader Bearer Token, in the "Bearer [Long String]" Format.
+ * @return True, if the token was issued for a skill to bot communication.
+ * Otherwise, false.
+ */
+ public static boolean isSkillToken(String authHeader) {
+ if (!JwtTokenValidation.isValidTokenFormat(authHeader)) {
+ return false;
+ }
+
+ // We know is a valid token, split it and work with it:
+ // [0] = "Bearer"
+ // [1] = "[Big Long String]"
+ String bearerToken = authHeader.split(" ")[1];
+
+ // Parse token
+ ClaimsIdentity identity = new ClaimsIdentity(JWT.decode(bearerToken));
+
+ return isSkillClaim(identity.claims());
+ }
+
+ /**
+ * Checks if the given list of claims represents a skill.
+ *
+ * A skill claim should contain: An {@link AuthenticationConstants#versionClaim}
+ * claim. An {@link AuthenticationConstants#audienceClaim} claim. An
+ * {@link AuthenticationConstants#appIdClaim} claim (v1) or an a
+ * {@link AuthenticationConstants#authorizedParty} claim (v2). And the appId
+ * claim should be different than the audience claim. When a channel (webchat,
+ * teams, etc.) invokes a bot, the {@link AuthenticationConstants#audienceClaim}
+ * is set to {@link AuthenticationConstants#toBotFromChannelTokenIssuer} but
+ * when a bot calls another bot, the audience claim is set to the appId of the
+ * bot being invoked. The protocol supports v1 and v2 tokens: For v1 tokens, the
+ * {@link AuthenticationConstants#appIdClaim} is present and set to the app Id
+ * of the calling bot. For v2 tokens, the
+ * {@link AuthenticationConstants#authorizedParty} is present and set to the app
+ * Id of the calling bot.
+ *
+ * @param claims A list of claims.
*
- * @param claims A map of claims
* @return True if the list of claims is a skill claim, false if is not.
*/
public static Boolean isSkillClaim(Map claims) {
for (Map.Entry entry : claims.entrySet()) {
- if (entry.getValue() == AuthenticationConstants.ANONYMOUS_SKILL_APPID
- && entry.getKey() == AuthenticationConstants.APPID_CLAIM) {
+ if (entry.getValue() != null && entry.getValue().equals(AuthenticationConstants.ANONYMOUS_SKILL_APPID)
+ && entry.getKey().equals(AuthenticationConstants.APPID_CLAIM)) {
return true;
}
}
Optional> version = claims.entrySet().stream()
- .filter((x) -> x.getKey() == AuthenticationConstants.VERSION_CLAIM).findFirst();
+ .filter((x) -> x.getKey().equals(AuthenticationConstants.VERSION_CLAIM)).findFirst();
if (!version.isPresent()) {
// Must have a version claim.
return false;
}
Optional> audience = claims.entrySet().stream()
- .filter((x) -> x.getKey() == AuthenticationConstants.AUDIENCE_CLAIM).findFirst();
+ .filter((x) -> x.getKey().equals(AuthenticationConstants.AUDIENCE_CLAIM)).findFirst();
if (!audience.isPresent()
- || AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER == audience.get().getValue()) {
+ || AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER.equals(audience.get().getValue())) {
// The audience is https://api.botframework.com and not an appId.
return false;
}
@@ -105,38 +132,50 @@ public static Boolean isSkillClaim(Map claims) {
}
/**
- * Determines if a given Auth header is from from a skill to bot or bot to skill request.
+ * Validates that the incoming Auth Header is a token sent from a bot to a skill
+ * or from a skill to a bot.
*
- * @param authHeader Bearer Token, in the "Bearer [Long String]" Format.
- * @return True, if the token was issued for a skill to bot communication. Otherwise, false.
+ * @param authHeader The raw HTTP header in the format: "Bearer
+ * [longString]".
+ * @param credentials The user defined set of valid credentials, such as the
+ * AppId.
+ * @param channelProvider The channelService value that distinguishes public
+ * Azure from US Government Azure.
+ * @param channelId The ID of the channel to validate.
+ * @param authConfig The authentication configuration.
+ *
+ * @return A {@link ClaimsIdentity} instance if the validation is successful.
*/
- public static boolean isSkillToken(String authHeader) {
- if (!JwtTokenValidation.isValidTokenFormat(authHeader)) {
- return false;
+ public static CompletableFuture authenticateChannelToken(String authHeader,
+ CredentialProvider credentials, ChannelProvider channelProvider, String channelId,
+ AuthenticationConfiguration authConfig) {
+ if (authConfig == null) {
+ return Async.completeExceptionally(new IllegalArgumentException("authConfig cannot be null."));
}
- // We know is a valid token, split it and work with it:
- // [0] = "Bearer"
- // [1] = "[Big Long String]"
- String bearerToken = authHeader.split(" ")[1];
+ String openIdMetadataUrl = channelProvider != null && channelProvider.isGovernment()
+ ? GovernmentAuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL
+ : AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL;
- // Parse token
- ClaimsIdentity identity = new ClaimsIdentity(JWT.decode(bearerToken));
+ JwtTokenExtractor tokenExtractor = new JwtTokenExtractor(TOKENVALIDATIONPARAMETERS, openIdMetadataUrl,
+ AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS);
- return isSkillClaim(identity.claims());
+ return tokenExtractor.getIdentity(authHeader, channelId, authConfig.requiredEndorsements())
+ .thenCompose(identity -> {
+ return validateIdentity(identity, credentials).thenCompose(result -> {
+ return CompletableFuture.completedFuture(identity);
+ });
+ });
}
/**
* Helper to validate a skills ClaimsIdentity.
*
- * @param identity The ClaimsIdentity to validate.
+ * @param identity The ClaimsIdentity to validate.
* @param credentials The CredentialProvider.
* @return Nothing if success, otherwise a CompletionException
*/
- public static CompletableFuture validateIdentity(
- ClaimsIdentity identity,
- CredentialProvider credentials
- ) {
+ public static CompletableFuture validateIdentity(ClaimsIdentity identity, CredentialProvider credentials) {
if (identity == null) {
// No valid identity. Not Authorized.
return Async.completeExceptionally(new AuthenticationException("Invalid Identity"));
@@ -148,28 +187,20 @@ public static CompletableFuture validateIdentity(
}
Optional> versionClaim = identity.claims().entrySet().stream()
- .filter(item -> StringUtils.equals(AuthenticationConstants.VERSION_CLAIM, item.getKey()))
- .findFirst();
+ .filter(item -> StringUtils.equals(AuthenticationConstants.VERSION_CLAIM, item.getKey())).findFirst();
if (!versionClaim.isPresent()) {
// No version claim
- return Async.completeExceptionally(
- new AuthenticationException(
- AuthenticationConstants.VERSION_CLAIM + " claim is required on skill Tokens."
- )
- );
+ return Async.completeExceptionally(new AuthenticationException(
+ AuthenticationConstants.VERSION_CLAIM + " claim is required on skill Tokens."));
}
// Look for the "aud" claim, but only if issued from the Bot Framework
Optional> audienceClaim = identity.claims().entrySet().stream()
- .filter(item -> StringUtils.equals(AuthenticationConstants.AUDIENCE_CLAIM, item.getKey()))
- .findFirst();
+ .filter(item -> StringUtils.equals(AuthenticationConstants.AUDIENCE_CLAIM, item.getKey())).findFirst();
if (!audienceClaim.isPresent() || StringUtils.isEmpty(audienceClaim.get().getValue())) {
// Claim is not present or doesn't have a value. Not Authorized.
- return Async.completeExceptionally(
- new AuthenticationException(
- AuthenticationConstants.AUDIENCE_CLAIM + " claim is required on skill Tokens."
- )
- );
+ return Async.completeExceptionally(new AuthenticationException(
+ AuthenticationConstants.AUDIENCE_CLAIM + " claim is required on skill Tokens."));
}
String appId = JwtTokenValidation.getAppIdFromClaims(identity.claims());
@@ -177,28 +208,25 @@ public static CompletableFuture validateIdentity(
return Async.completeExceptionally(new AuthenticationException("Invalid appId."));
}
- return credentials.isValidAppId(audienceClaim.get().getValue())
- .thenApply(isValid -> {
- if (!isValid) {
- throw new AuthenticationException("Invalid audience.");
- }
- return null;
- });
+ return credentials.isValidAppId(audienceClaim.get().getValue()).thenApply(isValid -> {
+ if (!isValid) {
+ throw new AuthenticationException("Invalid audience.");
+ }
+ return null;
+ });
}
/**
* Creates a ClaimsIdentity for an anonymous (unauthenticated) skill.
*
* @return A ClaimsIdentity instance with authentication type set to
- * AuthenticationConstants.AnonymousAuthType and a reserved
- * AuthenticationConstants.AnonymousSkillAppId claim.
+ * AuthenticationConstants.AnonymousAuthType and a reserved
+ * AuthenticationConstants.AnonymousSkillAppId claim.
*/
public static ClaimsIdentity createAnonymousSkillClaim() {
Map claims = new HashMap<>();
claims.put(AuthenticationConstants.APPID_CLAIM, AuthenticationConstants.ANONYMOUS_SKILL_APPID);
- return new ClaimsIdentity(
- AuthenticationConstants.ANONYMOUS_AUTH_TYPE,
- AuthenticationConstants.ANONYMOUS_AUTH_TYPE,
- claims);
+ return new ClaimsIdentity(AuthenticationConstants.ANONYMOUS_AUTH_TYPE,
+ AuthenticationConstants.ANONYMOUS_AUTH_TYPE, claims);
}
}
diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/BeginSkillDialogOptions.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/BeginSkillDialogOptions.java
new file mode 100644
index 000000000..1f472a3ba
--- /dev/null
+++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/BeginSkillDialogOptions.java
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.dialogs;
+
+import com.microsoft.bot.schema.Activity;
+
+/**
+ * A class with dialog arguments for a {@link SkillDialog} .
+ */
+public class BeginSkillDialogOptions {
+
+ private Activity activity;
+
+ /**
+ * Gets the {@link Activity} to send to the skill.
+ * @return the Activity value as a getActivity().
+ */
+ public Activity getActivity() {
+ return this.activity;
+ }
+
+ /**
+ * Sets the {@link Activity} to send to the skill.
+ * @param withActivity The Activity value.
+ */
+ public void setActivity(Activity withActivity) {
+ this.activity = withActivity;
+ }
+
+}
diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ComponentDialog.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ComponentDialog.java
index 92022a2ae..a66b080fb 100644
--- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ComponentDialog.java
+++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/ComponentDialog.java
@@ -10,409 +10,406 @@
import com.microsoft.bot.builder.TurnContext;
import com.microsoft.bot.connector.Async;
+/**
+ * A {@link Dialog} that is composed of other dialogs.
+ *
+ * A component dialog has an inner {@link DialogSet} and {@link DialogContext}
+ * ,which provides an inner dialog stack that is hidden from the parent dialog.
+ */
+public class ComponentDialog extends DialogContainer {
+
+ private String initialDialogId;
+
/**
- * A {@link Dialog} that is composed of other dialogs.
- *
- * A component dialog has an inner {@link DialogSet} and {@link DialogContext} ,which provides
- * an inner dialog stack that is hidden from the parent dialog.
+ * The id for the persisted dialog state.
*/
- public class ComponentDialog extends DialogContainer {
+ public static final String PERSISTEDDIALOGSTATE = "dialogs";
- private String initialDialogId;
+ private boolean initialized;
- /**
- * The id for the persisted dialog state.
- */
- public static final String PERSISTEDDIALOGSTATE = "dialogs";
+ /**
+ * Initializes a new instance of the {@link ComponentDialog} class.
+ *
+ * @param dialogId The D to assign to the new dialog within the parent dialog
+ * set.
+ */
+ public ComponentDialog(String dialogId) {
+ super(dialogId);
+ }
- private boolean initialized;
+ /**
+ * Called when the dialog is started and pushed onto the parent's dialog stack.
+ *
+ * @param outerDc The parent {@link DialogContext} for the current turn of
+ * conversation.
+ * @param options Optional, initial information to pass to the dialog.
+ *
+ * @return A {@link CompletableFuture} representing the hronous operation.
+ *
+ * If the task is successful, the result indicates whether the dialog is
+ * still active after the turn has been processed by the dialog.
+ */
+ @Override
+ public CompletableFuture beginDialog(DialogContext outerDc, Object options) {
- /**
- * Initializes a new instance of the {@link ComponentDialog} class.
- *
- * @param dialogId The D to assign to the new dialog within the parent dialog
- * set.
- */
- public ComponentDialog(String dialogId) {
- super(dialogId);
+ if (outerDc == null) {
+ return Async.completeExceptionally(new IllegalArgumentException("outerDc cannot be null."));
}
- /**
- * Called when the dialog is started and pushed onto the parent's dialog stack.
- *
- * @param outerDc The parent {@link DialogContext} for the current turn of
- * conversation.
- * @param options Optional, initial information to pass to the dialog.
- *
- * @return A {@link CompletableFuture} representing the hronous operation.
- *
- * If the task is successful, the result indicates whether the dialog is
- * still active after the turn has been processed by the dialog.
- */
- @Override
- public CompletableFuture beginDialog(DialogContext outerDc, Object options) {
-
- if (outerDc == null) {
- return Async.completeExceptionally(new IllegalArgumentException(
- "outerDc cannot be null."
- ));
- }
-
- ensureInitialized(outerDc).join();
-
- this.checkForVersionChange(outerDc).join();
-
- DialogContext innerDc = this.createChildContext(outerDc);
- DialogTurnResult turnResult = onBeginDialog(innerDc, options).join();
+ return ensureInitialized(outerDc).thenCompose(ensureResult -> {
+ return this.checkForVersionChange(outerDc).thenCompose(checkResult -> {
+ DialogContext innerDc = this.createChildContext(outerDc);
+ return onBeginDialog(innerDc, options).thenCompose(turnResult -> {
+ // Check for end of inner dialog
+ if (turnResult.getStatus() != DialogTurnStatus.WAITING) {
+ // Return result to calling dialog
+ return endComponent(outerDc, turnResult.getResult())
+ .thenCompose(result -> CompletableFuture.completedFuture(result));
+ }
+ getTelemetryClient().trackDialogView(getId(), null, null);
+ // Just signal waiting
+ return CompletableFuture.completedFuture(END_OF_TURN);
+ });
+ });
+ });
+ }
- // Check for end of inner dialog
- if (turnResult.getStatus() != DialogTurnStatus.WAITING) {
- // Return result to calling dialog
- DialogTurnResult result = endComponent(outerDc, turnResult.getResult()).join();
- return CompletableFuture.completedFuture(result);
- }
+ /**
+ * Called when the dialog is _continued_, where it is the active dialog and the
+ * user replies with a new activity.
+ *
+ * @param outerDc The parent {@link DialogContext} for the current turn of
+ * conversation.
+ *
+ * @return A {@link CompletableFuture} representing the hronous operation.
+ *
+ * If the task is successful, the result indicates whether the dialog is
+ * still active after the turn has been processed by the dialog. The
+ * result may also contain a return value. If this method is *not*
+ * overridden, the component dialog calls the
+ * {@link DialogContext#continueDialog(CancellationToken)} method on its
+ * inner dialog context. If the inner dialog stack is empty, the
+ * component dialog ends, and if a {@link DialogTurnResult#result} is
+ * available, the component dialog uses that as its return value.
+ */
+ @Override
+ public CompletableFuture continueDialog(DialogContext outerDc) {
+ return ensureInitialized(outerDc).thenCompose(ensureResult -> {
+ return this.checkForVersionChange(outerDc).thenCompose(checkResult -> {
+ // Continue execution of inner dialog
+ DialogContext innerDc = this.createChildContext(outerDc);
+ return this.onContinueDialog(innerDc).thenCompose(turnResult -> {
+ // Check for end of inner dialog
+ if (turnResult.getStatus() != DialogTurnStatus.WAITING) {
+ // Return to calling dialog
+ return this.endComponent(outerDc, turnResult.getResult())
+ .thenCompose(result -> CompletableFuture.completedFuture(result));
+ }
- getTelemetryClient().trackDialogView(getId(), null, null);
+ // Just signal waiting
+ return CompletableFuture.completedFuture(END_OF_TURN);
- // Just signal waiting
- return CompletableFuture.completedFuture(END_OF_TURN);
- }
+ });
+ });
- /**
- * Called when the dialog is _continued_, where it is the active dialog and the
- * user replies with a new activity.
- *
- * @param outerDc The parent {@link DialogContext} for the current turn of
- * conversation.
- *
- * @return A {@link CompletableFuture} representing the hronous operation.
- *
- * If the task is successful, the result indicates whether the dialog is
- * still active after the turn has been processed by the dialog. The
- * result may also contain a return value. If this method is *not*
- * overridden, the component dialog calls the
- * {@link DialogContext#continueDialog(CancellationToken)} method on its
- * inner dialog context. If the inner dialog stack is empty, the
- * component dialog ends, and if a {@link DialogTurnResult#result} is
- * available, the component dialog uses that as its return value.
- */
- @Override
- public CompletableFuture continueDialog(DialogContext outerDc) {
- ensureInitialized(outerDc).join();
-
- this.checkForVersionChange(outerDc).join();
-
- // Continue execution of inner dialog
- DialogContext innerDc = this.createChildContext(outerDc);
- DialogTurnResult turnResult = this.onContinueDialog(innerDc).join();
-
- // Check for end of inner dialog
- if (turnResult.getStatus() != DialogTurnStatus.WAITING) {
- // Return to calling dialog
- DialogTurnResult result = this.endComponent(outerDc, turnResult.getResult()).join();
- return CompletableFuture.completedFuture(result);
- }
+ });
+ }
- // Just signal waiting
- return CompletableFuture.completedFuture(END_OF_TURN);
- }
+ /**
+ * Called when a child dialog on the parent's dialog stack completed this turn,
+ * returning control to this dialog component.
+ *
+ * @param outerDc The {@link DialogContext} for the current turn of
+ * conversation.
+ * @param reason Reason why the dialog resumed.
+ * @param result Optional, value returned from the dialog that was called. The
+ * type of the value returned is dependent on the child dialog.
+ *
+ * @return A {@link CompletableFuture} representing the hronous operation.
+ *
+ * If the task is successful, the result indicates whether this dialog
+ * is still active after this dialog turn has been processed. Generally,
+ * the child dialog was started with a call to
+ * {@link BeginDialog(DialogContext, Object)} in the parent's context.
+ * However, if the {@link DialogContext#replaceDialog(String, Object)}
+ * method is called, the logical child dialog may be different than the
+ * original. If this method is *not* overridden, the dialog
+ * automatically calls its {@link RepromptDialog(TurnContext,
+ * DialogInstance)} when the user replies.
+ */
+ @Override
+ public CompletableFuture resumeDialog(DialogContext outerDc, DialogReason reason, Object result) {
+ return ensureInitialized(outerDc).thenCompose(ensureResult -> {
+ return this.checkForVersionChange(outerDc).thenCompose(versionCheckResult -> {
+ // Containers are typically leaf nodes on the stack but the dev is free to push
+ // other dialogs
+ // on top of the stack which will result in the container receiving an
+ // unexpected call to
+ // dialogResume() when the pushed on dialog ends.
+ // To avoid the container prematurely ending we need to implement this method
+ // and simply
+ // ask our inner dialog stack to re-prompt.
+ return repromptDialog(outerDc.getContext(), outerDc.getActiveDialog()).thenCompose(repromptResult -> {
+ return CompletableFuture.completedFuture(END_OF_TURN);
+ });
+ });
+ });
+ }
- /**
- * Called when a child dialog on the parent's dialog stack completed this turn,
- * returning control to this dialog component.
- *
- * @param outerDc The {@link DialogContext} for the current turn of
- * conversation.
- * @param reason Reason why the dialog resumed.
- * @param result Optional, value returned from the dialog that was called. The
- * type of the value returned is dependent on the child dialog.
- *
- * @return A {@link CompletableFuture} representing the hronous operation.
- *
- * If the task is successful, the result indicates whether this dialog
- * is still active after this dialog turn has been processed. Generally,
- * the child dialog was started with a call to
- * {@link BeginDialog(DialogContext, Object)} in the parent's context.
- * However, if the {@link DialogContext#replaceDialog(String, Object)}
- * method is called, the logical child dialog may be different than the
- * original. If this method is *not* overridden, the dialog
- * automatically calls its {@link RepromptDialog(TurnContext,
- * DialogInstance)} when the user replies.
- */
- @Override
- public CompletableFuture resumeDialog(DialogContext outerDc, DialogReason reason,
- Object result) {
-
- ensureInitialized(outerDc).join();
-
- this.checkForVersionChange(outerDc).join();
-
- // Containers are typically leaf nodes on the stack but the dev is free to push
- // other dialogs
- // on top of the stack which will result in the container receiving an
- // unexpected call to
- // dialogResume() when the pushed on dialog ends.
- // To avoid the container prematurely ending we need to implement this method
- // and simply
- // ask our inner dialog stack to re-prompt.
- repromptDialog(outerDc.getContext(), outerDc.getActiveDialog()).join();
- return CompletableFuture.completedFuture(END_OF_TURN);
- }
+ /**
+ * Called when the dialog should re-prompt the user for input.
+ *
+ * @param turnContext The context Object for this turn.
+ * @param instance State information for this dialog.
+ *
+ * @return A {@link CompletableFuture} representing the hronous operation.
+ */
+ @Override
+ public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) {
+ // Delegate to inner dialog.
+ DialogContext innerDc = this.createInnerDc(turnContext, instance);
+ return innerDc.repromptDialog().thenCompose(result -> onRepromptDialog(turnContext, instance));
+ }
- /**
- * Called when the dialog should re-prompt the user for input.
- *
- * @param turnContext The context Object for this turn.
- * @param instance State information for this dialog.
- *
- * @return A {@link CompletableFuture} representing the hronous operation.
- */
- @Override
- public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) {
- // Delegate to inner dialog.
+ /**
+ * Called when the dialog is ending.
+ *
+ * @param turnContext The context Object for this turn.
+ * @param instance State information associated with the instance of this
+ * component dialog on its parent's dialog stack.
+ * @param reason Reason why the dialog ended.
+ *
+ * @return A {@link CompletableFuture} representing the hronous operation.
+ *
+ * When this method is called from the parent dialog's context, the
+ * component dialog cancels all of the dialogs on its inner dialog stack
+ * before ending.
+ */
+ @Override
+ public CompletableFuture endDialog(TurnContext turnContext, DialogInstance instance, DialogReason reason) {
+ // Forward cancel to inner dialogs
+ if (reason == DialogReason.CANCEL_CALLED) {
DialogContext innerDc = this.createInnerDc(turnContext, instance);
- innerDc.repromptDialog().join();
-
- // Notify component
- return onRepromptDialog(turnContext, instance);
- }
-
- /**
- * Called when the dialog is ending.
- *
- * @param turnContext The context Object for this turn.
- * @param instance State information associated with the instance of this
- * component dialog on its parent's dialog stack.
- * @param reason Reason why the dialog ended.
- *
- * @return A {@link CompletableFuture} representing the hronous operation.
- *
- * When this method is called from the parent dialog's context, the
- * component dialog cancels all of the dialogs on its inner dialog stack
- * before ending.
- */
- @Override
- public CompletableFuture endDialog(TurnContext turnContext, DialogInstance instance,
- DialogReason reason) {
- // Forward cancel to inner dialogs
- if (reason == DialogReason.CANCEL_CALLED) {
- DialogContext innerDc = this.createInnerDc(turnContext, instance);
- innerDc.cancelAllDialogs().join();
- }
-
+ return innerDc.cancelAllDialogs().thenCompose(result -> onEndDialog(turnContext, instance, reason));
+ } else {
return onEndDialog(turnContext, instance, reason);
}
+ }
- /**
- * Adds a new {@link Dialog} to the component dialog and returns the updated
- * component.
- *
- * @param dialog The dialog to add.
- *
- * @return The {@link ComponentDialog} after the operation is complete.
- *
- * The added dialog's {@link Dialog#telemetryClient} is set to the
- * {@link DialogContainer#telemetryClient} of the component dialog.
- */
- public ComponentDialog addDialog(Dialog dialog) {
- this.getDialogs().add(dialog);
-
- if (this.getInitialDialogId() == null) {
- this.setInitialDialogId(dialog.getId());
- }
+ /**
+ * Adds a new {@link Dialog} to the component dialog and returns the updated
+ * component.
+ *
+ * @param dialog The dialog to add.
+ *
+ * @return The {@link ComponentDialog} after the operation is complete.
+ *
+ * The added dialog's {@link Dialog#telemetryClient} is set to the
+ * {@link DialogContainer#telemetryClient} of the component dialog.
+ */
+ public ComponentDialog addDialog(Dialog dialog) {
+ this.getDialogs().add(dialog);
- return this;
+ if (this.getInitialDialogId() == null) {
+ this.setInitialDialogId(dialog.getId());
}
- /**
- * Creates an inner {@link DialogContext} .
- *
- * @param dc The parent {@link DialogContext} .
- *
- * @return The created Dialog Context.
- */
- @Override
- public DialogContext createChildContext(DialogContext dc) {
- return this.createInnerDc(dc, dc.getActiveDialog());
- }
+ return this;
+ }
- /**
- * Ensures the dialog is initialized.
- *
- * @param outerDc The outer {@link DialogContext} .
- *
- * @return A {@link CompletableFuture} representing the hronous operation.
- */
- protected CompletableFuture ensureInitialized(DialogContext outerDc) {
- if (!this.initialized) {
- this.initialized = true;
- onInitialize(outerDc).join();
- }
+ /**
+ * Creates an inner {@link DialogContext} .
+ *
+ * @param dc The parent {@link DialogContext} .
+ *
+ * @return The created Dialog Context.
+ */
+ @Override
+ public DialogContext createChildContext(DialogContext dc) {
+ return this.createInnerDc(dc, dc.getActiveDialog());
+ }
+
+ /**
+ * Ensures the dialog is initialized.
+ *
+ * @param outerDc The outer {@link DialogContext} .
+ *
+ * @return A {@link CompletableFuture} representing the hronous operation.
+ */
+ protected CompletableFuture ensureInitialized(DialogContext outerDc) {
+ if (!this.initialized) {
+ this.initialized = true;
+ return onInitialize(outerDc).thenApply(result -> null);
+ } else {
return CompletableFuture.completedFuture(null);
}
+ }
- /**
- * Initilizes the dialog.
- *
- * @param dc The {@link DialogContext} to initialize.
- *
- * @return A {@link CompletableFuture} representing the hronous operation.
- */
- protected CompletableFuture onInitialize(DialogContext dc) {
- if (this.getInitialDialogId() == null) {
- Collection