This article demonstrates how to create a Spring Boot web app in Java that replicates the functionality of the “Messaging extensions - search” sample app on GitHub.
When developers build extensions for any application, they’d like to embed the custom functionality into the host application’s native user interface (UI). The same is true of Microsoft Teams, where your organization works and collaborates daily. You can ensure the transition to your helpful productivity app is as seamless as possible.
Specifically, you can create buttons or other controls that look like part of the host app, and the user can interact with your existing or new web services through those host controls. Your web service then handles the requests users send using those controls and produces the natural response for the given host app. You can achieve this embedded functionality in Microsoft Teams using messaging extensions.
We’ll explore embedding a Java-based application in Microsoft Teams in this three-part series. First, I’ll highlight how messaging extensions work in Microsoft Teams and build a messaging extension for search. Later, I’ll demonstrate building a link unfurling messaging extension and a notification connector.
Exploring Messaging Extensions in Microsoft Teams
Like any other Microsoft Teams app, a messaging extension comprises two elements: an existing or new web service and the app manifest. You implement the web service using your favorite tool.
You can specify up to ten commands in the app manifest, which act as shortcuts to your web service. You set the type and location in Microsoft Teams for each command where a user can invoke the command, like the compose message area, command bar, or a message.
Teams sends a JSON payload to your web service when the user invokes the command. You can then implement a custom login within your web service to handle the request and produce the response. You can use cards to display information to the user, such as visuals, text, or audio that accelerate conversations and user actions.
Microsoft Teams uses the Bot Framework to communicate with your web service. So, from our developer perspective, implementing the messaging extension involves implementing additional wrappers for the web service using the Bot Framework SDK.
I’ll show you how to implement a messaging extension for Microsoft Teams using the Java Spring MVC framework for a web service. The web service will return a response in the form of a card.
In this tutorial, I’ll test the entire solution using the local environment. However, you can also deploy the web service to Azure Spring Cloud.
To reproduce the example, you should install ngrok and prepare your environment.
You can find the companion code on GitHub.
Creating a Project
Start by creating the project using the Bot Framework Echo project template. Use the Yeoman generator:
yo botbuilder-java
Then configure your bot as follows:
- Name:
msg-extension-search
- Package name:
db.teamsext.search
- Template: Echo Bot
The command above generates the project template. This template has a structure you can use for developing bots. Namely, the EchoBot
class extends the ActivityHandler
. The Echo Bot echos every message you send to the bot. Use this template for the Echo Bot:
public class EchoBot extends ActivityHandler {
@Override
protected CompletableFuture<Void> onMessageActivity(TurnContext turnContext) {
return turnContext.sendActivity(
MessageFactory.text("Echo: " + turnContext.getActivity().getText())
).thenApply(sendResult -> null);
}
@Override
protected CompletableFuture<Void> onMembersAdded(
List<ChannelAccount> membersAdded,
TurnContext turnContext
) {
return membersAdded.stream()
.filter(
member -> !StringUtils
.equals(member.getId(), turnContext.getActivity().getRecipient().getId())
).map(channel -> turnContext.sendActivity
(MessageFactory.text("Hello and welcome!")))
.collect(CompletableFutures.toFutureList()).thenApply(resourceResponses -> null);
}
}
Before proceeding further, we need to register the bot and prepare the app manifest.
Registering and Configuring the Bot
We register the bot to ensure secure communication. You can use the App Studio in Microsoft Teams or the Developer Portal to register the bot. If you don’t already have a developer account, register one for free.
Here, I use the App Studio to register the bot. First, click Bot Management:
Next, type msg-extension-bot
, then click Create. This action creates the application ID and password.
Now, paste those credentials into the resources/application.json file of the project you created in the previous step (msg-extension-search
):
Next, run ngrok
:
ngrok http -host-header=rewrite 3978
Note that the port should match the server.port
property from application.properties.json. Here, I use a default value of 3978. The ngrok
should report the forwarding URL.
In my case, the value is https://e1fa-46-229-154-234.ngrok.io. We use this value to configure the bot. So, let’s go back to the Microsoft Teams manifest and update the bot endpoint address to: https://e1fa-46-229-154-234.ngrok.io/api/messages. Press Enter to save the changes.
Making the Application Manifest
After registering the bot, we can create the application manifest. So, in App Studio, click Create a new application. Then, populate the app details as follows:
- Short name:
msg-extension-search
- Full name: Messaging Extension Search
- App ID: Click Generate in the Identification group to create a new ID
- Package name:
db.teams.msgextensionsearch
- Version: 1.0.0
- Descriptions: Type any value
- Developer information: Type any value, for example, Contoso, Inc., and https://www.teams.com
- App URLs:
- Privacy statement: https://example.azurewebsites.net or any other valid URL
- Terms of use: https://example.azurewebsites.net or any valid URL
Then, go to Messaging Extensions and click Set Up. This action activates the popup window, where you click Use existing bot, click the Select from one of my existing bots radio button and choose msg-extension-bot from the dropdown list. For the bot name, type: msg-extension-bot
.
Click Save. The following screen then displays:
Now, click Add under Command. App Studio will display the new popup:
Next, choose the first option: Allow users to query your service for information and insert that into a message. This option enables your users to type the search query that the app will send to the Bot.
After, the App Manifest displays a New command screen, where you provide the following:
- Command Id:
searchQuery
- Title: Search
- Context: Command Box, Compose Box
- Parameter:
- Name:
searchQuery
- Title: Search Query
- Description: Your search query
- Type: Text
Finally, click Save. Wait for the popup to close, then go to Test and distribute in the Finish tab. Click Install and add the app to Microsoft Teams.
Adding the Echo Messaging Extension
Now, let’s implement the web service. It will retrieve the search query from the messaging extension then produce the card with the title set to “search query.”
First, open the EchoBot.java file and modify the class declaration as follows (replace ActivityHandler
with TeamsActivityHandler
):
public class EchoBot extends TeamsActivityHandler
Then, add the following method:
@Override
protected CompletableFuture<MessagingExtensionResponse> onTeamsMessagingExtensionQuery(
TurnContext turnContext,
MessagingExtensionQuery query
) {
String queryText = GetQueryText(query);
HeroCard card = new HeroCard();
card.setTitle("Echo");
card.setSubtitle(queryText);
card.setText("This sample is a sample hero card");
MessagingExtensionAttachment attachment = new MessagingExtensionAttachment();
attachment.setContent(card);
attachment.setContentType(HeroCard.CONTENTTYPE);
attachment.setPreview(card.toAttachment());
MessagingExtensionResult result = new MessagingExtensionResult();
result.setAttachmentLayout("list");
result.setType("result");
result.setAttachment(attachment);
return CompletableFuture.completedFuture(
new MessagingExtensionResponse(result));
}
The above method reads the search query (the value Microsoft Teams sends to the messaging extension) and stores it under the queryText
local variable. Then, this value populates the hero card, displaying one item. The card will have the Echo title and show the value from the queryText
variable as the subtitle. Additionally, the “This is a sample hero card
” string displays as the screenshot below shows:
To get the query text, use the helper method, GetQueryText
:
private String GetQueryText(MessagingExtensionQuery query) {
String queryText = "Empty query";
if (query != null && query.getParameters() != null) {
List<MessagingExtensionParameter> queryParams = query.getParameters();
if (!queryParams.isEmpty()) {
MessagingExtensionParameter firstParam = queryParams.get(0);
if(firstParam.getName().equals("searchQuery")) {
queryText = (String) queryParams.get(0).getValue();
}
}
}
return queryText;
}
This method accepts an instance of the MessagingExtensionQuery
class. This class represents the payload Microsoft Teams sends to your bot through the messaging extension. In particular, this payload includes the following:
commandId
– The command’s identifier. This identifier corresponds to what we declared earlier in the application manifest. parameters
– The query parameters. queryOptions
– The query options. state
– Teams passes this parameter back to the bot after authentication or configuration flow.
We configured the messaging extension such that it handles only one command. This command has only one parameter (searchQuery
). After validating the input, the GetQueryText
method looks for the searchQuery
parameter, then reads its value. We check if the parameter name matches the expected string because when you open the messaging extension for the first time, you get the initialRun
parameter with the Boolean value.
Finally, note that onTeamsMessagingExtensionQuery
returns an instance of CompletableFuture
with the MessagingExtensionResponse
. We create MessagingExtensionResponse
using a default constructor.
Then, we supplement the MessagingExtensionResponse
instance by attaching the hero card. We abstract the attachment with the MessagingExtensionAttachment
class instance. We set the content and preview based on the created HeroCard
class instance.
To test the above solution, launch the web service. Then, open Microsoft Teams. In the compose area, click the triple dots icon and look up the msg-extension-search
:
Click msg-extension-search and type your search query. Note that the search query passes to your web service, where you can process it and generate results. Here, we only read the search query and create the hero card. So, the final result will look like the screenshot below:
Creating the Messaging Extension with Search
Finally, we reproduce the C# messaging extension with search. The code takes the search query to look up the NuGet packages. To this end, the messaging extension sends the HTTP GET request via this link:
https://azuresearch-usnc.nuget.org/query?q=<search_query>&prerelease=true
The above Azure search responds with a JSON-formatted document comprising the NuGet package list. For example, the query https://azuresearch-usnc.nuget.org/query?q=sqlite&prerelease=true returns the JSON in the screenshot below:
We can use the findPackage
method to do the same in Java:
private CompletableFuture<List<String[]>> findPackages(String text) {
return CompletableFuture.supplyAsync(() -> {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(String
.format(
"https://azuresearch-usnc.nuget.org/query?q=id:%s&prerelease=true",
text
))
.build();
List<String[]> filteredItems = new ArrayList<>();
try {
Response response = client.newCall(request).execute();
JsonNode obj = Serialization.jsonToTree(response.body().string());
ArrayNode dataArray = (ArrayNode) obj.get("data");
for (int i = 0; i < dataArray.size(); i++) {
JsonNode item = dataArray.get(i);
filteredItems.add(new String[] {
item.get("id").asText(),
item.get("version").asText(),
item.get("description").asText(),
item.has("projectUrl") ? item.get("projectUrl").asText() : "",
item.has("iconUrl") ? item.get("iconUrl").asText() : ""
});
}
} catch (IOException e) {
LoggerFactory.getLogger(EchoBot.class)
.error("findPackages", e);
throw new CompletionException(e);
}
return filteredItems;
});
}
Then, we invoke the above method within onTeamsMessagingExtensionQuery
as follows:
@Override
protected CompletableFuture<MessagingExtensionResponse> onTeamsMessagingExtensionQuery(
TurnContext turnContext,
MessagingExtensionQuery query
) {
String text = "";
if (query != null && query.getParameters() != null) {
List<MessagingExtensionParameter> queryParams = query.getParameters();
if (!queryParams.isEmpty()) {
text = (String) queryParams.get(0).getValue();
}
}
return findPackages(text)
.thenApply(packages -> {
List<MessagingExtensionAttachment> attachments = new ArrayList<>();
for (String[] item : packages) {
ObjectNode data = Serialization.createObjectNode();
data.set("data", Serialization.objectToTree(item));
CardAction cardAction = new CardAction();
cardAction.setType(ActionTypes.INVOKE);
cardAction.setValue(Serialization.toStringSilent(data));
ThumbnailCard previewCard = new ThumbnailCard();
previewCard.setTitle(item[0]);
previewCard.setTap(cardAction);
if (!StringUtils.isEmpty(item[4])) {
CardImage cardImage = new CardImage();
cardImage.setUrl(item[4]);
cardImage.setAlt("Icon");
previewCard.setImages(Collections.singletonList(cardImage));
}
HeroCard heroCard = new HeroCard();
heroCard.setTitle(item[0]);
MessagingExtensionAttachment attachment =
new MessagingExtensionAttachment();
attachment.setContentType(HeroCard.CONTENTTYPE);
attachment.setContent(heroCard);
attachment.setPreview(previewCard.toAttachment());
attachments.add(attachment);
}
MessagingExtensionResult composeExtension = new MessagingExtensionResult();
composeExtension.setType("result");
composeExtension.setAttachmentLayout("list");
composeExtension.setAttachments(attachments);
return new MessagingExtensionResponse(composeExtension);
});
}
Additionally, we can respond to user actions using onTeamsMessagingExtensionSelectItem
:
@Override
protected CompletableFuture<MessagingExtensionResponse> onTeamsMessagingExtensionSelectItem(
TurnContext turnContext,
Object query
) {
Map cardValue = (Map) query;
List<String> data = (ArrayList) cardValue.get("data");
CardAction cardAction = new CardAction();
cardAction.setType(ActionTypes.OPEN_URL);
cardAction.setTitle("Project");
cardAction.setValue(data.get(3));
ThumbnailCard card = new ThumbnailCard();
card.setTitle(data.get(0));
card.setSubtitle(data.get(2));
card.setButtons(Arrays.asList(cardAction));
if (StringUtils.isNotBlank(data.get(4))) {
CardImage cardImage = new CardImage();
cardImage.setUrl(data.get(4));
cardImage.setAlt("Icon");
card.setImages(Collections.singletonList(cardImage));
}
MessagingExtensionAttachment attachment = new MessagingExtensionAttachment();
attachment.setContentType(ThumbnailCard.CONTENTTYPE);
attachment.setContent(card);
MessagingExtensionResult composeExtension = new MessagingExtensionResult();
composeExtension.setType("result");
composeExtension.setAttachmentLayout("list");
composeExtension.setAttachments(Collections.singletonList(attachment));
return CompletableFuture.completedFuture(new MessagingExtensionResponse(composeExtension));
}
After running the above code, you’ll see the list of NuGet packages:
You can click on any item you want. The messaging extension populates the message compose area with details about the selected NuGet package:
Next Steps
In this tutorial, I demonstrated the steps to build the messaging extension for Microsoft Teams. We learned that the messaging extension uses the Microsoft Bot framework for securing communication with the external web service. The response the bot produces can be composed of cards.
Continue to the second part of this three-part series to learn how to build a link unfurling messaging extension.
To learn how you can build, migrate and scale Java applications on Azure using Azure services, check out Get started with Java on Azure.