This is Part 3 of a 3-part series that demonstrates how to build Microsoft Teams apps in Java, with a focus on bots. This article shows how to create a bot that downloads and processes file attachments in chat messages.
Chatbots provide a convenient platform when working with file upload. Chat platforms like Microsoft Teams already expose a familiar interface for attaching files to messages, as well as taking care of hosting any message attachments. This relieves us of having to code the rather mundane, but still tricky logic that would otherwise be required to handle file uploads ourselves.
In this post, we’ll create a bot that downloads and processes file attachments in chat messages.
Prerequisites
To follow along in this article, you’ll need a Java 11 JDK, Apache Maven, Node.js and npm, an Azure subscription to deploy the final application, and the Azure CLI.
I’ve also used a scaffolding tool, Yeoman, to simplify setup. Install it with the following command:
npm install -g yo
The chatbot template is provided by the generator-botbuilder-java
package, which you’ll need to install with this command:
npm install -g generator-botbuilder-java
You now have everything you need to create your sample chatbot application.
Example Source Code
You can follow along by examining this project’s source code on its GitHub page.
Build the Base Application
Refer to the previous article in this series for the process of building and updating the base application. Follow the instructions under the headings “Creating the Sample Application” and “Updating the Sample Application” to create a Spring Boot project using Yeoman.
Add New Dependencies
In addition to the updated dependencies mentioned in the previous article, you also need to add Maven dependencies to support your file upload bot.
Add the following dependencies to the pom.xml file:
<dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
…
</dependencies>
Build the Upload Bot
Your bot is defined in the UploadBot
class and extends the ActivityHandler
class:
public class UploadBot extends ActivityHandler {
The onMembersAdded
method displays a message to any new chat members informing them that the bot will download files from message attachments:
@Override
protected CompletableFuture<Void> onMembersAdded(
final List<ChannelAccount> membersAdded,
final TurnContext turnContext
) {
return membersAdded.stream()
.filter(
member -> !StringUtils
.equals(member.getId(), turnContext.getActivity().getRecipient().getId())
).map(channel -> turnContext.sendActivity(
MessageFactory.text
("Welcome! Post a message with an attachment and I'll download it!")))
.collect(CompletableFutures.toFutureList()).thenApply(resourceResponses -> null);
}
The onMessageActivity
method inspects any new messages to determine if they have attachments:
@Override
protected CompletableFuture<Void> onMessageActivity(final TurnContext turnContext) {
if (messageWithDownload(turnContext.getActivity())) {
final Attachment attachment = turnContext.getActivity().getAttachments().get(0);
If an attachment was found, the file is downloaded to a local temporary location:
return downloadAttachment(attachment)
If there were any issues with the file download, a message is posted to the chat:
.thenCompose(result -> !result.result()
? turnContext.sendActivityBlind(
MessageFactory.text("Failed to download the attachment"))
If the attachment was successfully downloaded, it is inspected, and the file length and type are posted to the chat:
: turnContext.sendActivityBlind(
MessageFactory.text(
"Downloaded file " + attachment.getName() + ". It was "
+ getFileSize(result.getRight())
+ " bytes long and appears to be of type "
+ getFileType(result.getRight())))
);
}
If no attachments were found, the bot posts a reminder to the chat informing users that it will download any attachments:
return turnContext.sendActivity(
MessageFactory.text("Post a message with an attachment and I'll download it!")
).thenApply(sendResult -> null);
}
To determine if a message has an attachment, the messageWithDownload
method checks the attachments collection and verifies the content type of the first attachment:
private boolean messageWithDownload(final Activity activity) {
return activity.getAttachments() != null
&& activity.getAttachments().size() > 0
&& StringUtils.equalsIgnoreCase(
activity.getAttachments().get(0).getContentType(),
FileDownloadInfo.CONTENT_TYPE);
}
Downloading an attachment is similar to downloading any file from an HTTP server. The downloadAttachment
method downloads attachments to a temporary location:
private CompletableFuture<ResultPair<String>> downloadAttachment(final Attachment attachment) {
Any shared variable accessed from within lambda methods must be final, or effectively final, which simply means their value does not change once it has been set.
So, the result of your file download is captured in a ResultPair
, which is wrapped in a final AtomicReference
. This allows you to return the results of your file download from within the lambda methods created next:
final AtomicReference<ResultPair<String>> result = new AtomicReference<>();
The app creates a temporary file, then uses the Apache Commons library to download the attachment to it:
return CompletableFuture.runAsync(() -> {
try {
final FileDownloadInfo fileDownload = Serialization
.getAs(attachment.getContent(), FileDownloadInfo.class);
final File filePath = Files.createTempFile(
FilenameUtils.getBaseName(attachment.getName()),
"." + FilenameUtils.getExtension(attachment.getName())).toFile();
FileUtils.copyURLToFile(
new URL(fileDownload.getDownloadUrl()),
filePath,
30000,
30000);
If everything went well, you return true
and the path to the temporary file by setting the value wrapped by the AtomicReference
:
result.set(new ResultPair<>(true, filePath.getAbsolutePath()));
} catch (Throwable t) {
In the event of an error, you return false
and the error message:
result.set(new ResultPair<>(false, t.getLocalizedMessage()));
}
})
The wrapped ResultPair
is then returned by the CompletableFuture
:
.thenApply(aVoid -> result.get());
}
To demonstrate that the bot has successfully downloaded the attachment, the getFileSize
method returns the file’s size:
private long getFileSize(final String path) {
try {
return Files.size(Paths.get(path));
} catch (IOException e) {
return -1;
}
}
You also inspect the file’s type with the getFileType
method:
private String getFileType(final String path) {
try {
final String type = Files.probeContentType(Paths.get(path));
return type == null ? "unknown" : type;
} catch (IOException e) {
return "unknown";
}
}
}
Test the Bot
Refer to the instructions in the first article in this series, in the “Deploy the Bot” and “Link to Teams” sections, to deploy the catering bot and integrate it with Teams.
Then, post a message with an attachment. The bot will detect the attachment, download it, and report back with the file size and type:
Conclusion
Handling files with a chatbot is straightforward, requiring only the ability to download HTTP files from regular attachments in messages.
In this article, you created a simple chatbot that downloaded any message attachments and reported back the file size to indicate that it has successfully downloaded the attachment.
This example is easy to extend with any additional logic that your teams may require, allowing users to upload files through the familiar Teams chat interface. It can be used as the basis for tools such as file conversion, and can support workflows or application deployments. Simply clone the source code from GitHub and add your own custom logic to the UploadBot
class to build a bot that suits your needs.
To learn more about building your own bot for Microsoft Teams, check out Building great bots for Microsoft Teams with Azure Bot Framework Composer.