[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>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>metrics-int</artifactId>

View File

@@ -1,6 +1,5 @@
package dev.sheldan.abstracto.linkembed.listener;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.ButtonClickedListener;
@@ -26,9 +25,6 @@ import org.springframework.transaction.annotation.Transactional;
@Slf4j
public class MessageEmbedDeleteButtonClickedListener implements ButtonClickedListener {
@Autowired
private Gson gson;
@Autowired
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.modmail.condition.ModMailContextCondition;
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.service.ModMailThreadService;
import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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
*/
@Component
@Slf4j
public class Close extends AbstractConditionableCommand {
public static final String MODMAIL_CLOSE_DEFAULT_NOTE_TEMPLATE_KEY = "modmail_close_default_note";
@Autowired
private ModMailContextCondition requiresModMailCondition;
@@ -50,10 +54,17 @@ public class Close extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
// 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());
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());
}

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.modmail.condition.ModMailContextCondition;
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.service.ModMailThreadService;
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());
AChannel channel = channelManagementService.loadChannel(commandContext.getChannel());
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());
}

View File

@@ -75,7 +75,8 @@ public class ModMailMessageEditedListener implements AsyncMessageUpdatedListener
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());
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)) {
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);
@@ -85,7 +86,10 @@ public class ModMailMessageEditedListener implements AsyncMessageUpdatedListener
CompletableFuture<Member> loadEditingUser = memberService.getMemberInServerAsync(messageBefore.getServerId(), modMailMessage.getAuthor().getUserReference().getId());
CompletableFuture.allOf(parameterParseFuture, loadTargetUser, loadEditingUser).thenAccept(unused ->
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;
}

View File

@@ -1,25 +1,24 @@
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.service.BotService;
import dev.sheldan.abstracto.core.service.ChannelService;
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.ModMailThread;
import dev.sheldan.abstracto.modmail.model.dto.LoadedModmailThreadMessage;
import dev.sheldan.abstracto.modmail.model.dto.LoadedModmailThreadMessageList;
import dev.sheldan.abstracto.modmail.model.template.ModmailLoggingThreadMessages;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
@Slf4j
@@ -34,11 +33,16 @@ public class ModMailMessageServiceBean implements ModMailMessageService {
@Autowired
private ChannelService channelService;
@Autowired
private UserService userService;
private static final Integer HISTORY_RETRIEVAL_LIMIT = 100;
@Override
public LoadedModmailThreadMessageList loadModMailMessages(List<ModMailMessage> modMailMessages) {
public CompletableFuture<ModmailLoggingThreadMessages> loadModMailMessages(List<ModMailMessage> modMailMessages) {
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
ModMailThread thread = modMailMessages.get(0).getThreadReference();
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())
.serverId(thread.getServer().getId());
// 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())) {
log.debug("Message {} was from DM.", modMailMessage.getMessageId());
serverChannelMessageBuilder
@@ -59,53 +68,129 @@ public class ModMailMessageServiceBean implements ModMailMessageService {
}
messageIds.add(serverChannelMessageBuilder.build());
});
List<LoadedModmailThreadMessage> messageFutures = new ArrayList<>();
// add the place holder futures, which are then resolved one by one
// because we cannot directly fetch the messages, in case they are in a private channel
// the opening of a private channel is a rest operation it itself, so we need
// to create the promises here already, else the list is empty for example
modMailMessages.forEach(modMailMessage -> messageFutures.add(getLoadedModmailThreadMessage()));
List<Long> messageIdsToLoad = messageIds
.stream()
.map(ServerChannelMessageUser::getMessageId)
.collect(Collectors.toList());
Optional<TextChannel> textChannelFromServer = channelService.getTextChannelFromServerOptional(thread.getServer().getId(), thread.getChannel().getId());
if(textChannelFromServer.isPresent()) {
TextChannel modMailThread = textChannelFromServer.get();
Long userId = thread.getUser().getUserReference().getId();
botService.getInstance().openPrivateChannelById(userId).queue(privateChannel -> {
Iterator<LoadedModmailThreadMessage> iterator = messageFutures.iterator();
messageIds.forEach(serverChannelMessage -> {
log.debug("Loading message {}.", serverChannelMessage.getMessageId());
CompletableFuture<Message> messageFuture;
CompletableFuture<Member> memberFuture = memberService.getMemberInServerAsync(serverChannelMessage.getServerId(), serverChannelMessage.getUserId());
if(serverChannelMessage.getChannelId() == null){
messageFuture = channelService.retrieveMessageInChannel(privateChannel, serverChannelMessage.getMessageId());
} else {
messageFuture = channelService.retrieveMessageInChannel(modMailThread, serverChannelMessage.getMessageId());
}
CompletableFuture.allOf(messageFuture, memberFuture).whenComplete((aVoid, throwable) -> {
LoadedModmailThreadMessage next = iterator.next();
if(messageFuture.isCompletedExceptionally()) {
log.warn("Message {} from user {} in server {} failed to load.", serverChannelMessage.getMessageId(), serverChannelMessage.getUserId(), serverChannelMessage.getServerId());
messageFuture.exceptionally(throwable1 -> {
log.warn("Failed with:", throwable1);
return null;
});
next.getMessageFuture().complete(null);
} else {
next.getMessageFuture().complete(messageFuture.join());
}
if(memberFuture.isCompletedExceptionally()) {
next.getMemberFuture().complete(null);
} else {
next.getMemberFuture().complete(memberFuture.join());
}
Optional<ServerChannelMessageUser> latestThreadMessageOptional = messageIds
.stream()
.filter(serverChannelMessageUser -> serverChannelMessageUser.getChannelId() != null)
.max(Comparator.comparing(ServerChannelMessageUser::getMessageId));
Optional<ServerChannelMessageUser> latestPrivateMessageOptional = messageIds
.stream()
.filter(serverChannelMessageUser -> serverChannelMessageUser.getChannelId() == null)
.max(Comparator.comparing(ServerChannelMessageUser::getMessageId));
CompletableFuture<MessageHistory> threadHistoryFuture;
if(latestThreadMessageOptional.isPresent()) {
ServerChannelMessageUser latestPrivateMessage = latestThreadMessageOptional.get();
threadHistoryFuture = modMailThread.getHistoryAround(latestPrivateMessage.getMessageId(), HISTORY_RETRIEVAL_LIMIT).submit();
} else {
threadHistoryFuture = CompletableFuture.completedFuture(null);
}
CompletableFuture<MessageHistory> privateHistoryFuture;
if(latestPrivateMessageOptional.isPresent()) {
ServerChannelMessageUser latestThreadMessage = latestPrivateMessageOptional.get();
privateHistoryFuture = privateChannel.getHistoryAround(latestThreadMessage.getMessageId(), HISTORY_RETRIEVAL_LIMIT).submit();
} else {
privateHistoryFuture = CompletableFuture.completedFuture(null);
}
List<Message> loadedMessages = new ArrayList<>();
CompletableFuture.allOf(threadHistoryFuture, privateHistoryFuture)
.thenCompose(unused -> loadMoreMessages(messageIdsToLoad, privateHistoryFuture.join(), modMailThread, threadHistoryFuture.join(), privateChannel, loadedMessages, 0))
.thenAccept(unused -> {
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() {
return LoadedModmailThreadMessage.builder().memberFuture(new CompletableFuture<>()).messageFuture(new CompletableFuture<>()).build();
public CompletableFuture<Void> loadMoreMessages(List<Long> messagesToLoad,
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.ModMailThreadChannelNotFound;
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.dto.LoadedModmailThreadMessageList;
import dev.sheldan.abstracto.modmail.model.dto.ServerChoice;
import dev.sheldan.abstracto.modmail.model.template.*;
import dev.sheldan.abstracto.modmail.service.management.ModMailMessageManagementService;
@@ -47,6 +47,8 @@ import javax.annotation.PostConstruct;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
@Slf4j
@@ -63,8 +65,9 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
/**
* 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_THREAD_CREATED_TEMPLATE_KEY = "modmail_thread_created";
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;
@@ -132,6 +135,9 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private UserService userService;
@Autowired
private ModMailThreadServiceBean self;
@@ -186,10 +192,20 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
CompletableFuture<TextChannel> textChannelFuture = channelService.createTextChannel(user.getName() + user.getDiscriminator(), server, categoryId);
return textChannelFuture.thenCompose(channel -> {
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
* 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
*/
@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);
CompletableFuture<Void> headerFuture = sendModMailHeader(channel, member);
CompletableFuture<Message> userReplyMessage;
@@ -221,6 +237,10 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
return CompletableFuture.allOf(headerFuture, notificationFuture, userReplyMessage).thenAccept(aVoid -> {
undoActions.clear();
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
public void createModMailPrompt(AUser user, Message initialMessage) {
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()) {
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<>();
HashMap<String, Long> choices = new HashMap<>();
for (int i = 0; i < knownServers.size(); i++) {
@@ -309,7 +336,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
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(availableGuilds.size() > 1) {
ModMailServerChooserModel modMailServerChooserModel = ModMailServerChooserModel
@@ -323,13 +350,21 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
.setDescription(text)
.setAction(reactionEmote -> {
Long chosenServerId = choices.get(reactionEmote.getEmoji());
log.debug("Executing action for creationg a modmail thread in server {} for user {}.", chosenServerId, initialMessage.getAuthor().getIdLong());
memberService.getMemberInServerAsync(chosenServerId, initialMessage.getAuthor().getIdLong()).thenCompose(member ->
self.createModMailThreadForUser(member, initialMessage, initialMessage.getChannel(), true, new ArrayList<>()).exceptionally(throwable -> {
log.error("Failed to setup thread correctly", throwable);
return null;
})
);
Long userId = initialMessage.getAuthor().getIdLong();
log.debug("Executing action for creationg a modmail thread in server {} for user {}.", chosenServerId, userId);
memberService.getMemberInServerAsync(chosenServerId, userId).thenCompose(member -> {
try {
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();
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
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);
memberService.getMemberInServerAsync(chosenServerId, initialMessage.getAuthor().getIdLong()).thenCompose(member ->
self.createModMailThreadForUser(member, initialMessage, initialMessage.getChannel(), true, new ArrayList<>()).exceptionally(throwable -> {
log.error("Failed to setup thread correctly", throwable);
return null;
})
);
memberService.getMemberInServerAsync(chosenServerId, initialMessage.getAuthor().getIdLong()).thenCompose(member -> {
try {
return self.createModMailThreadForUser(member, initialMessage, initialMessage.getChannel(), true, new ArrayList<>());
} 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 {
log.info("No server available to open a modmail thread in.");
// in case there is no server available, send an error message
channelService.sendEmbedTemplateInMessageChannelList("modmail_no_server_available", new Object(), initialMessage.getChannel());
}
} 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();
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());
return memberService.getMemberInServerAsync(modMailThread.getServer().getId(), messageFromUser.getAuthor().getIdLong()).thenCompose(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());
}
});
return memberService.getMemberInServerAsync(modMailThread.getServer().getId(), messageFromUser.getAuthor().getIdLong()).thenCompose(member ->
self.relayMessage(messageFromUser, serverId, channelId, modmailThreadId, member)
);
}
@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
* 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()) {
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 {
subscriberMemberFutures.add(CompletableFuture.completedFuture(null));
}
return FutureUtils.toSingleFutureGeneric(subscriberMemberFutures).thenCompose(firstVoid -> {
List<FullUserInServer> subscribers = new ArrayList<>();
CompletableFuture<Message> messageFuture = new CompletableFuture<>();
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
.builder()
.postedMessage(messageFromUser)
@@ -436,19 +491,24 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_user_message", modMailUserReplyModel, textChannel.getGuild().getIdLong());
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 -> {
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);
})
.thenApply(aVoid -> {
Message createdMessage = completableFutures.get(0).join();
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
@Transactional
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());
AUserInAServer moderator = userInServerManagementService.loadOrCreateUser(replyCommandMessage.getMember());
@@ -514,32 +575,35 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
@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 shouldLogThread = log && loggingMode;
return closeModMailThread(modMailThread, note, notifyUser, shouldLogThread, undoActions);
closingConfig.setLog(closingConfig.getLog() && loggingMode);
return closeModMailThread(modMailThread, closingConfig, undoActions);
}
@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);
Long modMailThreadId = modMailThread.getId();
log.info("Starting closing procedure for thread {}", modMailThread.getId());
List<ModMailMessage> modMailMessages = modMailThread.getMessages();
Long userId = modMailThread.getUser().getUserReference().getId();
Long serverId = modMailThread.getServer().getId();
if(logThread) {
LoadedModmailThreadMessageList messages = modMailMessageService.loadModMailMessages(modMailMessages);
CompletableFuture<Void> messagesFuture = FutureUtils.toSingleFuture(messages.getAllFutures());
return messagesFuture.handle((aVoid, throwable) ->
self.logMessagesToModMailLog(note, notifyUser, modMailThreadId, undoActions, messages, serverId, userId)
).toCompletableFuture().thenCompose(o -> o);
if(closingConfig.getLog()) {
if(!modMailMessages.isEmpty()) {
return modMailMessageService.loadModMailMessages(modMailMessages)
.thenAccept(loadedModmailThreadMessages -> self.logMessagesToModMailLog(closingConfig, modMailThreadId, undoActions, loadedModmailThreadMessages, serverId, userId));
} else {
log.info("Modmail thread {} in server {} has no messages. Only logging header.", modMailThreadId, serverId);
return loadUserAndSendClosingHeader(modMailThread, closingConfig)
.thenAccept(unused -> memberService.getMemberInServerAsync(modMailThread.getUser()).thenCompose(member ->
self.afterSuccessfulLog(modMailThreadId, closingConfig.getNotifyUser(), member, undoActions)
));
}
} else {
log.debug("Not logging modmail thread {}.", modMailThreadId);
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
* 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 undoActions The list of {@link UndoActionInstance} to execute in case of exceptions
* @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
*/
@Transactional
public CompletableFuture<Void> logMessagesToModMailLog(String note, Boolean notifyUser, Long modMailThreadId, List<UndoActionInstance> undoActions, LoadedModmailThreadMessageList messages, Long serverId, Long userId) {
log.debug("Logging {} modmail messages for modmail thread {}.", messages.getMessageList().size(), modMailThreadId);
public CompletableFuture<Void> logMessagesToModMailLog(ClosingContext closingContext, Long modMailThreadId, List<UndoActionInstance> undoActions,
ModmailLoggingThreadMessages messages, Long serverId, Long userId) {
log.debug("Logging {} modmail messages for modmail thread {}.", messages.getMessages().size(), modMailThreadId);
try {
CompletableFutureList<Message> list = self.logModMailThread(modMailThreadId, messages, note, undoActions);
return list.getMainFuture().thenCompose(avoid -> {
list.getFutures().forEach(messageCompletableFuture -> {
Message message = messageCompletableFuture.join();
undoActions.add(UndoActionInstance.getMessageDeleteAction(message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong()));
});
return memberService.getMemberInServerAsync(serverId, userId).thenCompose(member ->
self.afterSuccessfulLog(modMailThreadId, notifyUser, member, undoActions)
).exceptionally(throwable -> {
log.warn("Failed to retrieve member for closing the modmail thread. Closing without member information.", throwable);
self.afterSuccessfulLog(modMailThreadId, false, null, undoActions);
return null;
});
});
return self.logModMailThread(modMailThreadId, messages, closingContext, undoActions, serverId)
.thenCompose(list -> list.getMainFuture().thenCompose(unused -> {
list.getFutures().forEach(messageCompletableFuture -> {
Message message = messageCompletableFuture.join();
undoActions.add(UndoActionInstance.getMessageDeleteAction(message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong()));
});
return memberService.getMemberInServerAsync(serverId, userId).thenCompose(member ->
self.afterSuccessfulLog(modMailThreadId, closingContext.getNotifyUser(), member, undoActions)
).exceptionally(throwable -> {
log.warn("Failed to retrieve member for closing the modmail thread. Closing without member information.", throwable);
self.afterSuccessfulLog(modMailThreadId, false, null, undoActions);
return null;
});
}));
} catch (Exception e) {
log.error("Failed to log mod mail messages", e);
throw new AbstractoRunTimeException(e);
@@ -663,74 +726,99 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* log concerning general information about the closed {@link ModMailThread}
* @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 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 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
* @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
* the result values of the individual futures after they are done.
*/
@Transactional
public CompletableFutureList<Message> logModMailThread(Long modMailThreadId, LoadedModmailThreadMessageList messages, String note, List<UndoActionInstance> undoActions) {
log.info("Logging mod mail thread {} with {} messages.", modMailThreadId, messages.getMessageList().size());
public CompletableFuture<CompletableFutureList<Message>> logModMailThread(Long modMailThreadId, ModmailLoggingThreadMessages messages,
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);
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
List<ModMailLoggedMessageModel> loggedMessages = new ArrayList<>();
messages.getMessageList().forEach(futures -> {
try {
CompletableFuture<Message> future = futures.getMessageFuture();
if(!future.isCompletedExceptionally()) {
Message loadedMessage = future.join();
if(loadedMessage != null) {
log.info("Logging message {} in modmail thread {}.", loadedMessage.getId(), modMailThreadId);
ModMailMessage modmailMessage = modMailThread.getMessages()
.stream()
.filter(modMailMessage -> {
if(modMailMessage.getDmChannel()) {
return modMailMessage.getCreatedMessageInDM().equals(loadedMessage.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."));
Member author = futures.getMemberFuture().join();
ModMailLoggedMessageModel modMailLoggedMessageModel =
ModMailLoggedMessageModel
.builder()
.message(loadedMessage)
.modMailMessage(modmailMessage)
.author(author) // doesnt work for the messages from the DM channel
.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);
}
Map<Long, User> authors = messages
.getAuthors()
.stream().collect(Collectors.toMap(ISnowflake::getIdLong, Function.identity()));
messages.getMessages().forEach(message -> {
log.info("Logging message {} in modmail thread {}.", message.getId(), modMailThreadId);
ModMailMessage modmailMessage = modMailThread.getMessages()
.stream()
.filter(modMailMessage -> {
if(modMailMessage.getDmChannel()) {
return modMailMessage.getCreatedMessageInDM().equals(message.getIdLong());
} else {
return modMailMessage.getCreatedMessageInChannel().equals(message.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());
ModMailLoggedMessageModel modMailLoggedMessageModel =
ModMailLoggedMessageModel
.builder()
.message(message)
.author(author)
.modMailMessage(modmailMessage)
.build();
loggedMessages.add(modMailLoggedMessageModel);
});
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);
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_close_header", headerModel, modMailThread.getServer().getId());
List<CompletableFuture<Message>> closeHeaderFutures = postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, modMailThread.getServer().getId());
CompletableFuture<Message> headerFuture = loadUserAndSendClosingHeader(modMailThread, context);
// TODO in case the rendering fails, the already sent messages are not deleted
completableFutures.addAll(closeHeaderFutures);
completableFutures.addAll(self.sendMessagesToPostTarget(modMailThread, loggedMessages));
completableFutures.add(headerFuture);
completableFutures.addAll(self.sendMessagesToPostTarget(modMailThread, loggedMessages, updateMessage));
return new CompletableFutureList<>(completableFutures);
} else {
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.
* @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
* @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<>();
// TODO order messages
loadedMessages.forEach(message -> {
log.debug("Sending message {} of modmail thread {} to modmail log post target.", modMailThread.getId(), message.getMessage().getId());
ClosingProgressModel progressModel = ClosingProgressModel
.builder()
.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());
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);
});
}
return messageFutures;
}

View File

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

View File

@@ -28,7 +28,7 @@ public class ModMailCategoryDelayedAction implements DelayedAction {
public void execute(DelayedActionConfig delayedActionConfig) {
ModMailCategoryDelayedActionConfig concrete = (ModMailCategoryDelayedActionConfig) delayedActionConfig;
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="feature_id" valueComputed="${modmailFeature}"/>
</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">
<column name="name" value="closeSilently"/>
<column name="module_id" valueComputed="${modmailModule}"/>

View File

@@ -1,6 +1,9 @@
abstracto.systemConfigs.modMailClosingText.name=modMailClosingText
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.enabled=false
@@ -12,5 +15,5 @@ abstracto.featureModes.log.mode=log
abstracto.featureModes.log.enabled=true
abstracto.featureModes.threadMessage.featureName=modmail
abstracto.featureModes.threadMessage.mode=filterNotifications
abstracto.featureModes.threadMessage.mode=threadMessage
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 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
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.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
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
@@ -22,13 +25,20 @@ public class ModMailClosingHeaderModel {
/**
* 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}
* @return The duration between the creation date and the date the thread has been closed
*/
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;
import dev.sheldan.abstracto.core.models.FullUserInServer;
import dev.sheldan.abstracto.modmail.model.database.ModMailMessage;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
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
@@ -25,9 +24,9 @@ public class ModMailLoggedMessageModel {
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;
import dev.sheldan.abstracto.core.models.FullUserInServer;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@@ -25,8 +24,8 @@ public class ModMailUserReplyModel {
*/
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
*/
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;
import dev.sheldan.abstracto.modmail.model.database.ModMailMessage;
import dev.sheldan.abstracto.modmail.model.dto.LoadedModmailThreadMessageList;
import net.dv8tion.jda.api.entities.Message;
import dev.sheldan.abstracto.modmail.model.template.ModmailLoggingThreadMessages;
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 {
/**
* 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);
CompletableFuture<ModmailLoggingThreadMessages> 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.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.modmail.model.ClosingContext;
import dev.sheldan.abstracto.modmail.model.database.ModMailThread;
import net.dv8tion.jda.api.entities.Guild;
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
* {@link ModMailThread} to CLOSED and notifies the user about closing.
* @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.
* @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,
@@ -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
* CLOSED and notifies the user about closing.
* @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
* 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 closingConfig The {@link ClosingContext config} how the thread shoudl be closed
* @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
*/
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(Long channelId);

View File

@@ -281,7 +281,7 @@ public class ChannelServiceBean implements ChannelService {
@Override
public CompletableFuture<Message> editMessageInAChannelFuture(MessageToSend messageToSend, MessageChannel channel, Long messageId) {
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);
messageAction = channel.editMessageById(messageId, messageToSend.getMessages().get(0));
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());
}
@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
public MessageAction editMessage(Message message, MessageEmbed messageEmbed) {
metricService.incrementCounter(MESSAGE_EDIT_METRIC);

View File

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

View File

@@ -74,8 +74,4 @@ Closing the mod mail thread without notifying the user::
* 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)
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.