[AB-298] fixing various issues related to modmail:

fixing editing a message without messages but only embeds not possible in channel service
refactoring closing parameters to use an object instead of parameters always
adding a progress indicator to closing a modmail thread
adding a notification to contact in order to show where the thread was created
fixing configuration for category (this caused the setup to fail, because there was no default value) and threadMessage feature modes not being correct
refactored model for closing header and added additional information
refactored modmail message logging to use the message history instead of individually loading the messages
adding nicer exception in case the mod mail message update failed
adding creation of AUserInAServer in case the user did not interact on the server yet
changed ID of modmail thread to be identical to the channel it was created in, this is so we can load the channel easier
This commit is contained in:
Sheldan
2021-07-04 12:14:04 +02:00
parent 61eefd53c3
commit 18929c9a01
28 changed files with 483 additions and 363 deletions

View File

@@ -44,11 +44,6 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency> <dependency>
<groupId>dev.sheldan.abstracto.core</groupId> <groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>metrics-int</artifactId> <artifactId>metrics-int</artifactId>

View File

@@ -1,6 +1,5 @@
package dev.sheldan.abstracto.linkembed.listener; package dev.sheldan.abstracto.linkembed.listener;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.listener.ButtonClickedListenerResult; import dev.sheldan.abstracto.core.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.ButtonClickedListener; import dev.sheldan.abstracto.core.listener.async.jda.ButtonClickedListener;
@@ -26,9 +25,6 @@ import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
public class MessageEmbedDeleteButtonClickedListener implements ButtonClickedListener { public class MessageEmbedDeleteButtonClickedListener implements ButtonClickedListener {
@Autowired
private Gson gson;
@Autowired @Autowired
private MessageService messageService; private MessageService messageService;

View File

@@ -12,10 +12,12 @@ import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService; import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition; import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition; import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition;
import dev.sheldan.abstracto.modmail.model.ClosingContext;
import dev.sheldan.abstracto.modmail.model.database.ModMailThread; import dev.sheldan.abstracto.modmail.model.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService; import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService; import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService;
import dev.sheldan.abstracto.core.templating.service.TemplateService; import dev.sheldan.abstracto.core.templating.service.TemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -29,8 +31,10 @@ import java.util.concurrent.CompletableFuture;
* This command takes an optional parameter, the note, which will be replaced with a default value, if not present * This command takes an optional parameter, the note, which will be replaced with a default value, if not present
*/ */
@Component @Component
@Slf4j
public class Close extends AbstractConditionableCommand { public class Close extends AbstractConditionableCommand {
public static final String MODMAIL_CLOSE_DEFAULT_NOTE_TEMPLATE_KEY = "modmail_close_default_note";
@Autowired @Autowired
private ModMailContextCondition requiresModMailCondition; private ModMailContextCondition requiresModMailCondition;
@@ -50,10 +54,17 @@ public class Close extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) { public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters(); List<Object> parameters = commandContext.getParameters().getParameters();
// the default value of the note is configurable via template // the default value of the note is configurable via template
String note = parameters.size() == 1 ? (String) parameters.get(0) : templateService.renderTemplate("modmail_close_default_note", new Object()); String note = parameters.size() == 1 ? (String) parameters.get(0) : templateService.renderTemplate(MODMAIL_CLOSE_DEFAULT_NOTE_TEMPLATE_KEY, new Object());
AChannel channel = channelManagementService.loadChannel(commandContext.getChannel()); AChannel channel = channelManagementService.loadChannel(commandContext.getChannel());
ModMailThread thread = modMailThreadManagementService.getByChannel(channel); ModMailThread thread = modMailThreadManagementService.getByChannel(channel);
return modMailThreadService.closeModMailThread(thread, note, true, commandContext.getUndoActions(), true) ClosingContext context = ClosingContext
.builder()
.closingMember(commandContext.getAuthor())
.notifyUser(true)
.log(true)
.note(note)
.build();
return modMailThreadService.closeModMailThread(thread, context, commandContext.getUndoActions())
.thenApply(aVoid -> CommandResult.fromIgnored()); .thenApply(aVoid -> CommandResult.fromIgnored());
} }

View File

@@ -1,93 +0,0 @@
package dev.sheldan.abstracto.modmail.command;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.condition.CommandCondition;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.HelpInfo;
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.config.FeatureMode;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatureConfig;
import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition;
import dev.sheldan.abstracto.modmail.config.ModMailMode;
import dev.sheldan.abstracto.modmail.model.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* This command closes a mod mail thread without logging the closing and the contents of the {@link ModMailThread}.
* This command is only available if the server has the {@link ModMailFeatureConfig}
* 'LOGGING' mode enabled, because else the normal close command behaves the same way.
*/
@Component
public class CloseNoLog extends AbstractConditionableCommand {
@Autowired
private ModMailContextCondition requiresModMailCondition;
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;
@Autowired
private ModMailThreadService modMailThreadService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelManagementService channelManagementService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
AChannel channel = channelManagementService.loadChannel(commandContext.getChannel());
ModMailThread thread = modMailThreadManagementService.getByChannel(channel);
// we don't have a note, therefore we cant pass any, the method handles this accordingly
return modMailThreadService.closeModMailThread(thread, null, false, commandContext.getUndoActions(), false)
.thenApply(aVoid -> CommandResult.fromIgnored());
}
@Override
public CommandConfiguration getConfiguration() {
HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
return CommandConfiguration.builder()
.name("closeNoLog")
.module(ModMailModuleDefinition.MODMAIL)
.async(true)
.help(helpInfo)
.supportsEmbedException(true)
.templated(true)
.causesReaction(true)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ModMailFeatureDefinition.MOD_MAIL;
}
@Override
public List<CommandCondition> getConditions() {
List<CommandCondition> conditions = super.getConditions();
conditions.add(requiresModMailCondition);
return conditions;
}
/**
* This command is only available if the LOGGING feature mode is enabled
*/
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(ModMailMode.LOGGING);
}
}

View File

@@ -12,6 +12,7 @@ import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService; import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition; import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition; import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition;
import dev.sheldan.abstracto.modmail.model.ClosingContext;
import dev.sheldan.abstracto.modmail.model.database.ModMailThread; import dev.sheldan.abstracto.modmail.model.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService; import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService; import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService;
@@ -52,7 +53,14 @@ public class CloseSilently extends AbstractConditionableCommand {
String note = parameters.size() == 1 ? (String) parameters.get(0) : templateService.renderTemplate("modmail_close_default_note", new Object()); String note = parameters.size() == 1 ? (String) parameters.get(0) : templateService.renderTemplate("modmail_close_default_note", new Object());
AChannel channel = channelManagementService.loadChannel(commandContext.getChannel()); AChannel channel = channelManagementService.loadChannel(commandContext.getChannel());
ModMailThread thread = modMailThreadManagementService.getByChannel(channel); ModMailThread thread = modMailThreadManagementService.getByChannel(channel);
return modMailThreadService.closeModMailThread(thread, note, false, commandContext.getUndoActions(), true) ClosingContext context = ClosingContext
.builder()
.closingMember(commandContext.getAuthor())
.notifyUser(false)
.log(true)
.note(note)
.build();
return modMailThreadService.closeModMailThread(thread, context, commandContext.getUndoActions())
.thenApply(aVoid -> CommandResult.fromIgnored()); .thenApply(aVoid -> CommandResult.fromIgnored());
} }

View File

@@ -75,7 +75,8 @@ public class ModMailMessageEditedListener implements AsyncMessageUpdatedListener
messageOptional.ifPresent(modMailMessage -> { messageOptional.ifPresent(modMailMessage -> {
log.info("Editing send message {} in channel {} in mod mail thread {} in server {}.", messageBefore.getMessageId(), messageBefore.getChannelId(), modMailMessage.getThreadReference().getId(), messageBefore.getServerId()); log.info("Editing send message {} in channel {} in mod mail thread {} in server {}.", messageBefore.getMessageId(), messageBefore.getChannelId(), modMailMessage.getThreadReference().getId(), messageBefore.getServerId());
String contentStripped = message.getContentStripped(); String contentStripped = message.getContentStripped();
String commandName = commandRegistry.getCommandName(contentStripped.substring(0, contentStripped.indexOf(" ")), messageBefore.getServerId()); int spaceIndex = contentStripped.contains(" ") ? contentStripped.indexOf(" ") : contentStripped.length() - 1;
String commandName = commandRegistry.getCommandName(contentStripped.substring(0, spaceIndex), messageBefore.getServerId());
if(!commandService.doesCommandExist(commandName)) { if(!commandService.doesCommandExist(commandName)) {
commandName = DEFAULT_COMMAND_FOR_MODMAIL_EDIT; commandName = DEFAULT_COMMAND_FOR_MODMAIL_EDIT;
log.info("Edit did not contain the original command to retrieve the parameters for. Resulting to {}.", DEFAULT_COMMAND_FOR_MODMAIL_EDIT); log.info("Edit did not contain the original command to retrieve the parameters for. Resulting to {}.", DEFAULT_COMMAND_FOR_MODMAIL_EDIT);
@@ -85,7 +86,10 @@ public class ModMailMessageEditedListener implements AsyncMessageUpdatedListener
CompletableFuture<Member> loadEditingUser = memberService.getMemberInServerAsync(messageBefore.getServerId(), modMailMessage.getAuthor().getUserReference().getId()); CompletableFuture<Member> loadEditingUser = memberService.getMemberInServerAsync(messageBefore.getServerId(), modMailMessage.getAuthor().getUserReference().getId());
CompletableFuture.allOf(parameterParseFuture, loadTargetUser, loadEditingUser).thenAccept(unused -> CompletableFuture.allOf(parameterParseFuture, loadTargetUser, loadEditingUser).thenAccept(unused ->
self.updateMessageInThread(message, parameterParseFuture.join(), loadTargetUser.join(), loadEditingUser.join()) self.updateMessageInThread(message, parameterParseFuture.join(), loadTargetUser.join(), loadEditingUser.join())
); ).exceptionally(throwable -> {
log.error("Failed to update reply for mod mail thread in channel {}.", model.getAfter().getChannel().getIdLong(), throwable);
return null;
});
}); });
return DefaultListenerResult.PROCESSED; return DefaultListenerResult.PROCESSED;
} }

View File

@@ -1,25 +1,24 @@
package dev.sheldan.abstracto.modmail.service; package dev.sheldan.abstracto.modmail.service;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.models.ServerChannelMessageUser; import dev.sheldan.abstracto.core.models.ServerChannelMessageUser;
import dev.sheldan.abstracto.core.service.BotService; import dev.sheldan.abstracto.core.service.BotService;
import dev.sheldan.abstracto.core.service.ChannelService; import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.MemberService; import dev.sheldan.abstracto.core.service.MemberService;
import dev.sheldan.abstracto.core.service.UserService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.modmail.model.database.ModMailMessage; import dev.sheldan.abstracto.modmail.model.database.ModMailMessage;
import dev.sheldan.abstracto.modmail.model.database.ModMailThread; import dev.sheldan.abstracto.modmail.model.database.ModMailThread;
import dev.sheldan.abstracto.modmail.model.dto.LoadedModmailThreadMessage; import dev.sheldan.abstracto.modmail.model.template.ModmailLoggingThreadMessages;
import dev.sheldan.abstracto.modmail.model.dto.LoadedModmailThreadMessageList;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList; import java.util.*;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component @Component
@Slf4j @Slf4j
@@ -34,11 +33,16 @@ public class ModMailMessageServiceBean implements ModMailMessageService {
@Autowired @Autowired
private ChannelService channelService; private ChannelService channelService;
@Autowired
private UserService userService;
private static final Integer HISTORY_RETRIEVAL_LIMIT = 100;
@Override @Override
public LoadedModmailThreadMessageList loadModMailMessages(List<ModMailMessage> modMailMessages) { public CompletableFuture<ModmailLoggingThreadMessages> loadModMailMessages(List<ModMailMessage> modMailMessages) {
if(modMailMessages.isEmpty()) { if(modMailMessages.isEmpty()) {
return LoadedModmailThreadMessageList.builder().build(); return CompletableFuture.completedFuture(ModmailLoggingThreadMessages.builder().build());
} }
CompletableFuture<ModmailLoggingThreadMessages> future = new CompletableFuture<>();
// all message must be from the same thread // all message must be from the same thread
ModMailThread thread = modMailMessages.get(0).getThreadReference(); ModMailThread thread = modMailMessages.get(0).getThreadReference();
log.debug("Loading {} mod mail messages from thread {} in server {}.", modMailMessages.size(), thread.getId(), thread.getServer().getId()); log.debug("Loading {} mod mail messages from thread {} in server {}.", modMailMessages.size(), thread.getId(), thread.getServer().getId());
@@ -49,6 +53,11 @@ public class ModMailMessageServiceBean implements ModMailMessageService {
.userId(modMailMessage.getAuthor().getUserReference().getId()) .userId(modMailMessage.getAuthor().getUserReference().getId())
.serverId(thread.getServer().getId()); .serverId(thread.getServer().getId());
// if its not from a private chat, we need to set channel ID in order to fetch the data // if its not from a private chat, we need to set channel ID in order to fetch the data
// this is necessary, because we only log to the current modmail channel in a certain feature mode
// but the DMs _always_ receive the messages from modmail thread.
// the channelID is null, if it was a message from a modmail thread
// which means, in order to retrieve the messages which were mod -> member
// we need to select the elements in which channel is null
if(Boolean.FALSE.equals(modMailMessage.getDmChannel())) { if(Boolean.FALSE.equals(modMailMessage.getDmChannel())) {
log.debug("Message {} was from DM.", modMailMessage.getMessageId()); log.debug("Message {} was from DM.", modMailMessage.getMessageId());
serverChannelMessageBuilder serverChannelMessageBuilder
@@ -59,53 +68,129 @@ public class ModMailMessageServiceBean implements ModMailMessageService {
} }
messageIds.add(serverChannelMessageBuilder.build()); messageIds.add(serverChannelMessageBuilder.build());
}); });
List<LoadedModmailThreadMessage> messageFutures = new ArrayList<>(); List<Long> messageIdsToLoad = messageIds
// add the place holder futures, which are then resolved one by one .stream()
// because we cannot directly fetch the messages, in case they are in a private channel .map(ServerChannelMessageUser::getMessageId)
// the opening of a private channel is a rest operation it itself, so we need .collect(Collectors.toList());
// to create the promises here already, else the list is empty for example
modMailMessages.forEach(modMailMessage -> messageFutures.add(getLoadedModmailThreadMessage()));
Optional<TextChannel> textChannelFromServer = channelService.getTextChannelFromServerOptional(thread.getServer().getId(), thread.getChannel().getId()); Optional<TextChannel> textChannelFromServer = channelService.getTextChannelFromServerOptional(thread.getServer().getId(), thread.getChannel().getId());
if(textChannelFromServer.isPresent()) { if(textChannelFromServer.isPresent()) {
TextChannel modMailThread = textChannelFromServer.get(); TextChannel modMailThread = textChannelFromServer.get();
Long userId = thread.getUser().getUserReference().getId(); Long userId = thread.getUser().getUserReference().getId();
botService.getInstance().openPrivateChannelById(userId).queue(privateChannel -> { botService.getInstance().openPrivateChannelById(userId).queue(privateChannel -> {
Iterator<LoadedModmailThreadMessage> iterator = messageFutures.iterator(); Optional<ServerChannelMessageUser> latestThreadMessageOptional = messageIds
messageIds.forEach(serverChannelMessage -> { .stream()
log.debug("Loading message {}.", serverChannelMessage.getMessageId()); .filter(serverChannelMessageUser -> serverChannelMessageUser.getChannelId() != null)
CompletableFuture<Message> messageFuture; .max(Comparator.comparing(ServerChannelMessageUser::getMessageId));
CompletableFuture<Member> memberFuture = memberService.getMemberInServerAsync(serverChannelMessage.getServerId(), serverChannelMessage.getUserId()); Optional<ServerChannelMessageUser> latestPrivateMessageOptional = messageIds
if(serverChannelMessage.getChannelId() == null){ .stream()
messageFuture = channelService.retrieveMessageInChannel(privateChannel, serverChannelMessage.getMessageId()); .filter(serverChannelMessageUser -> serverChannelMessageUser.getChannelId() == null)
} else { .max(Comparator.comparing(ServerChannelMessageUser::getMessageId));
messageFuture = channelService.retrieveMessageInChannel(modMailThread, serverChannelMessage.getMessageId()); CompletableFuture<MessageHistory> threadHistoryFuture;
} if(latestThreadMessageOptional.isPresent()) {
CompletableFuture.allOf(messageFuture, memberFuture).whenComplete((aVoid, throwable) -> { ServerChannelMessageUser latestPrivateMessage = latestThreadMessageOptional.get();
LoadedModmailThreadMessage next = iterator.next(); threadHistoryFuture = modMailThread.getHistoryAround(latestPrivateMessage.getMessageId(), HISTORY_RETRIEVAL_LIMIT).submit();
if(messageFuture.isCompletedExceptionally()) { } else {
log.warn("Message {} from user {} in server {} failed to load.", serverChannelMessage.getMessageId(), serverChannelMessage.getUserId(), serverChannelMessage.getServerId()); threadHistoryFuture = CompletableFuture.completedFuture(null);
messageFuture.exceptionally(throwable1 -> { }
log.warn("Failed with:", throwable1); CompletableFuture<MessageHistory> privateHistoryFuture;
return null; if(latestPrivateMessageOptional.isPresent()) {
}); ServerChannelMessageUser latestThreadMessage = latestPrivateMessageOptional.get();
next.getMessageFuture().complete(null); privateHistoryFuture = privateChannel.getHistoryAround(latestThreadMessage.getMessageId(), HISTORY_RETRIEVAL_LIMIT).submit();
} else { } else {
next.getMessageFuture().complete(messageFuture.join()); privateHistoryFuture = CompletableFuture.completedFuture(null);
} }
List<Message> loadedMessages = new ArrayList<>();
if(memberFuture.isCompletedExceptionally()) { CompletableFuture.allOf(threadHistoryFuture, privateHistoryFuture)
next.getMemberFuture().complete(null); .thenCompose(unused -> loadMoreMessages(messageIdsToLoad, privateHistoryFuture.join(), modMailThread, threadHistoryFuture.join(), privateChannel, loadedMessages, 0))
} else { .thenAccept(unused -> {
next.getMemberFuture().complete(memberFuture.join()); Set<Long> userIds = messageIds
} .stream()
.map(ServerChannelMessageUser::getUserId)
.collect(Collectors.toSet());
CompletableFutureList<User> userFuture = userService.retrieveUsers(new ArrayList<>(userIds));
userFuture.getMainFuture().thenAccept(unused1 -> {
ModmailLoggingThreadMessages result = ModmailLoggingThreadMessages
.builder()
.messages(loadedMessages)
.authors(userFuture.getObjects())
.build();
future.complete(result);
}); });
}); });
}); });
} else {
future.completeExceptionally(new AbstractoRunTimeException("Channel for modmail thread not found. How did we get here?"));
} }
return LoadedModmailThreadMessageList.builder().messageList(messageFutures).build(); return future;
} }
public LoadedModmailThreadMessage getLoadedModmailThreadMessage() { public CompletableFuture<Void> loadMoreMessages(List<Long> messagesToLoad,
return LoadedModmailThreadMessage.builder().memberFuture(new CompletableFuture<>()).messageFuture(new CompletableFuture<>()).build(); MessageHistory privateMessageHistory, TextChannel thread,
MessageHistory threadMessageHistory, PrivateChannel dmChannel, List<Message> loadedMessages, Integer counter) {
if(counter == messagesToLoad.size()) {
log.warn("We encountered the maximum of {} iterations when loading modmail history - aborting.", messagesToLoad.size());
return CompletableFuture.completedFuture(null);
}
Map<Long, Message> threadMessagesInStep = mapHistoryToMessageIds(threadMessageHistory);
Map<Long, Message> privateMessagesInStep = mapHistoryToMessageIds(privateMessageHistory);
List<Long> messagesLoadedThisStep = new ArrayList<>();
messagesToLoad.forEach(messageId -> {
if(threadMessagesInStep.containsKey(messageId)) {
loadedMessages.add(threadMessagesInStep.get(messageId));
messagesLoadedThisStep.add(messageId);
} else if(privateMessagesInStep.containsKey(messageId)){
loadedMessages.add(privateMessagesInStep.get(messageId));
messagesLoadedThisStep.add(messageId);
}
});
messagesToLoad.removeAll(messagesLoadedThisStep);
if(messagesToLoad.isEmpty()) {
return CompletableFuture.completedFuture(null);
} else {
final CompletableFuture<MessageHistory> threadHistoryAction;
if(doesHistoryContainValues(threadMessageHistory)) {
Optional<Message> minThreadMessage = getOldestMessage(threadMessageHistory.getRetrievedHistory());
if(minThreadMessage.isPresent()) {
threadHistoryAction = thread.getHistoryBefore(minThreadMessage.get(), HISTORY_RETRIEVAL_LIMIT).submit();
} else {
threadHistoryAction = CompletableFuture.completedFuture(null);
}
} else {
threadHistoryAction = CompletableFuture.completedFuture(null);
}
final CompletableFuture<MessageHistory> privateHistoryAction;
if(doesHistoryContainValues(privateMessageHistory)) {
Optional<Message> minDmMessage = getOldestMessage(privateMessageHistory.getRetrievedHistory());
if(minDmMessage.isPresent()) {
privateHistoryAction = dmChannel.getHistoryBefore(minDmMessage.get(), HISTORY_RETRIEVAL_LIMIT).submit();
} else {
privateHistoryAction = CompletableFuture.completedFuture(null);
}
} else {
privateHistoryAction = CompletableFuture.completedFuture(null);
}
return CompletableFuture.allOf(threadHistoryAction, privateHistoryAction)
.thenCompose(lists -> loadMoreMessages(messagesToLoad, threadHistoryAction.join(), thread, privateHistoryAction.join(), dmChannel, loadedMessages, counter + 1));
}
} }
private Map<Long, Message> mapHistoryToMessageIds(MessageHistory threadMessageHistory) {
if(!doesHistoryContainValues(threadMessageHistory)) {
return new HashMap<>();
}
return threadMessageHistory
.getRetrievedHistory()
.stream()
.collect(Collectors.toMap(ISnowflake::getIdLong, Function.identity()));
}
private boolean doesHistoryContainValues(MessageHistory threadMessageHistory) {
return threadMessageHistory != null && !threadMessageHistory.isEmpty();
}
private Optional<Message> getOldestMessage(List<Message> messages) {
return messages.stream().min(Comparator.comparing(ISnowflake::getTimeCreated));
}
} }

View File

@@ -25,8 +25,8 @@ import dev.sheldan.abstracto.modmail.config.ModMailPostTargets;
import dev.sheldan.abstracto.modmail.exception.ModMailCategoryIdException; import dev.sheldan.abstracto.modmail.exception.ModMailCategoryIdException;
import dev.sheldan.abstracto.modmail.exception.ModMailThreadChannelNotFound; import dev.sheldan.abstracto.modmail.exception.ModMailThreadChannelNotFound;
import dev.sheldan.abstracto.modmail.exception.ModMailThreadNotFoundException; import dev.sheldan.abstracto.modmail.exception.ModMailThreadNotFoundException;
import dev.sheldan.abstracto.modmail.model.ClosingContext;
import dev.sheldan.abstracto.modmail.model.database.*; import dev.sheldan.abstracto.modmail.model.database.*;
import dev.sheldan.abstracto.modmail.model.dto.LoadedModmailThreadMessageList;
import dev.sheldan.abstracto.modmail.model.dto.ServerChoice; import dev.sheldan.abstracto.modmail.model.dto.ServerChoice;
import dev.sheldan.abstracto.modmail.model.template.*; import dev.sheldan.abstracto.modmail.model.template.*;
import dev.sheldan.abstracto.modmail.service.management.ModMailMessageManagementService; import dev.sheldan.abstracto.modmail.service.management.ModMailMessageManagementService;
@@ -47,6 +47,8 @@ import javax.annotation.PostConstruct;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component @Component
@Slf4j @Slf4j
@@ -63,8 +65,9 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
/** /**
* The template key used for default mod mail exceptions * The template key used for default mod mail exceptions
*/ */
public static final String MODMAIL_EXCEPTION_GENERIC_TEMPLATE = "modmail_exception_generic"; public static final String MODMAIL_CLOSE_PROGRESS_TEMPLATE_KEY = "modmail_closing_progress";
public static final String MODMAIL_STAFF_MESSAGE_TEMPLATE_KEY = "modmail_staff_message"; public static final String MODMAIL_STAFF_MESSAGE_TEMPLATE_KEY = "modmail_staff_message";
public static final String MODMAIL_THREAD_CREATED_TEMPLATE_KEY = "modmail_thread_created";
@Autowired @Autowired
private ModMailThreadManagementService modMailThreadManagementService; private ModMailThreadManagementService modMailThreadManagementService;
@@ -132,6 +135,9 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Autowired @Autowired
private ServerManagementService serverManagementService; private ServerManagementService serverManagementService;
@Autowired
private UserService userService;
@Autowired @Autowired
private ModMailThreadServiceBean self; private ModMailThreadServiceBean self;
@@ -186,10 +192,20 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
CompletableFuture<TextChannel> textChannelFuture = channelService.createTextChannel(user.getName() + user.getDiscriminator(), server, categoryId); CompletableFuture<TextChannel> textChannelFuture = channelService.createTextChannel(user.getName() + user.getDiscriminator(), server, categoryId);
return textChannelFuture.thenCompose(channel -> { return textChannelFuture.thenCompose(channel -> {
undoActions.add(UndoActionInstance.getChannelDeleteAction(serverId, channel.getIdLong())); undoActions.add(UndoActionInstance.getChannelDeleteAction(serverId, channel.getIdLong()));
return self.performModMailThreadSetup(member, initialMessage, channel, userInitiated, undoActions); return self.performModMailThreadSetup(member, initialMessage, channel, userInitiated, undoActions, feedBackChannel);
}); });
} }
@Transactional
public CompletableFuture<Void> sendContactNotification(Member member, TextChannel textChannel, MessageChannel feedBackChannel) {
ContactNotificationModel model = ContactNotificationModel
.builder()
.createdChannel(textChannel)
.targetMember(member)
.build();
return FutureUtils.toSingleFutureGeneric(channelService.sendEmbedTemplateInMessageChannelList(MODMAIL_THREAD_CREATED_TEMPLATE_KEY, model, feedBackChannel));
}
/** /**
* This method is responsible for creating the instance in the database, sending the header in the newly created text channel and forwarding the initial message * This method is responsible for creating the instance in the database, sending the header in the newly created text channel and forwarding the initial message
* by the user (if any), after this is complete, this method executes the method to perform the mod mail notification. * by the user (if any), after this is complete, this method executes the method to perform the mod mail notification.
@@ -201,7 +217,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* @return A {@link CompletableFuture future} which completes when the setup is done * @return A {@link CompletableFuture future} which completes when the setup is done
*/ */
@Transactional @Transactional
public CompletableFuture<Void> performModMailThreadSetup(Member member, Message initialMessage, TextChannel channel, boolean userInitiated, List<UndoActionInstance> undoActions) { public CompletableFuture<Void> performModMailThreadSetup(Member member, Message initialMessage, TextChannel channel, boolean userInitiated, List<UndoActionInstance> undoActions, MessageChannel feedBackChannel) {
log.info("Performing modmail thread setup for channel {} for user {} in server {}. It was initiated by a user: {}.", channel.getIdLong(), member.getId(), channel.getGuild().getId(), userInitiated); log.info("Performing modmail thread setup for channel {} for user {} in server {}. It was initiated by a user: {}.", channel.getIdLong(), member.getId(), channel.getGuild().getId(), userInitiated);
CompletableFuture<Void> headerFuture = sendModMailHeader(channel, member); CompletableFuture<Void> headerFuture = sendModMailHeader(channel, member);
CompletableFuture<Message> userReplyMessage; CompletableFuture<Message> userReplyMessage;
@@ -221,6 +237,10 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
return CompletableFuture.allOf(headerFuture, notificationFuture, userReplyMessage).thenAccept(aVoid -> { return CompletableFuture.allOf(headerFuture, notificationFuture, userReplyMessage).thenAccept(aVoid -> {
undoActions.clear(); undoActions.clear();
self.setupModMailThreadInDB(initialMessage, channel, member, userReplyMessage.join()); self.setupModMailThreadInDB(initialMessage, channel, member, userReplyMessage.join());
}).thenAccept(unused -> {
if(!userInitiated) {
self.sendContactNotification(member, channel, feedBackChannel);
}
}); });
} }
@@ -286,9 +306,16 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Override @Override
public void createModMailPrompt(AUser user, Message initialMessage) { public void createModMailPrompt(AUser user, Message initialMessage) {
List<AUserInAServer> knownServers = userInServerManagementService.getUserInAllServers(user.getId()); List<AUserInAServer> knownServers = userInServerManagementService.getUserInAllServers(user.getId());
// do nothing if we don't know the user // if the user doesnt exist in the servery set, we need to create the user first in all of them, in order to offer it
if(knownServers.isEmpty()) {
List<Guild> mutualServers = initialMessage.getJDA().getMutualGuilds(initialMessage.getAuthor());
mutualServers.forEach(guild -> {
AServer server = serverManagementService.loadServer(guild);
knownServers.add(userInServerManagementService.loadOrCreateUser(server, user));
});
}
if(!knownServers.isEmpty()) { if(!knownServers.isEmpty()) {
log.info("There are {} shared servers between user and abstracto.", knownServers.size()); log.info("There are {} shared servers between user and the bot.", knownServers.size());
List<ServerChoice> availableGuilds = new ArrayList<>(); List<ServerChoice> availableGuilds = new ArrayList<>();
HashMap<String, Long> choices = new HashMap<>(); HashMap<String, Long> choices = new HashMap<>();
for (int i = 0; i < knownServers.size(); i++) { for (int i = 0; i < knownServers.size(); i++) {
@@ -309,7 +336,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
availableGuilds.add(serverChoice); availableGuilds.add(serverChoice);
} }
} }
log.info("There were {} shared servers found which have modmailenabled.", availableGuilds.size()); log.info("There were {} shared servers found which have modmail enabled.", availableGuilds.size());
// if more than 1 server is available, show a choice dialog // if more than 1 server is available, show a choice dialog
if(availableGuilds.size() > 1) { if(availableGuilds.size() > 1) {
ModMailServerChooserModel modMailServerChooserModel = ModMailServerChooserModel ModMailServerChooserModel modMailServerChooserModel = ModMailServerChooserModel
@@ -323,13 +350,21 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
.setDescription(text) .setDescription(text)
.setAction(reactionEmote -> { .setAction(reactionEmote -> {
Long chosenServerId = choices.get(reactionEmote.getEmoji()); Long chosenServerId = choices.get(reactionEmote.getEmoji());
log.debug("Executing action for creationg a modmail thread in server {} for user {}.", chosenServerId, initialMessage.getAuthor().getIdLong()); Long userId = initialMessage.getAuthor().getIdLong();
memberService.getMemberInServerAsync(chosenServerId, initialMessage.getAuthor().getIdLong()).thenCompose(member -> log.debug("Executing action for creationg a modmail thread in server {} for user {}.", chosenServerId, userId);
self.createModMailThreadForUser(member, initialMessage, initialMessage.getChannel(), true, new ArrayList<>()).exceptionally(throwable -> { memberService.getMemberInServerAsync(chosenServerId, userId).thenCompose(member -> {
log.error("Failed to setup thread correctly", throwable); try {
return null; return self.createModMailThreadForUser(member, initialMessage, initialMessage.getChannel(), true, new ArrayList<>());
}) } catch (Exception exception) {
); log.error("Setting up modmail thread for user {} in server {} failed.", userId, chosenServerId, exception);
CompletableFuture<Void> future = new CompletableFuture<>();
future.completeExceptionally(exception);
return future;
}
}).exceptionally(throwable -> {
log.error("Failed to load member {} for modmail in server {}.", userId, chosenServerId, throwable);
return null;
});
}) })
.build(); .build();
log.debug("Displaying server choice message for user {} in channel {}.", user.getId(), initialMessage.getChannel().getId()); log.debug("Displaying server choice message for user {} in channel {}.", user.getId(), initialMessage.getChannel().getId());
@@ -338,19 +373,25 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
// if exactly one server is available, open the thread directly // if exactly one server is available, open the thread directly
Long chosenServerId = choices.get(availableGuilds.get(0).getReactionEmote()); Long chosenServerId = choices.get(availableGuilds.get(0).getReactionEmote());
log.info("Only one server available to modmail. Directly opening modmail thread for user {} in server {}.", initialMessage.getAuthor().getId(), chosenServerId); log.info("Only one server available to modmail. Directly opening modmail thread for user {} in server {}.", initialMessage.getAuthor().getId(), chosenServerId);
memberService.getMemberInServerAsync(chosenServerId, initialMessage.getAuthor().getIdLong()).thenCompose(member -> memberService.getMemberInServerAsync(chosenServerId, initialMessage.getAuthor().getIdLong()).thenCompose(member -> {
self.createModMailThreadForUser(member, initialMessage, initialMessage.getChannel(), true, new ArrayList<>()).exceptionally(throwable -> { try {
log.error("Failed to setup thread correctly", throwable); return self.createModMailThreadForUser(member, initialMessage, initialMessage.getChannel(), true, new ArrayList<>());
return null; } catch (Exception exception) {
}) CompletableFuture<Void> future = new CompletableFuture<>();
); future.completeExceptionally(exception);
return future;
}
}).exceptionally(throwable -> {
log.error("Failed to setup thread correctly", throwable);
return null;
});
} else { } else {
log.info("No server available to open a modmail thread in."); log.info("No server available to open a modmail thread in.");
// in case there is no server available, send an error message // in case there is no server available, send an error message
channelService.sendEmbedTemplateInMessageChannelList("modmail_no_server_available", new Object(), initialMessage.getChannel()); channelService.sendEmbedTemplateInMessageChannelList("modmail_no_server_available", new Object(), initialMessage.getChannel());
} }
} else { } else {
log.warn("User which was not known in any of the servers tried to contact the bot. {}", user.getId()); log.warn("User {} which was not known in any of the servers tried to contact the bot.", user.getId());
} }
} }
@@ -383,23 +424,28 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
Long modmailThreadId = modMailThread.getId(); Long modmailThreadId = modMailThread.getId();
metricService.incrementCounter(MDOMAIL_THREAD_MESSAGE_RECEIVED); metricService.incrementCounter(MDOMAIL_THREAD_MESSAGE_RECEIVED);
log.debug("Relaying message {} to modmail thread {} for user {} to server {}.", messageFromUser.getId(), modMailThread.getId(), messageFromUser.getAuthor().getIdLong(), modMailThread.getServer().getId()); log.debug("Relaying message {} to modmail thread {} for user {} to server {}.", messageFromUser.getId(), modMailThread.getId(), messageFromUser.getAuthor().getIdLong(), modMailThread.getServer().getId());
return memberService.getMemberInServerAsync(modMailThread.getServer().getId(), messageFromUser.getAuthor().getIdLong()).thenCompose(member -> { return memberService.getMemberInServerAsync(modMailThread.getServer().getId(), messageFromUser.getAuthor().getIdLong()).thenCompose(member ->
Optional<TextChannel> textChannelFromServer = channelService.getTextChannelFromServerOptional(serverId, channelId); self.relayMessage(messageFromUser, serverId, channelId, modmailThreadId, member)
if(textChannelFromServer.isPresent()) { );
TextChannel textChannel = textChannelFromServer.get();
return self.sendUserReply(textChannel, modmailThreadId, messageFromUser, member, true);
} else {
log.warn("Closing mod mail thread {}, because it seems the channel {} in server {} got deleted.", modmailThreadId, channelId, serverId);
// in this case there was no text channel on the server associated with the mod mail thread
// close the existing one, so the user can start a new one
self.closeModMailThreadInDb(modmailThreadId);
String textToSend = templateService.renderTemplate("modmail_failed_to_forward_message", new Object());
return channelService.sendTextToChannel(textToSend, messageFromUser.getChannel());
}
});
} }
@Transactional
public CompletableFuture<Message> relayMessage(Message messageFromUser, Long serverId, Long channelId, Long modmailThreadId, Member member) {
Optional<TextChannel> textChannelFromServer = channelService.getTextChannelFromServerOptional(serverId, channelId);
if(textChannelFromServer.isPresent()) {
TextChannel textChannel = textChannelFromServer.get();
return self.sendUserReply(textChannel, modmailThreadId, messageFromUser, member, true);
} else {
log.warn("Closing mod mail thread {}, because it seems the channel {} in server {} got deleted.", modmailThreadId, channelId, serverId);
// in this case there was no text channel on the server associated with the mod mail thread
// close the existing one, so the user can start a new one
self.closeModMailThreadInDb(modmailThreadId);
String textToSend = templateService.renderTemplate("modmail_failed_to_forward_message", new Object());
return channelService.sendTextToChannel(textToSend, messageFromUser.getChannel());
}
}
/** /**
* This message takes a received {@link Message} from a user, renders it to a new message to send and sends it to * This message takes a received {@link Message} from a user, renders it to a new message to send and sends it to
* the appropriate {@link ModMailThread} channel, the returned promise only returns if the message was dealt with on the user * the appropriate {@link ModMailThread} channel, the returned promise only returns if the message was dealt with on the user
@@ -422,12 +468,21 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
if(subscriberList.isEmpty()) { if(subscriberList.isEmpty()) {
subscriberMemberFutures.add(CompletableFuture.completedFuture(null)); subscriberMemberFutures.add(CompletableFuture.completedFuture(null));
} }
log.debug("Pinging {} subscribers for modmail thread {}.", subscriberList.size(), modMailThreadId); log.debug("Mentioning {} subscribers for modmail thread {}.", subscriberList.size(), modMailThreadId);
} else { } else {
subscriberMemberFutures.add(CompletableFuture.completedFuture(null)); subscriberMemberFutures.add(CompletableFuture.completedFuture(null));
} }
return FutureUtils.toSingleFutureGeneric(subscriberMemberFutures).thenCompose(firstVoid -> { CompletableFuture<Message> messageFuture = new CompletableFuture<>();
List<FullUserInServer> subscribers = new ArrayList<>(); FutureUtils.toSingleFutureGeneric(subscriberMemberFutures).whenComplete((unused, throwable) -> {
if(throwable != null) {
log.warn("Failed to load subscriber users. Still relaying message.", throwable);
}
List<Member> subscribers = subscriberMemberFutures
.stream()
.filter(memberCompletableFuture -> !memberCompletableFuture.isCompletedExceptionally())
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList());
ModMailUserReplyModel modMailUserReplyModel = ModMailUserReplyModel ModMailUserReplyModel modMailUserReplyModel = ModMailUserReplyModel
.builder() .builder()
.postedMessage(messageFromUser) .postedMessage(messageFromUser)
@@ -436,19 +491,24 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
.build(); .build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_user_message", modMailUserReplyModel, textChannel.getGuild().getIdLong()); MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_user_message", modMailUserReplyModel, textChannel.getGuild().getIdLong());
List<CompletableFuture<Message>> completableFutures = channelService.sendMessageToSendToChannel(messageToSend, textChannel); List<CompletableFuture<Message>> completableFutures = channelService.sendMessageToSendToChannel(messageToSend, textChannel);
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])) CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]))
.thenCompose(aVoid -> { .thenCompose(aVoid -> {
log.debug("Adding read reaction to initial message for mod mail thread in channel {}.", textChannel.getGuild().getId()); log.debug("Adding read reaction to initial message for mod mail thread in channel {}.", textChannel.getGuild().getId());
return reactionService.addReactionToMessageAsync("readReaction", textChannel.getGuild().getIdLong(), messageFromUser); return reactionService.addReactionToMessageAsync("readReaction", textChannel.getGuild().getIdLong(), messageFromUser);
}) })
.thenApply(aVoid -> { .thenApply(aVoid -> {
Message createdMessage = completableFutures.get(0).join();
if(modMailThreadExists) { if(modMailThreadExists) {
self.postProcessSendMessages(textChannel, completableFutures.get(0).join(), messageFromUser); self.postProcessSendMessages(textChannel, createdMessage, messageFromUser);
} }
return completableFutures.get(0).join(); return messageFuture.complete(createdMessage);
}).exceptionally(throwable1 -> {
log.error("Failed to forward message to thread.", throwable1);
messageFuture.completeExceptionally(throwable1);
return null;
}); });
}); });
return messageFuture;
} }
@@ -474,6 +534,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
} }
@Override @Override
@Transactional
public CompletableFuture<Void> relayMessageToDm(Long modmailThreadId, String text, Message replyCommandMessage, boolean anonymous, MessageChannel feedBack, List<UndoActionInstance> undoActions, Member targetMember) { public CompletableFuture<Void> relayMessageToDm(Long modmailThreadId, String text, Message replyCommandMessage, boolean anonymous, MessageChannel feedBack, List<UndoActionInstance> undoActions, Member targetMember) {
log.info("Relaying message {} to user {} in modmail thread {} on server {}.", replyCommandMessage.getId(), targetMember.getId(), modmailThreadId, targetMember.getGuild().getId()); log.info("Relaying message {} to user {} in modmail thread {} on server {}.", replyCommandMessage.getId(), targetMember.getId(), modmailThreadId, targetMember.getGuild().getId());
AUserInAServer moderator = userInServerManagementService.loadOrCreateUser(replyCommandMessage.getMember()); AUserInAServer moderator = userInServerManagementService.loadOrCreateUser(replyCommandMessage.getMember());
@@ -514,32 +575,35 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
} }
@Override @Override
public CompletableFuture<Void> closeModMailThread(ModMailThread modMailThread, String note, boolean notifyUser, List<UndoActionInstance> undoActions, Boolean log) { public CompletableFuture<Void> closeModMailThreadEvaluateLogging(ModMailThread modMailThread, ClosingContext closingConfig, List<UndoActionInstance> undoActions) {
boolean loggingMode = featureModeService.featureModeActive(ModMailFeatureDefinition.MOD_MAIL, modMailThread.getServer(), ModMailMode.LOGGING); boolean loggingMode = featureModeService.featureModeActive(ModMailFeatureDefinition.MOD_MAIL, modMailThread.getServer(), ModMailMode.LOGGING);
boolean shouldLogThread = log && loggingMode; closingConfig.setLog(closingConfig.getLog() && loggingMode);
return closeModMailThread(modMailThread, note, notifyUser, shouldLogThread, undoActions); return closeModMailThread(modMailThread, closingConfig, undoActions);
} }
@Override @Override
public CompletableFuture<Void> closeModMailThread(ModMailThread modMailThread, String note, boolean notifyUser, boolean logThread, List<UndoActionInstance> undoActions) { public CompletableFuture<Void> closeModMailThread(ModMailThread modMailThread, ClosingContext closingConfig, List<UndoActionInstance> undoActions) {
metricService.incrementCounter(MODMAIL_THREAD_CLOSED_COUNTER); metricService.incrementCounter(MODMAIL_THREAD_CLOSED_COUNTER);
Long modMailThreadId = modMailThread.getId(); Long modMailThreadId = modMailThread.getId();
log.info("Starting closing procedure for thread {}", modMailThread.getId()); log.info("Starting closing procedure for thread {}", modMailThread.getId());
List<ModMailMessage> modMailMessages = modMailThread.getMessages(); List<ModMailMessage> modMailMessages = modMailThread.getMessages();
Long userId = modMailThread.getUser().getUserReference().getId(); Long userId = modMailThread.getUser().getUserReference().getId();
Long serverId = modMailThread.getServer().getId(); Long serverId = modMailThread.getServer().getId();
if(logThread) { if(closingConfig.getLog()) {
LoadedModmailThreadMessageList messages = modMailMessageService.loadModMailMessages(modMailMessages); if(!modMailMessages.isEmpty()) {
CompletableFuture<Void> messagesFuture = FutureUtils.toSingleFuture(messages.getAllFutures()); return modMailMessageService.loadModMailMessages(modMailMessages)
.thenAccept(loadedModmailThreadMessages -> self.logMessagesToModMailLog(closingConfig, modMailThreadId, undoActions, loadedModmailThreadMessages, serverId, userId));
return messagesFuture.handle((aVoid, throwable) -> } else {
self.logMessagesToModMailLog(note, notifyUser, modMailThreadId, undoActions, messages, serverId, userId) log.info("Modmail thread {} in server {} has no messages. Only logging header.", modMailThreadId, serverId);
).toCompletableFuture().thenCompose(o -> o); return loadUserAndSendClosingHeader(modMailThread, closingConfig)
.thenAccept(unused -> memberService.getMemberInServerAsync(modMailThread.getUser()).thenCompose(member ->
self.afterSuccessfulLog(modMailThreadId, closingConfig.getNotifyUser(), member, undoActions)
));
}
} else { } else {
log.debug("Not logging modmail thread {}.", modMailThreadId); log.debug("Not logging modmail thread {}.", modMailThreadId);
return memberService.getMemberInServerAsync(modMailThread.getUser()).thenCompose(member -> return memberService.getMemberInServerAsync(modMailThread.getUser()).thenCompose(member ->
self.afterSuccessfulLog(modMailThreadId, notifyUser, member, undoActions) self.afterSuccessfulLog(modMailThreadId, closingConfig.getNotifyUser(), member, undoActions)
); );
} }
} }
@@ -558,8 +622,6 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
/** /**
* This method takes the actively loaded futures, calls the method responsible for logging the messages, and calls the method * This method takes the actively loaded futures, calls the method responsible for logging the messages, and calls the method
* after the logging has been done. * after the logging has been done.
* @param note The note which was provided when closing the {@link ModMailThread}
* @param notifyUser Whether or not to notify the user
* @param modMailThreadId The ID of the {@link ModMailThread} which is being closed * @param modMailThreadId The ID of the {@link ModMailThread} which is being closed
* @param undoActions The list of {@link UndoActionInstance} to execute in case of exceptions * @param undoActions The list of {@link UndoActionInstance} to execute in case of exceptions
* @param messages The list of loaded {@link Message} to log * @param messages The list of loaded {@link Message} to log
@@ -568,23 +630,24 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* @return A {@link CompletableFuture future} which completes when the messages have been logged * @return A {@link CompletableFuture future} which completes when the messages have been logged
*/ */
@Transactional @Transactional
public CompletableFuture<Void> logMessagesToModMailLog(String note, Boolean notifyUser, Long modMailThreadId, List<UndoActionInstance> undoActions, LoadedModmailThreadMessageList messages, Long serverId, Long userId) { public CompletableFuture<Void> logMessagesToModMailLog(ClosingContext closingContext, Long modMailThreadId, List<UndoActionInstance> undoActions,
log.debug("Logging {} modmail messages for modmail thread {}.", messages.getMessageList().size(), modMailThreadId); ModmailLoggingThreadMessages messages, Long serverId, Long userId) {
log.debug("Logging {} modmail messages for modmail thread {}.", messages.getMessages().size(), modMailThreadId);
try { try {
CompletableFutureList<Message> list = self.logModMailThread(modMailThreadId, messages, note, undoActions); return self.logModMailThread(modMailThreadId, messages, closingContext, undoActions, serverId)
return list.getMainFuture().thenCompose(avoid -> { .thenCompose(list -> list.getMainFuture().thenCompose(unused -> {
list.getFutures().forEach(messageCompletableFuture -> { list.getFutures().forEach(messageCompletableFuture -> {
Message message = messageCompletableFuture.join(); Message message = messageCompletableFuture.join();
undoActions.add(UndoActionInstance.getMessageDeleteAction(message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong())); undoActions.add(UndoActionInstance.getMessageDeleteAction(message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong()));
}); });
return memberService.getMemberInServerAsync(serverId, userId).thenCompose(member -> return memberService.getMemberInServerAsync(serverId, userId).thenCompose(member ->
self.afterSuccessfulLog(modMailThreadId, notifyUser, member, undoActions) self.afterSuccessfulLog(modMailThreadId, closingContext.getNotifyUser(), member, undoActions)
).exceptionally(throwable -> { ).exceptionally(throwable -> {
log.warn("Failed to retrieve member for closing the modmail thread. Closing without member information.", throwable); log.warn("Failed to retrieve member for closing the modmail thread. Closing without member information.", throwable);
self.afterSuccessfulLog(modMailThreadId, false, null, undoActions); self.afterSuccessfulLog(modMailThreadId, false, null, undoActions);
return null; return null;
}); });
}); }));
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to log mod mail messages", e); log.error("Failed to log mod mail messages", e);
throw new AbstractoRunTimeException(e); throw new AbstractoRunTimeException(e);
@@ -663,74 +726,99 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* log concerning general information about the closed {@link ModMailThread} * log concerning general information about the closed {@link ModMailThread}
* @param modMailThreadId The ID of the {@link ModMailThread} to log the messages of * @param modMailThreadId The ID of the {@link ModMailThread} to log the messages of
* @param messages The list of {@link CompletableFuture} which contain the {@link Message} which could be loaded * @param messages The list of {@link CompletableFuture} which contain the {@link Message} which could be loaded
* @param note The note which was entered when closing the {@link ModMailThread}
* @param undoActions A list of {@link dev.sheldan.abstracto.core.models.UndoAction actions} to be undone in case the operation fails. This list will be filled in the method. * @param undoActions A list of {@link dev.sheldan.abstracto.core.models.UndoAction actions} to be undone in case the operation fails. This list will be filled in the method.
* @param serverId The ID of the {@link Guild server} the modmail thread is in
* @throws ModMailThreadNotFoundException in case the {@link ModMailThread} is not found by the ID * @throws ModMailThreadNotFoundException in case the {@link ModMailThread} is not found by the ID
* @return An instance of {@link CompletableFutureList}, which contains a main {@link CompletableFuture} which is resolved, * @return An instance of {@link CompletableFutureList}, which contains a main {@link CompletableFuture} which is resolved,
* when all of the smaller {@link CompletableFuture} in it are resolved. We need this construct, because we need to access * when all of the smaller {@link CompletableFuture} in it are resolved. We need this construct, because we need to access
* the result values of the individual futures after they are done. * the result values of the individual futures after they are done.
*/ */
@Transactional @Transactional
public CompletableFutureList<Message> logModMailThread(Long modMailThreadId, LoadedModmailThreadMessageList messages, String note, List<UndoActionInstance> undoActions) { public CompletableFuture<CompletableFutureList<Message>> logModMailThread(Long modMailThreadId, ModmailLoggingThreadMessages messages,
log.info("Logging mod mail thread {} with {} messages.", modMailThreadId, messages.getMessageList().size()); ClosingContext context, List<UndoActionInstance> undoActions, Long serverId) {
log.info("Logging mod mail thread {} with {} messages.", modMailThreadId, messages.getMessages().size());
if(messages.getMessages().isEmpty()) {
log.info("Modmail thread {} is empty. No messages to log.", modMailThreadId);
return CompletableFuture.completedFuture(new CompletableFutureList<>(new ArrayList<>()));
}
TextChannel channel = channelService.getTextChannelFromServer(serverId, modMailThreadId);
ClosingProgressModel progressModel = ClosingProgressModel
.builder()
.loggedMessages(0)
.totalMessages(messages.getMessages().size())
.build();
List<CompletableFuture<Message>> updateMessageFutures = channelService.sendEmbedTemplateInTextChannelList(MODMAIL_CLOSE_PROGRESS_TEMPLATE_KEY, progressModel, channel);
return FutureUtils.toSingleFutureGeneric(updateMessageFutures)
.thenApply(updateMessage -> self.logMessages(modMailThreadId, messages, context, updateMessageFutures.get(0).join()));
}
@Transactional
public CompletableFutureList<Message> logMessages(Long modMailThreadId, ModmailLoggingThreadMessages messages, ClosingContext context, Message updateMessage) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getByIdOptional(modMailThreadId); Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getByIdOptional(modMailThreadId);
if(modMailThreadOpt.isPresent()) { if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get(); ModMailThread modMailThread = modMailThreadOpt.get();
List<ModMailLoggedMessageModel> loggedMessages = new ArrayList<>(); List<ModMailLoggedMessageModel> loggedMessages = new ArrayList<>();
messages.getMessageList().forEach(futures -> { Map<Long, User> authors = messages
try { .getAuthors()
CompletableFuture<Message> future = futures.getMessageFuture(); .stream().collect(Collectors.toMap(ISnowflake::getIdLong, Function.identity()));
if(!future.isCompletedExceptionally()) { messages.getMessages().forEach(message -> {
Message loadedMessage = future.join(); log.info("Logging message {} in modmail thread {}.", message.getId(), modMailThreadId);
if(loadedMessage != null) { ModMailMessage modmailMessage = modMailThread.getMessages()
log.info("Logging message {} in modmail thread {}.", loadedMessage.getId(), modMailThreadId); .stream()
ModMailMessage modmailMessage = modMailThread.getMessages() .filter(modMailMessage -> {
.stream() if(modMailMessage.getDmChannel()) {
.filter(modMailMessage -> { return modMailMessage.getCreatedMessageInDM().equals(message.getIdLong());
if(modMailMessage.getDmChannel()) { } else {
return modMailMessage.getCreatedMessageInDM().equals(loadedMessage.getIdLong()); return modMailMessage.getCreatedMessageInChannel().equals(message.getIdLong());
} else { }
return modMailMessage.getCreatedMessageInChannel().equals(loadedMessage.getIdLong()); })
} .findFirst().orElseThrow(() -> new AbstractoRunTimeException("Could not find desired message in list of messages in thread. This should not happen, as we just retrieved them from the same place."));
}) User author = authors.getOrDefault(modmailMessage.getAuthor().getUserReference().getId(), message.getJDA().getSelfUser());
.findFirst().orElseThrow(() -> new AbstractoRunTimeException("Could not find desired message in list of messages in thread. This should not happen, as we just retrieved them from the same place.")); ModMailLoggedMessageModel modMailLoggedMessageModel =
Member author = futures.getMemberFuture().join(); ModMailLoggedMessageModel
ModMailLoggedMessageModel modMailLoggedMessageModel = .builder()
ModMailLoggedMessageModel .message(message)
.builder() .author(author)
.message(loadedMessage) .modMailMessage(modmailMessage)
.modMailMessage(modmailMessage) .build();
.author(author) // doesnt work for the messages from the DM channel loggedMessages.add(modMailLoggedMessageModel);
.build();
loggedMessages.add(modMailLoggedMessageModel);
}
} else {
log.warn("One future failed to load. Will not log a message in modmail thread {}.", modMailThreadId);
}
} catch (Exception e) {
log.error("Failed handle the loaded messages.", e);
}
}); });
List<CompletableFuture<Message>> completableFutures = new ArrayList<>(); List<CompletableFuture<Message>> completableFutures = new ArrayList<>();
// TODO dont use this
modMailThread.setClosed(Instant.now());
ModMailClosingHeaderModel headerModel = ModMailClosingHeaderModel
.builder()
.closedThread(modMailThread)
.note(note)
.build();
log.debug("Sending close header and individual mod mail messages to mod mail log target for thread {}.", modMailThreadId); log.debug("Sending close header and individual mod mail messages to mod mail log target for thread {}.", modMailThreadId);
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_close_header", headerModel, modMailThread.getServer().getId()); CompletableFuture<Message> headerFuture = loadUserAndSendClosingHeader(modMailThread, context);
List<CompletableFuture<Message>> closeHeaderFutures = postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, modMailThread.getServer().getId());
// TODO in case the rendering fails, the already sent messages are not deleted // TODO in case the rendering fails, the already sent messages are not deleted
completableFutures.addAll(closeHeaderFutures); completableFutures.add(headerFuture);
completableFutures.addAll(self.sendMessagesToPostTarget(modMailThread, loggedMessages)); completableFutures.addAll(self.sendMessagesToPostTarget(modMailThread, loggedMessages, updateMessage));
return new CompletableFutureList<>(completableFutures); return new CompletableFutureList<>(completableFutures);
} else { } else {
throw new ModMailThreadNotFoundException(modMailThreadId); throw new ModMailThreadNotFoundException(modMailThreadId);
} }
} }
private CompletableFuture<Message> loadUserAndSendClosingHeader(ModMailThread modMailThread, ClosingContext closingContext) {
ModMailClosingHeaderModel headerModel = ModMailClosingHeaderModel
.builder()
.closingMember(closingContext.getClosingMember())
.note(closingContext.getNote())
.silently(closingContext.getNotifyUser())
.messageCount(modMailThread.getMessages().size())
.startDate(modMailThread.getCreated())
.serverId(modMailThread.getServer().getId())
.silently(!closingContext.getNotifyUser())
.userId(modMailThread.getUser().getUserReference().getId())
.build();
return userService.retrieveUserForId(modMailThread.getUser().getUserReference().getId()).thenApply(user -> {
headerModel.setUser(user);
return self.sendClosingHeader(headerModel).get(0);
}).thenCompose(Function.identity());
}
@Transactional
public List<CompletableFuture<Message>> sendClosingHeader(ModMailClosingHeaderModel model) {
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_close_header", model, model.getServerId());
return postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, model.getServerId());
}
/** /**
* Sets the {@link ModMailThread} in the database to CLOSED. * Sets the {@link ModMailThread} in the database to CLOSED.
* @param modMailThreadId The ID of the {@link ModMailThread} to update the state of * @param modMailThreadId The ID of the {@link ModMailThread} to update the state of
@@ -756,15 +844,25 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* @param loadedMessages The list of {@link ModMailLoggedMessageModel} which can be rendered * @param loadedMessages The list of {@link ModMailLoggedMessageModel} which can be rendered
* @return A list of {@link CompletableFuture} which represent each of the messages being send to the {@link PostTarget} * @return A list of {@link CompletableFuture} which represent each of the messages being send to the {@link PostTarget}
*/ */
public List<CompletableFuture<Message>> sendMessagesToPostTarget(ModMailThread modMailThread, List<ModMailLoggedMessageModel> loadedMessages) { public List<CompletableFuture<Message>> sendMessagesToPostTarget(ModMailThread modMailThread, List<ModMailLoggedMessageModel> loadedMessages, Message updateMessage) {
List<CompletableFuture<Message>> messageFutures = new ArrayList<>(); List<CompletableFuture<Message>> messageFutures = new ArrayList<>();
// TODO order messages ClosingProgressModel progressModel = ClosingProgressModel
loadedMessages.forEach(message -> { .builder()
log.debug("Sending message {} of modmail thread {} to modmail log post target.", modMailThread.getId(), message.getMessage().getId()); .loggedMessages(0)
.totalMessages(loadedMessages.size())
.build();
loadedMessages = loadedMessages.stream().sorted(Comparator.comparing(o -> o.getMessage().getTimeCreated())).collect(Collectors.toList());
for (int i = 0; i < loadedMessages.size(); i++) {
ModMailLoggedMessageModel message = loadedMessages.get(i);
log.debug("Sending message {} of modmail thread {} to modmail log post target.", modMailThread.getId(), message.getMessage().getId());
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_close_logged_message", message, modMailThread.getServer().getId()); MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_close_logged_message", message, modMailThread.getServer().getId());
List<CompletableFuture<Message>> logFuture = postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, modMailThread.getServer().getId()); List<CompletableFuture<Message>> logFuture = postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, modMailThread.getServer().getId());
if(i != 0 && (i % 10) == 0) {
progressModel.setLoggedMessages(i);
messageService.editMessageWithNewTemplate(updateMessage, MODMAIL_CLOSE_PROGRESS_TEMPLATE_KEY, progressModel);
}
messageFutures.addAll(logFuture); messageFutures.addAll(logFuture);
}); }
return messageFutures; return messageFutures;
} }

View File

@@ -104,6 +104,7 @@ public class ModMailThreadManagementServiceBean implements ModMailThreadManageme
public ModMailThread createModMailThread(AUserInAServer userInAServer, AChannel channel) { public ModMailThread createModMailThread(AUserInAServer userInAServer, AChannel channel) {
ModMailThread thread = ModMailThread ModMailThread thread = ModMailThread
.builder() .builder()
.id(channel.getId())
.channel(channel) .channel(channel)
.created(Instant.now()) .created(Instant.now())
.user(userInAServer) .user(userInAServer)

View File

@@ -28,7 +28,7 @@ public class ModMailCategoryDelayedAction implements DelayedAction {
public void execute(DelayedActionConfig delayedActionConfig) { public void execute(DelayedActionConfig delayedActionConfig) {
ModMailCategoryDelayedActionConfig concrete = (ModMailCategoryDelayedActionConfig) delayedActionConfig; ModMailCategoryDelayedActionConfig concrete = (ModMailCategoryDelayedActionConfig) delayedActionConfig;
log.info("Executing delayed action for configuration the mdomail category to {} in server {}.", concrete.getCategoryId(), concrete.getServerId()); log.info("Executing delayed action for configuration the mdomail category to {} in server {}.", concrete.getCategoryId(), concrete.getServerId());
configService.setLongValue(ModMailThreadServiceBean.MODMAIL_CATEGORY, concrete.getServerId(), concrete.getCategoryId()); configService.setOrCreateConfigValue(ModMailThreadServiceBean.MODMAIL_CATEGORY, concrete.getServerId(), concrete.getCategoryId().toString());
} }
/** /**

View File

@@ -20,11 +20,6 @@
<column name="module_id" valueComputed="${modmailModule}"/> <column name="module_id" valueComputed="${modmailModule}"/>
<column name="feature_id" valueComputed="${modmailFeature}"/> <column name="feature_id" valueComputed="${modmailFeature}"/>
</insert> </insert>
<insert tableName="command">
<column name="name" value="closeNoLog"/>
<column name="module_id" valueComputed="${modmailModule}"/>
<column name="feature_id" valueComputed="${modmailFeature}"/>
</insert>
<insert tableName="command"> <insert tableName="command">
<column name="name" value="closeSilently"/> <column name="name" value="closeSilently"/>
<column name="module_id" valueComputed="${modmailModule}"/> <column name="module_id" valueComputed="${modmailModule}"/>

View File

@@ -1,6 +1,9 @@
abstracto.systemConfigs.modMailClosingText.name=modMailClosingText abstracto.systemConfigs.modMailClosingText.name=modMailClosingText
abstracto.systemConfigs.modMailClosingText.stringValue=Thread has been closed. abstracto.systemConfigs.modMailClosingText.stringValue=Thread has been closed.
abstracto.systemConfigs.modmailCategory.name=modmailCategory
abstracto.systemConfigs.modmailCategory.longValue=0
abstracto.featureFlags.modmail.featureName=modmail abstracto.featureFlags.modmail.featureName=modmail
abstracto.featureFlags.modmail.enabled=false abstracto.featureFlags.modmail.enabled=false
@@ -12,5 +15,5 @@ abstracto.featureModes.log.mode=log
abstracto.featureModes.log.enabled=true abstracto.featureModes.log.enabled=true
abstracto.featureModes.threadMessage.featureName=modmail abstracto.featureModes.threadMessage.featureName=modmail
abstracto.featureModes.threadMessage.mode=filterNotifications abstracto.featureModes.threadMessage.mode=threadMessage
abstracto.featureModes.threadMessage.enabled=true abstracto.featureModes.threadMessage.enabled=true

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.modmail.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
@Getter
@Setter
@Builder
public class ClosingContext {
private Boolean notifyUser;
private Boolean log;
private Member closingMember;
private String note;
}

View File

@@ -26,7 +26,6 @@ import java.util.List;
public class ModMailThread implements Serializable { public class ModMailThread implements Serializable {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false) @Column(name = "id", nullable = false)
private Long id; private Long id;

View File

@@ -1,17 +0,0 @@
package dev.sheldan.abstracto.modmail.model.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import java.util.concurrent.CompletableFuture;
@Getter
@Setter
@Builder
public class LoadedModmailThreadMessage {
private CompletableFuture<Message> messageFuture;
private CompletableFuture<Member> memberFuture;
}

View File

@@ -1,24 +0,0 @@
package dev.sheldan.abstracto.modmail.model.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Getter
@Setter
@Builder
public class LoadedModmailThreadMessageList {
private List<LoadedModmailThreadMessage> messageList;
public List<CompletableFuture> getAllFutures() {
List<CompletableFuture> futures = new ArrayList<>();
messageList.forEach(loadedModmailThreadMessage -> {
futures.add(loadedModmailThreadMessage.getMemberFuture());
futures.add(loadedModmailThreadMessage.getMessageFuture());
});
return futures;
}
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.modmail.model.template;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ClosingProgressModel {
private Integer loggedMessages;
private Integer totalMessages;
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.modmail.model.template;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.TextChannel;
@Getter
@Setter
@Builder
public class ContactNotificationModel {
private Member targetMember;
private TextChannel createdChannel;
}

View File

@@ -4,8 +4,11 @@ import dev.sheldan.abstracto.modmail.model.database.ModMailThread;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
/** /**
* This model is used when rendering the message before logging the messages in a closed {@link ModMailThread} and contains * This model is used when rendering the message before logging the messages in a closed {@link ModMailThread} and contains
@@ -22,13 +25,20 @@ public class ModMailClosingHeaderModel {
/** /**
* The {@link ModMailThread} which was closed * The {@link ModMailThread} which was closed
*/ */
private ModMailThread closedThread; private Integer messageCount;
private Instant startDate;
private Long userId;
/** /**
* The duration between the creation and closed date of a {@link ModMailThread} * The duration between the creation and closed date of a {@link ModMailThread}
* @return The duration between the creation date and the date the thread has been closed * @return The duration between the creation date and the date the thread has been closed
*/ */
public Duration getDuration() { public Duration getDuration() {
return Duration.between(closedThread.getCreated(), closedThread.getClosed()); return Duration.between(startDate, Instant.now());
} }
private Member closingMember;
private Boolean silently;
private User user;
private Long serverId;
} }

View File

@@ -1,12 +1,11 @@
package dev.sheldan.abstracto.modmail.model.template; package dev.sheldan.abstracto.modmail.model.template;
import dev.sheldan.abstracto.core.models.FullUserInServer;
import dev.sheldan.abstracto.modmail.model.database.ModMailMessage; import dev.sheldan.abstracto.modmail.model.database.ModMailMessage;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
/** /**
* This model is used to render a message from a mod mail thread when closing the thread and logging the thread to the logging post target * This model is used to render a message from a mod mail thread when closing the thread and logging the thread to the logging post target
@@ -25,9 +24,9 @@ public class ModMailLoggedMessageModel {
private ModMailMessage modMailMessage; private ModMailMessage modMailMessage;
/** /**
* A reference to the {@link FullUserInServer} which is the author. The member part is null, if the member left the guild. * A reference to the {@link User} which is the author. The member part is null, if the member left the guild.
*/ */
private Member author; private User author;
} }

View File

@@ -1,6 +1,5 @@
package dev.sheldan.abstracto.modmail.model.template; package dev.sheldan.abstracto.modmail.model.template;
import dev.sheldan.abstracto.core.models.FullUserInServer;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -25,8 +24,8 @@ public class ModMailUserReplyModel {
*/ */
private Message postedMessage; private Message postedMessage;
/** /**
* List of {@link FullUserInServer} which are registered as subscribers for a particular mod mail thread and will be pinged * List of {@link Member} which are registered as subscribers for a particular mod mail thread and will be pinged
* when the user sends a new message * when the user sends a new message
*/ */
private List<FullUserInServer> subscribers; private List<Member> subscribers;
} }

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.modmail.model.template;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import java.util.List;
@Getter
@Setter
@Builder
public class ModmailLoggingThreadMessages {
private List<Message> messages;
private List<User> authors;
}

View File

@@ -1,19 +1,11 @@
package dev.sheldan.abstracto.modmail.service; package dev.sheldan.abstracto.modmail.service;
import dev.sheldan.abstracto.modmail.model.database.ModMailMessage; import dev.sheldan.abstracto.modmail.model.database.ModMailMessage;
import dev.sheldan.abstracto.modmail.model.dto.LoadedModmailThreadMessageList; import dev.sheldan.abstracto.modmail.model.template.ModmailLoggingThreadMessages;
import net.dv8tion.jda.api.entities.Message;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Service to handle the messages of a {@link dev.sheldan.abstracto.modmail.model.database.ModMailThread}
*/
public interface ModMailMessageService { public interface ModMailMessageService {
/** CompletableFuture<ModmailLoggingThreadMessages> loadModMailMessages(List<ModMailMessage> modMailMessages);
* Loads the given mod mail messages in the form of {@link Message} from Discord and returns the created promises, some of which might fail, if the message was already deleted
* @param modMailMessages The list of {@link ModMailMessage} to load
* @return A instance of {@link LoadedModmailThreadMessageList} which contain the individual results of actively loading the {@link Message} and the {@link net.dv8tion.jda.api.entities.Member}
*/
LoadedModmailThreadMessageList loadModMailMessages(List<ModMailMessage> modMailMessages);
} }

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.core.models.UndoActionInstance;
import dev.sheldan.abstracto.core.models.database.AChannel; import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUser; import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer; import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.modmail.model.ClosingContext;
import dev.sheldan.abstracto.modmail.model.database.ModMailThread; import dev.sheldan.abstracto.modmail.model.database.ModMailThread;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@@ -79,13 +80,10 @@ public interface ModMailThreadService {
* post target. This also takes an optional note, which will be displayed in the first message of the logging. This method changes the state of the * post target. This also takes an optional note, which will be displayed in the first message of the logging. This method changes the state of the
* {@link ModMailThread} to CLOSED and notifies the user about closing. * {@link ModMailThread} to CLOSED and notifies the user about closing.
* @param modMailThread The {@link ModMailThread} which is being closed. * @param modMailThread The {@link ModMailThread} which is being closed.
* @param note The text of the note used for the header message of the logged mod mail thread.
* @param notifyUser Whether or not the user should be notified
* @param log whether or not the closed {@link ModMailThread} should be logged (if the {@link dev.sheldan.abstracto.core.config.FeatureMode} is enabled)
* @param undoActions A list of {@link dev.sheldan.abstracto.core.models.UndoAction actions} to be undone in case the operation fails. This list will be filled in the method. * @param undoActions A list of {@link dev.sheldan.abstracto.core.models.UndoAction actions} to be undone in case the operation fails. This list will be filled in the method.
* @return A {@link CompletableFuture future} which completes when the {@link ModMailThread thread} has been closed. * @return A {@link CompletableFuture future} which completes when the {@link ModMailThread thread} has been closed.
*/ */
CompletableFuture<Void> closeModMailThread(ModMailThread modMailThread, String note, boolean notifyUser, List<UndoActionInstance> undoActions, Boolean log); CompletableFuture<Void> closeModMailThreadEvaluateLogging(ModMailThread modMailThread, ClosingContext closingConfig, List<UndoActionInstance> undoActions);
/** /**
* Closes the mod mail thread which means: deletes the {@link net.dv8tion.jda.api.entities.TextChannel} associated with the mod mail thread, * Closes the mod mail thread which means: deletes the {@link net.dv8tion.jda.api.entities.TextChannel} associated with the mod mail thread,
@@ -93,14 +91,11 @@ public interface ModMailThreadService {
* be displayed in the first message of the logging. This method changes the state of the {@link ModMailThread} to * be displayed in the first message of the logging. This method changes the state of the {@link ModMailThread} to
* CLOSED and notifies the user about closing. * CLOSED and notifies the user about closing.
* @param modMailThread The {@link ModMailThread} which is being closed. * @param modMailThread The {@link ModMailThread} which is being closed.
* @param note The text of the note used for the header message of the logged mod mail thread, this is only required when actually * @param closingConfig The {@link ClosingContext config} how the thread shoudl be closed
* logging the mod mail thread
* @param notifyUser Whether or not the user should be notified
* @param logThread Whether or not the thread should be logged to the appropriate post target
* @param undoActions A list of {@link dev.sheldan.abstracto.core.models.UndoAction actions} to be undone in case the operation fails. This list will be filled in the method. * @param undoActions A list of {@link dev.sheldan.abstracto.core.models.UndoAction actions} to be undone in case the operation fails. This list will be filled in the method.
* @return A {@link CompletableFuture future} which completes when the {@link ModMailThread thread} has been closed * @return A {@link CompletableFuture future} which completes when the {@link ModMailThread thread} has been closed
*/ */
CompletableFuture<Void> closeModMailThread(ModMailThread modMailThread, String note, boolean notifyUser, boolean logThread, List<UndoActionInstance> undoActions); CompletableFuture<Void> closeModMailThread(ModMailThread modMailThread, ClosingContext closingConfig, List<UndoActionInstance> undoActions);
boolean isModMailThread(AChannel channel); boolean isModMailThread(AChannel channel);
boolean isModMailThread(Long channelId); boolean isModMailThread(Long channelId);

View File

@@ -281,7 +281,7 @@ public class ChannelServiceBean implements ChannelService {
@Override @Override
public CompletableFuture<Message> editMessageInAChannelFuture(MessageToSend messageToSend, MessageChannel channel, Long messageId) { public CompletableFuture<Message> editMessageInAChannelFuture(MessageToSend messageToSend, MessageChannel channel, Long messageId) {
MessageAction messageAction; MessageAction messageAction;
if(!StringUtils.isBlank(messageToSend.getMessages().get(0))) { if(!messageToSend.getMessages().isEmpty() && !StringUtils.isBlank(messageToSend.getMessages().get(0))) {
log.debug("Editing message {} with new text content.", messageId); log.debug("Editing message {} with new text content.", messageId);
messageAction = channel.editMessageById(messageId, messageToSend.getMessages().get(0)); messageAction = channel.editMessageById(messageId, messageToSend.getMessages().get(0));
if(messageToSend.getEmbeds() != null && !messageToSend.getEmbeds().isEmpty()) { if(messageToSend.getEmbeds() != null && !messageToSend.getEmbeds().isEmpty()) {

View File

@@ -205,6 +205,12 @@ public class MessageServiceBean implements MessageService {
return loadMessage(message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong()); return loadMessage(message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong());
} }
@Override
public CompletableFuture<Message> editMessageWithNewTemplate(Message message, String templateKey, Object model) {
MessageToSend messageToSend = templateService.renderEmbedTemplate(templateKey, model, message.getGuild().getIdLong());
return channelService.editMessageInAChannelFuture(messageToSend, message.getChannel(), message.getIdLong());
}
@Override @Override
public MessageAction editMessage(Message message, MessageEmbed messageEmbed) { public MessageAction editMessage(Message message, MessageEmbed messageEmbed) {
metricService.incrementCounter(MESSAGE_EDIT_METRIC); metricService.incrementCounter(MESSAGE_EDIT_METRIC);

View File

@@ -35,6 +35,7 @@ public interface MessageService {
CompletableFuture<Message> loadMessageFromCachedMessage(CachedMessage cachedMessage); CompletableFuture<Message> loadMessageFromCachedMessage(CachedMessage cachedMessage);
CompletableFuture<Message> loadMessage(Long serverId, Long channelId, Long messageId); CompletableFuture<Message> loadMessage(Long serverId, Long channelId, Long messageId);
CompletableFuture<Message> loadMessage(Message message); CompletableFuture<Message> loadMessage(Message message);
CompletableFuture<Message> editMessageWithNewTemplate(Message message, String templateKey, Object model);
MessageAction editMessage(Message message, MessageEmbed messageEmbed); MessageAction editMessage(Message message, MessageEmbed messageEmbed);
MessageAction editMessage(Message message, String text, MessageEmbed messageEmbed); MessageAction editMessage(Message message, String text, MessageEmbed messageEmbed);
AuditableRestAction<Void> deleteMessageWithAction(Message message); AuditableRestAction<Void> deleteMessageWithAction(Message message);

View File

@@ -74,8 +74,4 @@ Closing the mod mail thread without notifying the user::
* Usage: `closeSilently [note]` * Usage: `closeSilently [note]`
* Description: Closes the thread, deletes the text channel containing the thread and logs the interactions between the member and the moderators in the `modmailLog` post target. (only if `modmail_logging` is enabled) * Description: Closes the thread, deletes the text channel containing the thread and logs the interactions between the member and the moderators in the `modmailLog` post target. (only if `modmail_logging` is enabled)
When closing a thread, a closing header with general information will be send and the note will be displayed there. When closing a thread, a closing header with general information will be send and the note will be displayed there.
Close a thread without logging::
* Usage: `closeNoLog`
* Description: Closes the thread without notifying the user and without logging the messages.
* Mode Restriction: This command is only available when the feature mode `log` is enabled.