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

Commit 6ef7451

Browse files
committed
Initial iFrame based Teams Task Module sample
1 parent 5f345d5 commit 6ef7451

File tree

20 files changed

+1844
-82
lines changed

20 files changed

+1844
-82
lines changed

libraries/bot-builder/src/main/java/com/microsoft/bot/builder/MessageFactory.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ public static Activity attachment(Attachment attachment) {
201201
return attachment(attachment, null, null, null);
202202
}
203203

204+
/**
205+
* Returns a message activity that contains an attachment.
206+
*
207+
* @param attachments Attachments to include in the message.
208+
* @return A message activity containing the attachment.
209+
*/
210+
public static Activity attachment(List<Attachment> attachments) {
211+
return attachment(attachments, null, null, null);
212+
}
213+
204214
/**
205215
* Returns a message activity that contains an attachment.
206216
*
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.bot.integration;
5+
6+
import java.util.concurrent.CompletableFuture;
7+
import java.util.concurrent.CompletionException;
8+
9+
/**
10+
* Asyc and CompletableFuture helpers methods.
11+
*/
12+
public final class Async {
13+
private Async() {
14+
15+
}
16+
17+
/**
18+
* Executes a block and throws a completion exception if needed.
19+
*
20+
* @param supplier The block to execute.
21+
* @param <T> The type of the return value.
22+
* @return The return value.
23+
*/
24+
public static <T> T tryThrow(ThrowSupplier<T> supplier) {
25+
try {
26+
return supplier.get();
27+
} catch (CompletionException ce) {
28+
throw ce;
29+
} catch (Throwable t) {
30+
throw new CompletionException(t);
31+
}
32+
}
33+
34+
/**
35+
* Executes a block and returns a CompletableFuture with either the return
36+
* value or the exception (completeExceptionally).
37+
*
38+
* @param supplier The block to execute.
39+
* @param <T> The type of the CompletableFuture value.
40+
* @return The CompletableFuture
41+
*/
42+
public static <T> CompletableFuture<T> tryCompletion(ThrowSupplier<T> supplier) {
43+
CompletableFuture<T> result = new CompletableFuture<>();
44+
45+
try {
46+
result.complete(supplier.get());
47+
} catch (Throwable t) {
48+
result.completeExceptionally(t);
49+
}
50+
51+
return result;
52+
}
53+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.bot.integration;
5+
6+
/**
7+
* A Supplier that throws.
8+
* @param <T> The type of the Supplier return value.
9+
*/
10+
@FunctionalInterface
11+
public interface ThrowSupplier<T> {
12+
/**
13+
* Gets a result.
14+
*
15+
* @return a result
16+
* @throws Throwable Any exception
17+
*/
18+
T get() throws Throwable;
19+
}

libraries/bot-schema/src/main/java/com/microsoft/bot/schema/Serialization.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public static String toString(Object source) throws JsonProcessingException {
111111
* Parses a JSON document.
112112
*
113113
* @param json The JSON to parse.
114-
* @return A JsonNode containg the node tree.
114+
* @return A JsonNode containing the node tree.
115115
* @throws IOException Error parsing json.
116116
*/
117117
public static JsonNode jsonToTree(String json) throws IOException {

samples/54.teams-task-module/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ the Teams service needs to call into the bot.
3333

3434
1) Update the `resources/application.properties` configuration for the bot to use the Microsoft App Id and App Password from the Bot Framework registration. (Note the App Password is referred to as the "client secret" in the azure portal and you can always create a new client secret anytime.)
3535

36+
1) Update `CustomForm.html` to replace your Microsoft App Id *everywhere* you see the place holder string `<<YOUR-MICROSOFT-APP-ID>>`
37+
3638
1) __*This step is specific to Teams.*__
37-
- **Edit** the `manifest.json` contained in the `teamsAppManifest` folder to replace your Microsoft App Id (that was created when you registered your bot earlier) *everywhere* you see the place holder string `<<YOUR-MICROSOFT-APP-ID>>` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`)
39+
- **Edit** the `manifest.json` contained in the `teamsAppManifest` folder to replace your Microsoft App Id (that was created when you registered your bot earlier) *everywhere* you see the place holder string `<<YOUR-MICROSOFT-APP-ID>>` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`). **Note:** the Task Modules containing pages will require the deployed bot's domain in validDomains of the manifest.
3840
- **Zip** up the contents of the `teamsAppManifest` folder to create a `manifest.zip`
3941
- **Upload** the `manifest.zip` to Teams (in the Apps view click "Upload a custom app")
4042

samples/54.teams-task-module/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
<dependency>
7878
<groupId>com.microsoft.bot</groupId>
7979
<artifactId>bot-integration-spring</artifactId>
80-
<version>4.0.0-SNAPSHOT</version>
80+
<version>4.6.0-preview4</version>
8181
<scope>compile</scope>
8282
</dependency>
8383
<dependency>

samples/54.teams-task-module/src/main/java/com/microsoft/bot/sample/teamstaskmodule/TeamsTaskModuleBot.java

Lines changed: 132 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,31 @@
33

44
package com.microsoft.bot.sample.teamstaskmodule;
55

6-
import com.fasterxml.jackson.core.JsonProcessingException;
76
import com.fasterxml.jackson.databind.ObjectMapper;
87
import com.fasterxml.jackson.databind.node.ObjectNode;
98
import com.microsoft.bot.builder.MessageFactory;
109
import com.microsoft.bot.builder.TurnContext;
1110
import com.microsoft.bot.builder.teams.TeamsActivityHandler;
12-
import com.microsoft.bot.schema.Activity;
11+
import com.microsoft.bot.integration.Async;
12+
import com.microsoft.bot.integration.Configuration;
13+
import com.microsoft.bot.sample.teamstaskmodule.models.AdaptiveCardTaskFetchValue;
14+
import com.microsoft.bot.sample.teamstaskmodule.models.CardTaskFetchValue;
15+
import com.microsoft.bot.sample.teamstaskmodule.models.TaskModuleIds;
16+
import com.microsoft.bot.sample.teamstaskmodule.models.TaskModuleResponseFactory;
17+
import com.microsoft.bot.sample.teamstaskmodule.models.TaskModuleUIConstants;
18+
import com.microsoft.bot.sample.teamstaskmodule.models.UISettings;
1319
import com.microsoft.bot.schema.Attachment;
20+
import com.microsoft.bot.schema.CardAction;
1421
import com.microsoft.bot.schema.HeroCard;
22+
import com.microsoft.bot.schema.Serialization;
1523
import com.microsoft.bot.schema.teams.*;
24+
import java.io.IOException;
25+
import java.util.ArrayList;
26+
import java.util.HashMap;
27+
import java.util.Map;
1628
import java.util.concurrent.CompletionException;
29+
import java.util.stream.Collectors;
1730
import org.apache.commons.io.IOUtils;
18-
import org.json.JSONObject;
1931
import org.springframework.stereotype.Component;
2032

2133
import java.io.InputStream;
@@ -34,23 +46,29 @@
3446
*/
3547
@Component
3648
public class TeamsTaskModuleBot extends TeamsActivityHandler {
49+
private final String baseUrl;
50+
private final List<UISettings> actions = Arrays.asList(
51+
TaskModuleUIConstants.ADAPTIVECARD,
52+
TaskModuleUIConstants.CUSTOMFORM,
53+
TaskModuleUIConstants.YOUTUBE
54+
);
3755

38-
@Override
39-
protected CompletableFuture<Void> onTeamsMembersAdded(
40-
List<TeamsChannelAccount> membersAdded,
41-
TeamInfo teamInfo,
42-
TurnContext turnContext
43-
) {
44-
return turnContext.sendActivity(MessageFactory.attachment(getTaskModuleHeroCard()))
45-
.thenApply(resourceResponse -> null);
56+
public TeamsTaskModuleBot(Configuration configuration) {
57+
baseUrl = configuration.getProperty("BaseUrl");
4658
}
4759

4860
@Override
4961
protected CompletableFuture<Void> onMessageActivity(
5062
TurnContext turnContext
5163
) {
52-
Attachment attachment = getTaskModuleHeroCard();
53-
return turnContext.sendActivity(MessageFactory.attachment(attachment))
64+
// This displays two cards: A HeroCard and an AdaptiveCard. Both have the same
65+
// options. When any of the options are selected, `onTeamsTaskModuleFetch`
66+
// is called.
67+
return turnContext.sendActivity(MessageFactory.attachment(Arrays.asList(
68+
getTaskModuleHeroCardOptions(),
69+
getTaskModuleAdaptiveCardOptions()
70+
))
71+
)
5472
.thenApply(resourceResponse -> null);
5573
}
5674

@@ -59,92 +77,127 @@ protected CompletableFuture<TaskModuleResponse> onTeamsTaskModuleFetch(
5977
TurnContext turnContext,
6078
TaskModuleRequest taskModuleRequest
6179
) {
62-
Activity reply;
63-
try {
64-
reply = MessageFactory.text(
65-
"onTeamsTaskModuleFetch TaskModuleRequest: " +
66-
new ObjectMapper().writeValueAsString(taskModuleRequest));
67-
} catch (JsonProcessingException e) {
68-
CompletableFuture<TaskModuleResponse> result = new CompletableFuture<>();
69-
result.completeExceptionally(new CompletionException(e));
70-
return result;
71-
}
80+
// Called when the user selects an options from the displayed HeroCard or
81+
// AdaptiveCard. The result is the action to perform.
82+
return Async.tryCompletion(() -> {
83+
CardTaskFetchValue<String> value = Serialization
84+
.safeGetAs(taskModuleRequest.getData(), CardTaskFetchValue.class);
85+
86+
TaskModuleTaskInfo taskInfo = new TaskModuleTaskInfo();
87+
switch (value.getData()) {
88+
// Display the YouTube.html page
89+
case TaskModuleIds.YOUTUBE: {
90+
String url = baseUrl + "/" + TaskModuleIds.YOUTUBE;
91+
taskInfo.setUrl(url);
92+
taskInfo.setFallbackUrl(url);
93+
setTaskInfo(taskInfo, TaskModuleUIConstants.YOUTUBE);
94+
break;
95+
}
96+
97+
// Display the CustomForm.html page, and post the form data back via
98+
// onTeamsTaskModuleSubmit.
99+
case TaskModuleIds.CUSTOMFORM: {
100+
String url = baseUrl + "/" + TaskModuleIds.CUSTOMFORM;
101+
taskInfo.setUrl(url);
102+
taskInfo.setFallbackUrl(url);
103+
setTaskInfo(taskInfo, TaskModuleUIConstants.CUSTOMFORM);
104+
break;
105+
}
72106

73-
return turnContext.sendActivity(reply)
74-
.thenApply(resourceResponse -> {
75-
Attachment adaptiveCard = getTaskModuleAdaptiveCard();
76-
77-
return new TaskModuleResponse() {{
78-
setTask(new TaskModuleContinueResponse() {{
79-
setType("continue");
80-
setValue(new TaskModuleTaskInfo() {{
81-
setCard(adaptiveCard);
82-
setHeight(200);
83-
setWidth(400);
84-
setTitle("Adaptive Card: Inputs");
85-
}});
86-
}});
87-
}};
88-
});
107+
// Display an AdaptiveCard to prompt user for text, and post it back via
108+
// onTeamsTaskModuleSubmit.
109+
case TaskModuleIds.ADAPTIVECARD:
110+
taskInfo.setCard(createAdaptiveCardAttachment());
111+
setTaskInfo(taskInfo, TaskModuleUIConstants.ADAPTIVECARD);
112+
break;
113+
114+
default:
115+
break;
116+
}
117+
118+
return taskInfo;
119+
})
120+
.thenApply(TaskModuleResponseFactory::toTaskModuleResponse);
89121
}
90122

91123
@Override
92124
protected CompletableFuture<TaskModuleResponse> onTeamsTaskModuleSubmit(
93125
TurnContext turnContext,
94126
TaskModuleRequest taskModuleRequest
95127
) {
96-
Activity reply;
97-
try {
98-
reply = MessageFactory.text(
99-
"onTeamsTaskModuleSubmit TaskModuleRequest: " +
100-
new ObjectMapper().writeValueAsString(taskModuleRequest));
101-
} catch (JsonProcessingException e) {
102-
CompletableFuture<TaskModuleResponse> result = new CompletableFuture<>();
103-
result.completeExceptionally(new CompletionException(e));
104-
return result;
105-
}
106-
107-
turnContext.sendActivity(reply)
108-
.thenApply(resourceResponse -> null);
128+
// Called when data is being returned from the selected option (see `onTeamsTaskModuleFetch').
129+
return Async.tryCompletion(() ->
130+
// Echo the users input back. In a production bot, this is where you'd add behavior in
131+
// response to the input.
132+
MessageFactory.text(
133+
"onTeamsTaskModuleSubmit TaskModuleRequest: "
134+
+ new ObjectMapper().writeValueAsString(taskModuleRequest)
135+
)
136+
)
137+
.thenCompose(turnContext::sendActivity)
138+
.thenApply(resourceResponse -> TaskModuleResponseFactory.createResponse("Thanks!"));
139+
}
109140

110-
return CompletableFuture.completedFuture(new TaskModuleResponse() {{
111-
setTask(new TaskModuleMessageResponse() {{
112-
setType("message");
113-
setValue("Thanks!");
114-
}});
115-
}});
141+
private void setTaskInfo(TaskModuleTaskInfo taskInfo, UISettings uiSettings) {
142+
taskInfo.setHeight(uiSettings.getHeight());
143+
taskInfo.setWidth(uiSettings.getWidth());
144+
taskInfo.setTitle(uiSettings.getTitle());
116145
}
117146

118-
private Attachment getTaskModuleHeroCard() {
147+
private Attachment getTaskModuleHeroCardOptions() {
148+
List<CardAction> buttons = actions.stream().map(cardType ->
149+
new TaskModuleAction(cardType.getButtonTitle(),
150+
new CardTaskFetchValue<String>() {{
151+
setData(cardType.getId());
152+
}})
153+
).collect(Collectors.toCollection(ArrayList::new));
154+
119155
HeroCard card = new HeroCard() {{
120156
setTitle("Task Module Invocation from Hero Card");
121-
setSubtitle(
122-
"This is a hero card with a Task Module Action button. Click the button to show an Adaptive Card within a Task Module.");
123-
setButtons(Arrays.asList(
124-
new TaskModuleAction(
125-
"Adaptive Card",
126-
new JSONObject().put(
127-
"data",
128-
"adaptivecard"
129-
).toString()
130-
)
131-
));
157+
setButtons(buttons);
132158
}};
133159
return card.toAttachment();
134160
}
135161

136-
private Attachment getTaskModuleAdaptiveCard() {
137-
try {
138-
InputStream inputStream = getClass().getClassLoader()
139-
.getResourceAsStream("adaptivecard.json");
140-
String result = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
162+
private Attachment getTaskModuleAdaptiveCardOptions() {
163+
try (InputStream inputStream = getClass().getClassLoader()
164+
.getResourceAsStream("adaptiveTemplate.json")
165+
) {
166+
String cardTemplate = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
167+
168+
List<Map<String,Object>> cardActions = actions.stream().map(cardType -> {
169+
Map<String, Object> a = new HashMap<>();
170+
a.put("type", "Action.Submit");
171+
a.put("title", cardType.getButtonTitle());
172+
a.put("data", new AdaptiveCardTaskFetchValue<String>() {{
173+
setData(cardType.getId());
174+
}});
175+
return a;
176+
}).collect(Collectors.toCollection(ArrayList::new));
177+
178+
String adaptiveCardJson = String.format(cardTemplate, Serialization.toString(cardActions));
141179

142-
return new Attachment() {{
143-
setContentType("application/vnd.microsoft.card.adaptive");
144-
setContent(new ObjectMapper().readValue(result, ObjectNode.class));
145-
}};
180+
return adaptiveCardAttachmentFromJson(adaptiveCardJson);
146181
} catch (Throwable t) {
147182
throw new CompletionException(t);
148183
}
149184
}
185+
186+
private Attachment createAdaptiveCardAttachment() {
187+
try (InputStream inputStream = getClass().getClassLoader()
188+
.getResourceAsStream("adaptivecard.json")
189+
) {
190+
String result = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
191+
return adaptiveCardAttachmentFromJson(result);
192+
} catch (Throwable t) {
193+
throw new CompletionException(t);
194+
}
195+
}
196+
197+
private Attachment adaptiveCardAttachmentFromJson(String json) throws IOException {
198+
return new Attachment() {{
199+
setContentType("application/vnd.microsoft.card.adaptive");
200+
setContent(new ObjectMapper().readValue(json, ObjectNode.class));
201+
}};
202+
}
150203
}

0 commit comments

Comments
 (0)