[AB-93] changing how MessageToSend handles attached plaintext files

This commit is contained in:
Sheldan
2023-05-18 22:32:56 +02:00
parent ca530949c6
commit 3df688571f
9 changed files with 64 additions and 155 deletions

View File

@@ -7,7 +7,6 @@ import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.exception.UploadFileTooLargeException;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.ChannelService;
@@ -25,8 +24,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@@ -46,8 +43,6 @@ import java.util.concurrent.CompletableFuture;
public class ExportEmoteStats extends AbstractConditionableCommand {
public static final String DOWNLOAD_EMOTE_STATS_NO_STATS_AVAILABLE_RESPONSE_TEMPLATE_KEY = "downloadEmoteStats_no_stats_available_response";
public static final String DOWNLOAD_EMOTE_STATS_FILE_NAME_TEMPLATE_KEY = "downloadEmoteStats_file_name";
public static final String DOWNLOAD_EMOTE_STATS_FILE_CONTENT_TEMPLATE_KEY = "downloadEmoteStats_file_content";
public static final String DOWNLOAD_EMOTE_STATS_RESPONSE_TEMPLATE_KEY = "downloadEmoteStats_response";
@Autowired
private ServerManagementService serverManagementService;
@@ -93,30 +88,10 @@ public class ExportEmoteStats extends AbstractConditionableCommand {
.requester(commandContext.getAuthor())
.statsSince(toUseForModel)
.build();
String fileName = templateService.renderTemplate(DOWNLOAD_EMOTE_STATS_FILE_NAME_TEMPLATE_KEY, model);
String fileContent = templateService.renderTemplate(DOWNLOAD_EMOTE_STATS_FILE_CONTENT_TEMPLATE_KEY, model);
model.setEmoteStatsFileName(fileName);
File tempFile = fileService.createTempFile(fileName);
try {
fileService.writeContentToFile(tempFile, fileContent);
long maxFileSize = commandContext.getGuild().getMaxFileSize();
// in this case, we cannot upload the file, so we need to fail
if(tempFile.length() > maxFileSize) {
throw new UploadFileTooLargeException(tempFile.length(), maxFileSize);
}
MessageToSend messageToSend = templateService.renderEmbedTemplate(DOWNLOAD_EMOTE_STATS_RESPONSE_TEMPLATE_KEY, model, actualServer.getId());
messageToSend.getAttachedFiles().get(0).setFile(tempFile.getAbsoluteFile());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(unused -> CommandResult.fromIgnored());
} catch (IOException e) {
throw new AbstractoRunTimeException(e);
} finally {
try {
fileService.safeDelete(tempFile);
} catch (IOException e) {
log.error("Failed to delete temporary export emote statistics file {}.", tempFile.getAbsoluteFile(), e);
}
}
MessageToSend messageToSend = templateService.renderEmbedTemplate(DOWNLOAD_EMOTE_STATS_RESPONSE_TEMPLATE_KEY, model, actualServer.getId());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override

View File

@@ -2,8 +2,6 @@ package dev.sheldan.abstracto.statistic.emote.command;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.exception.UploadFileTooLargeException;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
@@ -14,7 +12,6 @@ import dev.sheldan.abstracto.statistic.config.StatisticFeatureDefinition;
import dev.sheldan.abstracto.statistic.emote.model.DownloadEmoteStatsModel;
import dev.sheldan.abstracto.statistic.emote.model.database.UsedEmote;
import dev.sheldan.abstracto.statistic.emote.service.management.UsedEmoteManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import org.junit.Assert;
import org.junit.Test;
@@ -22,11 +19,8 @@ import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -77,53 +71,11 @@ public class ExportEmoteStatsTest {
verify(channelService, times(1)).sendEmbedTemplateInTextChannelList(eq(DOWNLOAD_EMOTE_STATS_NO_STATS_AVAILABLE_RESPONSE_TEMPLATE_KEY), any(), eq(commandContext.getChannel()));
}
@Test(expected = AbstractoRunTimeException.class)
public void testFileIOException() throws IOException {
CommandContext commandContext = CommandTestUtilities.getNoParameters();
mockServerAndFileRendering(commandContext);
File file = Mockito.mock(File.class);
when(fileService.createTempFile(FILE_NAME)).thenReturn(file);
doThrow(new IOException()).when(fileService).writeContentToFile(file, FILE_CONTENT);
testUnit.executeAsync(commandContext);
}
@Test(expected = UploadFileTooLargeException.class)
public void testExportAllEmoteStatsTooBig() throws IOException {
CommandContext commandContext = CommandTestUtilities.getNoParameters();
when(commandContext.getGuild().getMaxFileSize()).thenReturn(2L);
mockServerAndFileRendering(commandContext);
File file = Mockito.mock(File.class);
when(fileService.createTempFile(FILE_NAME)).thenReturn(file);
when(file.length()).thenReturn(3L);
MessageToSend messageToSend = Mockito.mock(MessageToSend.class);
CompletableFuture<CommandResult> asyncResult = testUnit.executeAsync(commandContext);
CommandTestUtilities.checkSuccessfulCompletionAsync(asyncResult);
verify(fileService, times(1)).writeContentToFile(file, FILE_CONTENT);
verify(fileService, times(1)).safeDelete(file);
verifyModel();
}
@Test
public void testFeature() {
Assert.assertEquals(StatisticFeatureDefinition.EMOTE_TRACKING, testUnit.getFeature());
}
private void verifyModel() {
DownloadEmoteStatsModel model = modelArgumentCaptor.getValue();
Assert.assertEquals(1, model.getEmotes().size());
Assert.assertEquals(usedEmote, model.getEmotes().get(0));
}
private void mockServerAndFileRendering(CommandContext commandContext) {
when(commandContext.getGuild().getIdLong()).thenReturn(SERVER_ID);
AServer server = Mockito.mock(AServer.class);
when(serverManagementService.loadServer(SERVER_ID)).thenReturn(server);
List<UsedEmote> usedEmotes = Arrays.asList(usedEmote);
when(usedEmoteManagementService.loadEmoteUsagesForServerSince(server, Instant.EPOCH)).thenReturn(usedEmotes);
when(templateService.renderTemplate(eq(DOWNLOAD_EMOTE_STATS_FILE_NAME_TEMPLATE_KEY), modelArgumentCaptor.capture())).thenReturn(FILE_NAME);
when(templateService.renderTemplate(eq(DOWNLOAD_EMOTE_STATS_FILE_CONTENT_TEMPLATE_KEY), any())).thenReturn(FILE_CONTENT);
}
@Test
public void validateCommand() {
CommandConfigValidator.validateCommandConfiguration(testUnit.getConfiguration());

View File

@@ -11,9 +11,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.exception.CustomTemplateNotFoundException;
import dev.sheldan.abstracto.core.exception.UploadFileTooLargeException;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.models.template.commands.GetCustomTemplateModel;
import dev.sheldan.abstracto.core.service.ChannelService;
@@ -30,8 +28,6 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -67,28 +63,10 @@ public class GetCustomTemplate extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
String templateKey = (String) commandContext.getParameters().getParameters().get(0);
GetCustomTemplateModel model = getModel(templateKey, commandContext.getGuild());
File tempFile = fileService.createTempFile(templateKey + ".ftl");
try {
fileService.writeContentToFile(tempFile, model.getTemplateContent());
long maxFileSize = commandContext.getGuild().getIdLong();
// in this case, we cannot upload the file, so we need to fail
if(tempFile.length() > maxFileSize) {
throw new UploadFileTooLargeException(tempFile.length(), maxFileSize);
}
MessageToSend messageToSend = templateService.renderEmbedTemplate(GET_CUSTOM_TEMPLATE_RESPONSE_TEMPLATE_KEY, model, commandContext.getGuild().getIdLong());
messageToSend.getAttachedFiles().get(0).setFile(tempFile);
return FutureUtils.toSingleFutureGeneric(channelService.sendEmbedTemplateInMessageChannelList(GET_CUSTOM_TEMPLATE_RESPONSE_TEMPLATE_KEY, model, commandContext.getChannel()))
.thenApply(unused -> CommandResult.fromSuccess());
} catch (IOException e) {
throw new AbstractoRunTimeException(e);
} finally {
try {
fileService.safeDelete(tempFile);
} catch (IOException e) {
log.error("Failed to delete temporary get custom template file {}.", tempFile.getAbsoluteFile(), e);
}
}
MessageToSend messageToSend = templateService.renderEmbedTemplate(GET_CUSTOM_TEMPLATE_RESPONSE_TEMPLATE_KEY, model, commandContext.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenAccept(interactionHook -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromSuccess());
}
private GetCustomTemplateModel getModel(String templateKey, Guild guild) {
@@ -109,27 +87,10 @@ public class GetCustomTemplate extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
String templateKey = slashCommandParameterService.getCommandOption(TEMPLATE_KEY_PARAMETER, event, String.class);
GetCustomTemplateModel model = getModel(templateKey, event.getGuild());
File tempFile = fileService.createTempFile(templateKey + ".ftl");
try {
fileService.writeContentToFile(tempFile, model.getTemplateContent());
long maxFileSize = event.getGuild().getMaxFileSize();
// in this case, we cannot upload the file, so we need to fail
if(tempFile.length() > maxFileSize) {
throw new UploadFileTooLargeException(tempFile.length(), maxFileSize);
}
MessageToSend messageToSend = templateService.renderEmbedTemplate(GET_CUSTOM_TEMPLATE_RESPONSE_TEMPLATE_KEY, model, event.getGuild().getIdLong());
messageToSend.getAttachedFiles().get(0).setFile(tempFile);
return interactionService.replyMessageToSend(messageToSend, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
} catch (IOException e) {
throw new AbstractoRunTimeException(e);
} finally {
try {
fileService.safeDelete(tempFile);
} catch (IOException e) {
log.error("Failed to delete temporary get custom template file {}.", tempFile.getAbsoluteFile(), e);
}
}
MessageToSend messageToSend = templateService.renderEmbedTemplate(GET_CUSTOM_TEMPLATE_RESPONSE_TEMPLATE_KEY, model, event.getGuild().getIdLong());
return interactionService.replyMessageToSend(messageToSend, event)
.thenAccept(interactionHook -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override

View File

@@ -11,7 +11,6 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.exception.TemplateNotFoundException;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.models.template.commands.GetTemplateModel;
@@ -27,8 +26,6 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -63,44 +60,20 @@ public class GetTemplate extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
String templateKey = (String) commandContext.getParameters().getParameters().get(0);
GetTemplateModel model = getModel(templateKey);
File tempFile = fileService.createTempFile(templateKey + ".ftl");
try {
fileService.writeContentToFile(tempFile, model.getTemplateContent());
MessageToSend messageToSend = templateService.renderEmbedTemplate(GET_TEMPLATE_RESPONSE_TEMPLATE_KEY, model, commandContext.getGuild().getIdLong());
messageToSend.getAttachedFiles().get(0).setFile(tempFile);
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenAccept(interactionHook -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromSuccess());
} catch (IOException e) {
throw new AbstractoRunTimeException(e);
} finally {
try {
fileService.safeDelete(tempFile);
} catch (IOException e) {
log.error("Failed to delete temporary get template file {}.", tempFile.getAbsoluteFile(), e);
}
}
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
String templateKey = slashCommandParameterService.getCommandOption(TEMPLATE_KEY_PARAMETER, event, String.class);
GetTemplateModel model = getModel(templateKey);
File tempFile = fileService.createTempFile(templateKey + ".ftl");
try {
fileService.writeContentToFile(tempFile, model.getTemplateContent());
MessageToSend messageToSend = templateService.renderEmbedTemplate(GET_TEMPLATE_RESPONSE_TEMPLATE_KEY, model, event.getGuild().getIdLong());
messageToSend.getAttachedFiles().get(0).setFile(tempFile);
return interactionService.replyMessageToSend(messageToSend, event)
.thenAccept(interactionHook -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(interactionHook -> CommandResult.fromSuccess());
} catch (IOException e) {
throw new AbstractoRunTimeException(e);
} finally {
try {
fileService.safeDelete(tempFile);
} catch (IOException e) {
log.error("Failed to delete temporary get template file {}.", tempFile.getAbsoluteFile(), e);
}
}
}
private GetTemplateModel getModel(String templateKey) {

View File

@@ -10,6 +10,7 @@ import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.templating.model.AttachedFile;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FileService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
@@ -57,6 +58,9 @@ public class InteractionServiceBean implements InteractionService {
@Autowired
private TemplateService templateService;
@Autowired
private FileService fileService;
public static final CounterMetric EPHEMERAL_MESSAGES_SEND = CounterMetric
.builder()
.name(DISCORD_API_INTERACTION_METRIC)

View File

@@ -199,6 +199,16 @@ public class ChannelServiceBean implements ChannelService {
if(messageToSend.getEphemeral()) {
throw new IllegalArgumentException("Ephemeral messages are only supported in interaction context.");
}
if(textChannel instanceof GuildMessageChannel) {
GuildMessageChannel guildMessageChannel = (GuildMessageChannel) textChannel;
long maxFileSize = guildMessageChannel.getGuild().getMaxFileSize();
// in this case, we cannot upload the file, so we need to fail
messageToSend.getAttachedFiles().forEach(attachedFile -> {
if(attachedFile.getFile().length() > maxFileSize) {
throw new UploadFileTooLargeException(attachedFile.getFile().length(), maxFileSize);
}
});
}
List<CompletableFuture<Message>> futures = new ArrayList<>();
List<MessageCreateAction> allMessageActions = new ArrayList<>();
Iterator<MessageEmbed> embedIterator = messageToSend.getEmbeds().iterator();
@@ -332,6 +342,14 @@ public class ChannelServiceBean implements ChannelService {
throw new IllegalArgumentException("Message to send did not contain anything to send.");
}
}
if(messageToSend.getAttachedFiles() != null && !messageToSend.getAttachedFiles().isEmpty()) {
List<FileUpload> files = messageToSend
.getAttachedFiles()
.stream()
.map(AttachedFile::convertToFileUpload)
.collect(Collectors.toList());
messageAction = messageAction.setFiles(files);
}
messageAction = messageAction.setComponents(messageToSend.getActionRows());
metricService.incrementCounter(MESSAGE_EDIT_METRIC);
return messageAction.submit();

View File

@@ -10,4 +10,6 @@ import lombok.Setter;
public class FileConfig {
private String fileName;
private Boolean spoiler;
// only used for plaintext files
private String fileContent;
}

View File

@@ -10,6 +10,7 @@ import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.templating.Templatable;
import dev.sheldan.abstracto.core.templating.exception.TemplatingException;
import dev.sheldan.abstracto.core.templating.model.*;
import dev.sheldan.abstracto.core.utils.FileService;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
@@ -28,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.time.Duration;
@@ -56,6 +58,9 @@ public class TemplateServiceBean implements TemplateService {
@Autowired
private ConfigService configService;
@Autowired
private FileService fileService;
/**
* Formats the passed passed count with the embed used for formatting pages.
*
@@ -172,11 +177,22 @@ public class TemplateServiceBean implements TemplateService {
List<AttachedFile> files = new ArrayList<>();
if(messageConfiguration.getFiles() != null && !messageConfiguration.getFiles().isEmpty()) {
messageConfiguration.getFiles().forEach(fileToAttach -> {
String fileName = fileToAttach.getFileName() != null ? fileToAttach.getFileName() : RandomStringUtils.randomAlphabetic(5);
AttachedFile attachedFile = AttachedFile
.builder()
.fileName(fileToAttach.getFileName() != null ? fileToAttach.getFileName() : RandomStringUtils.randomAlphabetic(5))
.spoiler(fileToAttach.getSpoiler())
.fileName(fileName)
.spoiler(fileToAttach.getSpoiler() != null && fileToAttach.getSpoiler())
.build();
if(fileToAttach.getFileContent() != null) {
File tempFile = fileService.createTempFile(fileName);
try {
fileService.writeContentToFile(tempFile, fileToAttach.getFileContent());
} catch (IOException e) {
log.error("Failed to write local temporary file.", e);
throw new AbstractoRunTimeException(e);
}
attachedFile.setFile(tempFile);
}
files.add(attachedFile);
});
}

View File

@@ -36,4 +36,12 @@ public class FileService {
public void safeDelete(File file) throws IOException {
java.nio.file.Files.delete(file.toPath());
}
public void safeDeleteIgnoreException(File file) {
try {
Files.delete(file.toPath());
} catch (IOException e) {
log.warn("Failed to delete file - ignoring.", e);
}
}
}