diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsWorkflowConfig.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsWorkflowConfig.java index 5c0d3a53d..446a49dea 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsWorkflowConfig.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/TeamsWorkflowConfig.java @@ -3,8 +3,6 @@ import java.io.IOException; import java.util.List; -import javax.annotation.PostConstruct; - import org.finos.springbot.ChatWorkflowConfig; import org.finos.springbot.teams.bot.BotController; import org.finos.springbot.teams.content.TeamsContentConfig; @@ -15,12 +13,14 @@ import org.finos.springbot.teams.form.TeamsFormConverter; import org.finos.springbot.teams.form.TeamsFormDeserializerModule; import org.finos.springbot.teams.handlers.ActivityHandler; +import org.finos.springbot.teams.handlers.AttachmentHandler; import org.finos.springbot.teams.handlers.SimpleActivityHandler; +import org.finos.springbot.teams.handlers.SimpleAttachmentHandler; import org.finos.springbot.teams.handlers.TeamsResponseHandler; import org.finos.springbot.teams.history.StateStorageBasedTeamsHistory; import org.finos.springbot.teams.history.StorageIDResponseHandler; import org.finos.springbot.teams.history.TeamsHistory; -import org.finos.springbot.teams.messages.MessageActivityHandler; +import org.finos.springbot.teams.messages.FileActivityHandler; import org.finos.springbot.teams.response.templating.EntityMarkupTemplateProvider; import org.finos.springbot.teams.state.AzureBlobStateStorage; import org.finos.springbot.teams.state.FileStateStorage; @@ -60,6 +60,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.teams.TeamsActivityHandler; import com.microsoft.bot.integration.BotFrameworkHttpAdapter; import com.microsoft.bot.schema.ChannelAccount; @@ -122,16 +123,25 @@ public ThymeleafTemplateProvider thymeleafWorkTemplater( return new ThymeleafTemplateProvider(prefix, suffix, defaultName, resourceLoader, formConverter); } + + + @Bean + @ConditionalOnMissingBean + public AttachmentHandler attachmentHandler() { + return new SimpleAttachmentHandler(); + } + @Bean @ConditionalOnMissingBean public TeamsResponseHandler teamsResponseHandler( + AttachmentHandler attachmentHandler, EntityMarkupTemplateProvider markupTemplater, AdaptiveCardTemplateProvider formTemplater, ThymeleafTemplateProvider displayTemplater, TeamsStateStorage th, ActivityHandler ah) { return new TeamsResponseHandler( - null, // attachment handler + attachmentHandler, // attachment handler markupTemplater, formTemplater, displayTemplater, @@ -200,21 +210,33 @@ public TeamsFormConverter teamsFormConverter(AllConversations tc) { return new TeamsFormConverter(om); } +// @Bean +// @ConditionalOnMissingBean +// public MessageActivityHandler teamsMessageActivityHandler( +// List messageConsumers, +// TeamsHTMLParser parser, +// FormValidationProcessor fvp, +// TeamsConversations tc, +// TeamsStateStorage teamsStateStorage, +// TeamsFormConverter fc) { +// return new MessageActivityHandler(messageConsumers, tc, teamsStateStorage, parser, fc, fvp); +// } + @Bean @ConditionalOnMissingBean - public MessageActivityHandler teamsMessageActivityHandler( + public TeamsActivityHandler teamsActivityHandler( List messageConsumers, TeamsHTMLParser parser, FormValidationProcessor fvp, TeamsConversations tc, TeamsStateStorage teamsStateStorage, TeamsFormConverter fc) { - return new MessageActivityHandler(messageConsumers, tc, teamsStateStorage, parser, fc, fvp); + return new FileActivityHandler(messageConsumers, tc, teamsStateStorage, parser, fc, fvp); } @Bean @ConditionalOnMissingBean - public BotController teamsBotController(MessageActivityHandler mah, BotFrameworkHttpAdapter bfa) { + public BotController teamsBotController(TeamsActivityHandler mah, BotFrameworkHttpAdapter bfa) { return new BotController(bfa, mah); } diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/AttachmentHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/AttachmentHandler.java index 425710184..49285f9ed 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/AttachmentHandler.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/AttachmentHandler.java @@ -2,7 +2,9 @@ import org.finos.springbot.workflow.response.AttachmentResponse; +import com.microsoft.bot.schema.Attachment; + public interface AttachmentHandler { - public Object formatAttachment(AttachmentResponse ar); + public Attachment formatAttachment(AttachmentResponse ar); } diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/SimpleAttachmentHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/SimpleAttachmentHandler.java new file mode 100644 index 000000000..82f16b8b5 --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/SimpleAttachmentHandler.java @@ -0,0 +1,44 @@ +package org.finos.springbot.teams.handlers; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +import org.finos.springbot.workflow.response.AttachmentResponse; + +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.teams.FileConsentCard; + +public class SimpleAttachmentHandler implements AttachmentHandler { + + public Attachment formatAttachment(AttachmentResponse ar) { + + try { + Attachment a = new Attachment(); + String fileName = ar.getName().replaceAll("\\s+", "_") + "." + ar.getExtension(); + File f = null; + f = File.createTempFile("temp", fileName); + Files.write(f.toPath(), ar.getAttachment()); + + Map consentContext = new HashMap<>(); + consentContext.put("filename", fileName); + consentContext.put("filepath", f.getAbsolutePath()); + + FileConsentCard fileCard = new FileConsentCard(); + fileCard.setDescription("This is the file I want to send you"); + fileCard.setSizeInBytes(ar.getAttachment().length); + fileCard.setAcceptContext(consentContext); + fileCard.setDeclineContext(consentContext); + + a.setContent(fileCard); + a.setName(fileName); + a.setContentType(FileConsentCard.CONTENT_TYPE); + return a; + } catch (IOException e) { + throw new RuntimeException(e); + } + + } +} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/TeamsResponseHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/TeamsResponseHandler.java index 01f7d04ba..79f54c071 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/TeamsResponseHandler.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/handlers/TeamsResponseHandler.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.function.BiFunction; @@ -93,7 +94,7 @@ public ResourceResponse apply(Response t) { try { if (t instanceof MessageResponse) { MessageResponse mr = (MessageResponse)t; - Object attachment = null; + Attachment attachment = null; MarkupAndEntities mae = messageTemplater.template(mr); String content = mae.getContents(); List entities = mae.getEntities(); @@ -149,10 +150,14 @@ protected TemplateType getTemplateType(WorkResponse wr) { return tt; } - protected CompletableFuture sendXMLResponse(String xml, Object attachment, TeamsAddressable address, List entities, Map data) throws Exception { + protected CompletableFuture sendXMLResponse(String xml, Attachment attachment, TeamsAddressable address, List entities, Map data) throws Exception { Activity out = Activity.createMessageActivity(); - out.setEntities(entities); - out.setTextFormat(TextFormatTypes.XML); + if(Objects.nonNull(attachment)) { + out.getAttachments().add(attachment); + }else { + out.setEntities(entities); + out.setTextFormat(TextFormatTypes.XML); + } out.setText(xml); return ah.handleActivity(out, address); } diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/history/StateStorageBasedTeamsHistory.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/history/StateStorageBasedTeamsHistory.java index c16038bef..fc76b8527 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/history/StateStorageBasedTeamsHistory.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/history/StateStorageBasedTeamsHistory.java @@ -26,7 +26,7 @@ */ public class StateStorageBasedTeamsHistory implements TeamsHistory { - private static final Logger LOG = LoggerFactory.getLogger(MessageActivityHandler.class); + private static final Logger LOG = LoggerFactory.getLogger(StateStorageBasedTeamsHistory.class); public final TeamsStateStorage tss; diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/messages/FileActivityHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/messages/FileActivityHandler.java new file mode 100644 index 000000000..19c660384 --- /dev/null +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/messages/FileActivityHandler.java @@ -0,0 +1,148 @@ +package org.finos.springbot.teams.messages; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.finos.springbot.teams.content.serialization.TeamsHTMLParser; +import org.finos.springbot.teams.conversations.TeamsConversations; +import org.finos.springbot.teams.state.TeamsStateStorage; +import org.finos.springbot.workflow.actions.consumers.ActionConsumer; +import org.finos.springbot.workflow.form.FormConverter; +import org.finos.springbot.workflow.form.FormValidationProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.ResultPair; +import com.microsoft.bot.schema.TextFormatTypes; +import com.microsoft.bot.schema.teams.FileConsentCardResponse; +import com.microsoft.bot.schema.teams.FileInfoCard; + +public class FileActivityHandler extends MessageActivityHandler { + + private static final Logger LOG = LoggerFactory.getLogger(FileActivityHandler.class); + + + public FileActivityHandler(List messageConsumers, TeamsConversations teamsConversations, + TeamsStateStorage teamsStateStorage, TeamsHTMLParser parser, FormConverter formConverter, + FormValidationProcessor validationProcessor) { + super(messageConsumers, teamsConversations, teamsStateStorage, parser, formConverter, validationProcessor); + } + + @Override + protected CompletableFuture onTeamsFileConsentAccept(TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse) { + + return upload(fileConsentCardResponse) + .thenCompose(result -> !result.result() ? fileUploadFailed(turnContext, result.value()) + : fileUploadCompleted(turnContext, fileConsentCardResponse)); + } + + @Override + protected CompletableFuture onTeamsFileConsentDecline(TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse) { + Map context = (Map) fileConsentCardResponse.getContext(); + + Activity reply = MessageFactory + .text(String.format("Declined. We won't upload file %s.", context.get("filename"))); + reply.setTextFormat(TextFormatTypes.XML); + + return turnContext.sendActivityBlind(reply); + } + + private CompletableFuture> upload(FileConsentCardResponse fileConsentCardResponse) { + AtomicReference> result = new AtomicReference<>(); + + return CompletableFuture.runAsync(() -> { + Map context = (Map) fileConsentCardResponse.getContext(); + LOG.info("filepath - {}", context.get("filepath")); + LOG.info("File upload endpoint : {}", fileConsentCardResponse.getUploadInfo().getUploadUrl()); + File filePath = new File(context.get("filepath")); + + HttpURLConnection connection = null; + try { + URL url = new URL(fileConsentCardResponse.getUploadInfo().getUploadUrl()); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("PUT"); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Length", Long.toString(filePath.length())); + connection.setRequestProperty("Content-Range", + String.format("bytes 0-%d/%d", filePath.length() - 1, filePath.length())); + + try (FileInputStream fileStream = new FileInputStream(filePath); + OutputStream uploadStream = connection.getOutputStream()) { + byte[] buffer = new byte[4096]; + int bytes_read; + while ((bytes_read = fileStream.read(buffer)) != -1) { + uploadStream.write(buffer, 0, bytes_read); + } + + uploadStream.flush(); + } + + try { + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String inputLine; + while ((inputLine = in.readLine()) != null) + LOG.info(inputLine); + in.close(); + } catch (Exception e) { + LOG.error("Exception occured while reading steam.. ignore this error " + e); + } + result.set(new ResultPair(true, null)); + } catch (Throwable t) { + result.set(new ResultPair(false, t.getLocalizedMessage())); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + }).thenApply(aVoid -> result.get()); + } + + private CompletableFuture fileUploadFailed(TurnContext turnContext, String error) { + Activity reply = MessageFactory.text("File upload failed. Error:
" + error + "
"); + reply.setTextFormat(TextFormatTypes.XML); + return turnContext.sendActivityBlind(reply); + } + + private CompletableFuture fileDownloadCompleted(TurnContext turnContext, Attachment attachment) { + Activity reply = MessageFactory.text(String.format("%s received and saved.", attachment.getName())); + reply.setTextFormat(TextFormatTypes.XML); + + return turnContext.sendActivityBlind(reply); + } + + private CompletableFuture fileUploadCompleted(TurnContext turnContext, + FileConsentCardResponse fileConsentCardResponse) { + FileInfoCard downloadCard = new FileInfoCard(); + downloadCard.setUniqueId(fileConsentCardResponse.getUploadInfo().getUniqueId()); + downloadCard.setFileType(fileConsentCardResponse.getUploadInfo().getFileType()); + + Attachment asAttachment = new Attachment(); + asAttachment.setContent(downloadCard); + asAttachment.setContentType(FileInfoCard.CONTENT_TYPE); + asAttachment.setName(fileConsentCardResponse.getUploadInfo().getName()); + asAttachment.setContentUrl(fileConsentCardResponse.getUploadInfo().getContentUrl()); + + Activity reply = MessageFactory + .text(String.format("File uploaded. Your file %s is ready to download", + fileConsentCardResponse.getUploadInfo().getName())); + reply.setTextFormat(TextFormatTypes.XML); + reply.setAttachment(asAttachment); + + return turnContext.sendActivityBlind(reply); + } +} diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/messages/MessageActivityHandler.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/messages/MessageActivityHandler.java index 21f8a6fc9..45eb10bd5 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/messages/MessageActivityHandler.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/messages/MessageActivityHandler.java @@ -26,12 +26,12 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; -import com.microsoft.bot.builder.ActivityHandler; import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.teams.TeamsActivityHandler; import com.microsoft.bot.schema.Activity; import com.microsoft.bot.schema.Attachment; -public class MessageActivityHandler extends ActivityHandler { +public class MessageActivityHandler extends TeamsActivityHandler { private static final Logger LOG = LoggerFactory.getLogger(MessageActivityHandler.class); diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/state/AzureBlobStateStorage.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/state/AzureBlobStateStorage.java index bd0eb2203..c11db6ce5 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/state/AzureBlobStateStorage.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/main/java/org/finos/springbot/teams/state/AzureBlobStateStorage.java @@ -41,7 +41,7 @@ */ public class AzureBlobStateStorage extends AbstractStateStorage { - private static final Logger LOG = LoggerFactory.getLogger(MessageActivityHandler.class); + private static final Logger LOG = LoggerFactory.getLogger(AzureBlobStateStorage.class); private final BlobContainerClient bcc; private final BlobServiceClient bsc; diff --git a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/controller/TeamsHandlerMappingTest.java b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/controller/TeamsHandlerMappingTest.java index ab1fd35a0..064f9f493 100644 --- a/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/controller/TeamsHandlerMappingTest.java +++ b/libs/teams/teams-chat-workflow-spring-boot-starter/src/test/java/org/finos/springbot/teams/controller/TeamsHandlerMappingTest.java @@ -14,7 +14,7 @@ import org.finos.springbot.teams.content.TeamsMultiwayChat; import org.finos.springbot.teams.content.TeamsUser; import org.finos.springbot.teams.conversations.TeamsConversations; -import org.finos.springbot.teams.messages.MessageActivityHandler; +import org.finos.springbot.teams.messages.FileActivityHandler; import org.finos.springbot.teams.state.TeamsStateStorage; import org.finos.springbot.teams.turns.CurrentTurnContext; import org.finos.springbot.tests.controller.AbstractHandlerMappingTest; @@ -72,7 +72,7 @@ public class TeamsHandlerMappingTest extends AbstractHandlerMappingTest { TurnContext tc; @Autowired - MessageActivityHandler mah; + FileActivityHandler mah; @Autowired ChatRequestChatHandlerMapping hm;