[AB-57] [AB-61] reworked commands and services to work with completable futures and moved the database operations to the very last operation so we have transaction safety in more areas

added some cache annotations to the default repository functions
reworked how the undo cations are processed within commands, they are executed in a post command listener when the state is error
added a counter id to generate ids to be unique within servers, changed a few tables to be unique within a server
added future utils class for wrapping a list of futures into one
moved abstracto tables to separate schema in the installer
refactored experience gain to work with more futures and delayed database access
This commit is contained in:
Sheldan
2020-09-20 11:11:20 +02:00
parent 552ecc26b8
commit 76adda90a3
220 changed files with 2691 additions and 1498 deletions

View File

@@ -18,6 +18,7 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Sends the reply from the staff member to the user, but marks the reply as anonymous. The original author is still
@@ -36,13 +37,13 @@ public class AnonReply extends AbstractConditionableCommand {
private ModMailThreadManagementService modMailThreadManagementService;
@Override
public CommandResult execute(CommandContext commandContext) {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
// text is optional, for example if only an attachment is sent
String text = parameters.size() == 1 ? (String) parameters.get(0) : "";
ModMailThread thread = modMailThreadManagementService.getByChannel(commandContext.getUserInitiatedContext().getChannel());
modMailThreadService.relayMessageToDm(thread, text, commandContext.getMessage(), true, commandContext.getChannel());
return CommandResult.fromSuccess();
return modMailThreadService.relayMessageToDm(thread, text, commandContext.getMessage(), true, commandContext.getChannel(), commandContext.getUndoActions())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
@@ -52,6 +53,7 @@ public class AnonReply extends AbstractConditionableCommand {
HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
return CommandConfiguration.builder()
.name("anonReply")
.async(true)
.module(ModMailModuleInterface.MODMAIL)
.parameters(parameters)
.supportsEmbedException(true)

View File

@@ -16,10 +16,10 @@ import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementS
import dev.sheldan.abstracto.templating.service.TemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Closes the mod mail thread: logs the messages to the log post target, if the feature has the appropriate
@@ -43,14 +43,13 @@ public class Close extends AbstractConditionableCommand {
@Override
@Transactional
public CommandResult execute(CommandContext commandContext) {
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());
ModMailThread thread = modMailThreadManagementService.getByChannel(commandContext.getUserInitiatedContext().getChannel());
modMailThreadService.closeModMailThread(thread, commandContext.getChannel(), note, true);
return CommandResult.fromSuccess();
return modMailThreadService.closeModMailThread(thread, note, true, commandContext.getUndoActions())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
@@ -63,9 +62,10 @@ public class Close extends AbstractConditionableCommand {
.module(ModMailModuleInterface.MODMAIL)
.parameters(parameters)
.help(helpInfo)
.async(true)
.supportsEmbedException(true)
.templated(true)
.causesReaction(true)
.causesReaction(false)
.build();
}

View File

@@ -20,6 +20,7 @@ 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}.
@@ -42,11 +43,11 @@ public class CloseNoLog extends AbstractConditionableCommand {
private TemplateService templateService;
@Override
public CommandResult execute(CommandContext commandContext) {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
ModMailThread thread = modMailThreadManagementService.getByChannel(commandContext.getUserInitiatedContext().getChannel());
// we don't have a note, therefore we cant pass any, the method handles this accordingly
modMailThreadService.closeModMailThread(thread, commandContext.getChannel(), null, false, false);
return CommandResult.fromSuccess();
return modMailThreadService.closeModMailThread(thread, null, false, commandContext.getUndoActions())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
@@ -55,6 +56,7 @@ public class CloseNoLog extends AbstractConditionableCommand {
return CommandConfiguration.builder()
.name("closeNoLog")
.module(ModMailModuleInterface.MODMAIL)
.async(true)
.help(helpInfo)
.supportsEmbedException(true)
.templated(true)

View File

@@ -19,6 +19,7 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* This command can be used to close the mod mail thread without sending a 'closing' message to the user.
@@ -39,13 +40,13 @@ public class CloseSilently extends AbstractConditionableCommand {
@Autowired
private TemplateService templateService;
@Override
public CommandResult execute(CommandContext commandContext) {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
// default note text is configurable via template, because the note is optional
String note = parameters.size() == 1 ? (String) parameters.get(0) : templateService.renderTemplate("modmail_close_default_note", new Object());
ModMailThread thread = modMailThreadManagementService.getByChannel(commandContext.getUserInitiatedContext().getChannel());
modMailThreadService.closeModMailThread(thread, commandContext.getChannel(), note, false);
return CommandResult.fromSuccess();
return modMailThreadService.closeModMailThread(thread, note, false, commandContext.getUndoActions())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
@@ -55,6 +56,7 @@ public class CloseSilently extends AbstractConditionableCommand {
HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
return CommandConfiguration.builder()
.name("closeSilently")
.async(true)
.module(ModMailModuleInterface.MODMAIL)
.parameters(parameters)
.help(helpInfo)

View File

@@ -12,17 +12,20 @@ import dev.sheldan.abstracto.core.models.FullUserInServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.models.template.ModMailThreadExistsModel;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
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 is used to create a thread with a member directly. If a thread already exists, this will post a link to
@@ -44,7 +47,7 @@ public class Contact extends AbstractConditionableCommand {
private ChannelService channelService;
@Override
public CommandResult execute(CommandContext commandContext) {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Member targetUser = (Member) commandContext.getParameters().getParameters().get(0);
AUserInAServer user = userManagementService.loadUser(targetUser);
// if this AUserInAServer already has an open thread, we should instead post a message
@@ -53,16 +56,17 @@ public class Contact extends AbstractConditionableCommand {
ModMailThreadExistsModel model = (ModMailThreadExistsModel) ContextConverter.fromCommandContext(commandContext, ModMailThreadExistsModel.class);
ModMailThread existingThread = modMailThreadManagementService.getOpenModMailThreadForUser(user);
model.setExistingModMailThread(existingThread);
channelService.sendEmbedTemplateInChannel("modmail_thread_already_exists", model, commandContext.getChannel());
List<CompletableFuture<Message>> futures = channelService.sendEmbedTemplateInChannel("modmail_thread_already_exists", model, commandContext.getChannel());
return FutureUtils.toSingleFutureGeneric(futures).thenApply(aVoid -> CommandResult.fromSuccess());
} else {
FullUserInServer fullUser = FullUserInServer
.builder()
.aUserInAServer(user)
.member(targetUser)
.build();
modMailThreadService.createModMailThreadForUser(fullUser, null, commandContext.getChannel(), false);
return modMailThreadService.createModMailThreadForUser(fullUser, null, commandContext.getChannel(), false, commandContext.getUndoActions())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
return CommandResult.fromSuccess();
}
@Override
@@ -74,6 +78,7 @@ public class Contact extends AbstractConditionableCommand {
.name("contact")
.module(ModMailModuleInterface.MODMAIL)
.parameters(parameters)
.async(true)
.help(helpInfo)
.supportsEmbedException(true)
.templated(true)

View File

@@ -18,6 +18,7 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Sends the reply from the staff member to the user, and shows the actual author in the created embed.
@@ -35,12 +36,12 @@ public class Reply extends AbstractConditionableCommand {
private ModMailThreadManagementService modMailThreadManagementService;
@Override
public CommandResult execute(CommandContext commandContext) {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
String text = parameters.size() == 1 ? (String) parameters.get(0) : "";
ModMailThread thread = modMailThreadManagementService.getByChannel(commandContext.getUserInitiatedContext().getChannel());
modMailThreadService.relayMessageToDm(thread, text, commandContext.getMessage(), false, commandContext.getChannel());
return CommandResult.fromSuccess();
return modMailThreadService.relayMessageToDm(thread, text, commandContext.getMessage(), false, commandContext.getChannel(), commandContext.getUndoActions())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
@@ -50,6 +51,7 @@ public class Reply extends AbstractConditionableCommand {
HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
return CommandConfiguration.builder()
.name("reply")
.async(true)
.module(ModMailModuleInterface.MODMAIL)
.parameters(parameters)
.help(helpInfo)

View File

@@ -30,6 +30,6 @@ public class RequiresModMailCondition implements ModMailContextCondition {
if(thread != null) {
return ConditionResult.builder().result(true).build();
}
throw new NotInModMailThreadException();
return ConditionResult.builder().result(false).exception(new NotInModMailThreadException()).build();
}
}

View File

@@ -15,6 +15,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
/**
* This listener is the core mechanic behind mod mail, if the bot receives a message via DM, this listener is executed
* and checks if the message should be forwarded to an existing mod mail thread, or if a new thread should be created/the
@@ -44,7 +46,7 @@ public class ModMailMessageListener implements PrivateMessageReceivedListener {
// there is only one open mod mail thread for a user at a time, so we can select the first one
// we cannot use the AUserInAServer directly, because a message in a private channel does not have a Member
ModMailThread existingThread = modMailThreadManagementService.getOpenModMailThreadsForUser(user).get(0);
modMailThreadService.relayMessageToModMailThread(existingThread, message);
modMailThreadService.relayMessageToModMailThread(existingThread, message, new ArrayList<>());
} else {
modMailThreadService.createModMailPrompt(user, message);
}

View File

@@ -3,8 +3,10 @@ package dev.sheldan.abstracto.modmail.repository;
import dev.sheldan.abstracto.modmail.models.database.ModMailMessage;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.stereotype.Repository;
import javax.persistence.QueryHint;
import java.util.List;
/**
@@ -12,5 +14,6 @@ import java.util.List;
*/
@Repository
public interface ModMailMessageRepository extends JpaRepository<ModMailMessage, Long> {
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
List<ModMailMessage> findByThreadReference(ModMailThread modMailThread);
}

View File

@@ -4,8 +4,10 @@ import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.modmail.models.database.ModMailRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.stereotype.Repository;
import javax.persistence.QueryHint;
import java.util.List;
/**
@@ -13,8 +15,12 @@ import java.util.List;
*/
@Repository
public interface ModMailRoleRepository extends JpaRepository<ModMailRole, Long> {
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
boolean existsByServerAndRole(AServer server, ARole role);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
void deleteByServerAndRole(AServer server, ARole role);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
List<ModMailRole> findByServer(AServer server);
}

View File

@@ -4,8 +4,10 @@ import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.models.database.ModMailThreadSubscriber;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.stereotype.Repository;
import javax.persistence.QueryHint;
import java.util.List;
/**
@@ -13,7 +15,12 @@ import java.util.List;
*/
@Repository
public interface ModMailSubscriberRepository extends JpaRepository<ModMailThreadSubscriber, Long> {
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
List<ModMailThreadSubscriber> findByThreadReference(ModMailThread thread);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
boolean existsBySubscriberAndThreadReference(AUserInAServer aUserInAServer, ModMailThread modMailThread);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
void deleteBySubscriberAndThreadReference(AUserInAServer aUserInAServer, ModMailThread modMailThread);
}

View File

@@ -6,23 +6,51 @@ import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.models.database.ModMailThreadState;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Repository;
import javax.persistence.QueryHint;
import java.util.List;
import java.util.Optional;
/**
* Repository to manage the stored {@link ModMailThread} instances
*/
@Repository
public interface ModMailThreadRepository extends JpaRepository<ModMailThread, Long> {
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
ModMailThread findByChannel(AChannel channel);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
List<ModMailThread> findByUser(AUserInAServer aUserInAServer);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
ModMailThread findTopByUserOrderByClosedDesc(AUserInAServer aUserInAServer);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
List<ModMailThread> findByUser_UserReferenceAndStateNot(AUser user, ModMailThreadState state);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
boolean existsByUser_UserReferenceAndStateNot(AUser user, ModMailThreadState state);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
List<ModMailThread> findByServerAndState(AServer server, ModMailThreadState state);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
ModMailThread findByUserAndStateNot(AUserInAServer userInAServer, ModMailThreadState state);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
boolean existsByUserAndStateNot(AUserInAServer userInAServer, ModMailThreadState state);
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
List<ModMailThread> findByUserAndState(AUserInAServer userInAServer, ModMailThreadState state);
@NotNull
@Override
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
Optional<ModMailThread> findById(@NonNull Long aLong);
}

View File

@@ -2,9 +2,8 @@ package dev.sheldan.abstracto.modmail.service;
import com.jagrosh.jdautilities.commons.waiter.EventWaiter;
import com.jagrosh.jdautilities.menu.ButtonMenu;
import dev.sheldan.abstracto.core.command.exception.AbstractoTemplatedException;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.exception.PostTargetNotFoundException;
import dev.sheldan.abstracto.core.exception.UserInServerNotFoundException;
import dev.sheldan.abstracto.core.models.FeatureValidationResult;
import dev.sheldan.abstracto.core.models.FullGuild;
import dev.sheldan.abstracto.core.models.FullUserInServer;
@@ -15,6 +14,7 @@ import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.modmail.config.*;
import dev.sheldan.abstracto.modmail.exception.ModMailCategoryIdException;
import dev.sheldan.abstracto.modmail.exception.ModMailThreadNotFoundException;
@@ -130,26 +130,20 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Override
public void createModMailThreadForUser(FullUserInServer aUserInAServer, Message initialMessage, MessageChannel feedBackChannel, boolean userInitiated) {
public CompletableFuture<Void> createModMailThreadForUser(FullUserInServer aUserInAServer, Message initialMessage, MessageChannel feedBackChannel, boolean userInitiated, List<UndoActionInstance> undoActions) {
Long serverId = aUserInAServer.getAUserInAServer().getServerReference().getId();
Long categoryId = configService.getLongValue(MODMAIL_CATEGORY, serverId);
User user = aUserInAServer.getMember().getUser();
CompletableFuture<TextChannel> textChannelFuture = channelService.createTextChannel(user.getName() + user.getDiscriminator(), aUserInAServer.getAUserInAServer().getServerReference(), categoryId);
Long userInServerId = aUserInAServer.getAUserInAServer().getUserInServerId();
textChannelFuture.thenAccept(channel -> {
List<UndoActionInstance> undoActions = new ArrayList<>();
return textChannelFuture.thenCompose(channel -> {
undoActions.add(UndoActionInstance.getChannelDeleteAction(serverId, channel.getIdLong()));
self.performModMailThreadSetup(aUserInAServer, initialMessage, channel, userInitiated, undoActions);
}).exceptionally(throwable -> {
log.error("Failed to create mod mail thread", throwable);
sendModMailFailure("modmail_exception_failed_to_create_mod_mail_thread", userInServerId, null, feedBackChannel, throwable);
return null;
return self.performModMailThreadSetup(aUserInAServer, initialMessage, channel, userInitiated, undoActions);
});
}
/**
* 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.
* @param aUserInAServer The {@link FullUserInServer} for which a {@link ModMailThread} is being created
* @param initialMessage The {@link Message} which was sent by the user to open a thread, this is null, if the thread was opened via a command
@@ -158,62 +152,63 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* @param undoActions The list of actions to undo, in case an exception occurs
*/
@Transactional
public void performModMailThreadSetup(FullUserInServer aUserInAServer, Message initialMessage, TextChannel channel, boolean userInitiated, List<UndoActionInstance> undoActions) {
try {
ModMailThread thread = createThreadObject(channel, aUserInAServer);
sendModMailHeader(channel, aUserInAServer, undoActions);
CompletableFuture<Void> future;
if(initialMessage != null){
future = self.sendUserReply(channel, thread, initialMessage);
} else {
future = CompletableFuture.completedFuture(null);
}
future.thenAccept(aVoid -> {
if(userInitiated) {
self.sendModMailNotification(aUserInAServer, thread, undoActions);
}
});
} catch (Exception e) {
log.error("Failed to perform mod mail thread setup.", e);
undoActionService.performActions(undoActions);
public CompletableFuture<Void> performModMailThreadSetup(FullUserInServer aUserInAServer, Message initialMessage, TextChannel channel, boolean userInitiated, List<UndoActionInstance> undoActions) {
Long userInServerId = aUserInAServer.getAUserInAServer().getUserInServerId();
CompletableFuture<Void> headerFuture = sendModMailHeader(channel, aUserInAServer);
CompletableFuture<Void> userReplyMessage;
if(initialMessage != null){
userReplyMessage = self.sendUserReply(channel, null, initialMessage, aUserInAServer.getAUserInAServer());
} else {
userReplyMessage = CompletableFuture.completedFuture(null);
}
CompletableFuture notificationFuture;
if (userInitiated) {
notificationFuture = self.sendModMailNotification(aUserInAServer);
} else {
notificationFuture = CompletableFuture.completedFuture(null);
}
return CompletableFuture.allOf(headerFuture, notificationFuture, userReplyMessage).thenAccept(aVoid -> {
undoActions.clear();
self.setupModMailThreadInDB(initialMessage, channel, userInServerId);
});
}
@Transactional
public void setupModMailThreadInDB(Message initialMessage, TextChannel channel, Long userInServerId) {
AUserInAServer aUserInAServer = userInServerManagementService.loadUser(userInServerId);
ModMailThread thread = createThreadObject(channel, aUserInAServer);
if(initialMessage != null) {
modMailMessageManagementService.addMessageToThread(thread, initialMessage, aUserInAServer, false, false);
}
}
/**
* Sends the message containing the pings to notify the staff members to handle the opened {@link ModMailThread}
* @param aUserInAServer The {@link FullUserInServer} which opened the thread
* @param thread The {@link ModMailThread} instance which was created
* @param undoActions The list of {@link UndoActionInstance} to perform, in case an exception occurs
*/
@Transactional
public void sendModMailNotification(FullUserInServer aUserInAServer, ModMailThread thread, List<UndoActionInstance> undoActions) {
List<ModMailRole> rolesToPing = modMailRoleManagementService.getRolesForServer(thread.getServer());
public CompletableFuture<Void> sendModMailNotification(FullUserInServer aUserInAServer) {
List<ModMailRole> rolesToPing = modMailRoleManagementService.getRolesForServer(aUserInAServer.getAUserInAServer().getServerReference());
ModMailNotificationModel modMailNotificationModel = ModMailNotificationModel
.builder()
.modMailThread(thread)
.threadUser(aUserInAServer)
.roles(rolesToPing)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_notification_message", modMailNotificationModel);
List<CompletableFuture<Message>> modmailping = postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_PING, thread.getServer().getId());
CompletableFuture.allOf(modmailping.toArray(new CompletableFuture[0])).whenComplete((aVoid, throwable) -> {
if(throwable != null) {
log.error("Failed to send mod mail thread notification ping.", throwable);
undoActionService.performActions(undoActions);
}
});
List<CompletableFuture<Message>> modmailping = postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_PING, aUserInAServer.getMember().getGuild().getIdLong());
return CompletableFuture.allOf(modmailping.toArray(new CompletableFuture[0]));
}
/**
* Creates the instance of the {@link ModMailThread} in the database.
* @param channel The {@link TextChannel} in which the {@link ModMailThread} is being done
* @param user The {@link FullUserInServer} which the thread is about
* @param user The {@link AUserInAServer} which the thread is about
* @return The created instance of {@link ModMailThread}
*/
public ModMailThread createThreadObject(TextChannel channel, FullUserInServer user) {
AChannel channel2 = channelManagementService.createChannel(channel.getIdLong(), AChannelType.TEXT, user.getAUserInAServer().getServerReference());
public ModMailThread createThreadObject(TextChannel channel, AUserInAServer user) {
AChannel channel2 = channelManagementService.createChannel(channel.getIdLong(), AChannelType.TEXT, user.getServerReference());
log.info("Creating mod mail thread in channel {} with db channel {}", channel.getIdLong(), channel2.getId());
return modMailThreadManagementService.createModMailThread(user.getAUserInAServer(), channel2);
return modMailThreadManagementService.createModMailThread(user, channel2);
}
@Override
@@ -266,7 +261,10 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
AUserInAServer chosenServer = choices.get(reactionEmote.getEmoji());
Member memberInServer = botService.getMemberInServer(chosenServer);
FullUserInServer fullUser = FullUserInServer.builder().member(memberInServer).aUserInAServer(chosenServer).build();
self.createModMailThreadForUser(fullUser, initialMessage, initialMessage.getChannel(), true);
self.createModMailThreadForUser(fullUser, initialMessage, initialMessage.getChannel(), true, new ArrayList<>()).exceptionally(throwable -> {
log.error("Failed to setup thread correctly", throwable);
return null;
});
})
.build();
menu.display(initialMessage.getChannel());
@@ -275,7 +273,10 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
AUserInAServer chosenServer = choices.get(availableGuilds.get(0).getReactionEmote());
Member memberInServer = botService.getMemberInServer(chosenServer);
FullUserInServer fullUser = FullUserInServer.builder().member(memberInServer).aUserInAServer(chosenServer).build();
self.createModMailThreadForUser(fullUser, initialMessage, initialMessage.getChannel(), true);
self.createModMailThreadForUser(fullUser, initialMessage, initialMessage.getChannel(), true, new ArrayList<>()).exceptionally(throwable -> {
log.error("Failed to setup thread correctly", throwable);
return null;
});
} else {
// in case there is no server available, send an error message
channelService.sendEmbedTemplateInChannel("modmail_no_server_available", new Object(), initialMessage.getChannel());
@@ -291,9 +292,8 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* the user which the thread is about
* @param channel The {@link TextChannel} in which the mod mail thread is present in
* @param aUserInAServer The {@link AUserInAServer} which the {@link ModMailThread} is about
* @param undoActions The list of {@link UndoActionInstance} to execute in case an exception occurs
*/
private void sendModMailHeader(TextChannel channel, FullUserInServer aUserInAServer, List<UndoActionInstance> undoActions) {
private CompletableFuture<Void> sendModMailHeader(TextChannel channel, FullUserInServer aUserInAServer) {
ModMailThread latestThread = modMailThreadManagementService.getLatestModMailThread(aUserInAServer.getAUserInAServer());
List<ModMailThread> oldThreads = modMailThreadManagementService.getModMailThreadForUser(aUserInAServer.getAUserInAServer());
ModMailThreaderHeader header = ModMailThreaderHeader
@@ -303,25 +303,20 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
.pastModMailThreadCount((long)oldThreads.size())
.build();
List<CompletableFuture<Message>> messages = channelService.sendEmbedTemplateInChannel("modmail_thread_header", header, channel);
CompletableFuture.allOf(messages.toArray(new CompletableFuture[0])).whenComplete((aVoid, throwable)-> {
if(throwable != null) {
log.error("Failed to send mod mail header for for user {}.", aUserInAServer.getAUserInAServer().getUserReference().getId(), throwable);
undoActionService.performActions(undoActions);
}
});
return CompletableFuture.allOf(messages.toArray(new CompletableFuture[0]));
}
@Override
public void relayMessageToModMailThread(ModMailThread modMailThread, Message message) {
public CompletableFuture<Void> relayMessageToModMailThread(ModMailThread modMailThread, Message message, List<UndoActionInstance> undoActions) {
Optional<TextChannel> textChannelFromServer = botService.getTextChannelFromServerOptional(modMailThread.getServer().getId(), modMailThread.getChannel().getId());
if(textChannelFromServer.isPresent()) {
TextChannel textChannel = textChannelFromServer.get();
self.sendUserReply(textChannel, modMailThread, message);
return self.sendUserReply(textChannel, modMailThread, message, modMailThread.getUser());
} else {
// 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(modMailThread.getId());
message.getChannel().sendMessage(templateService.renderTemplate("modmail_failed_to_forward_message", new Object())).queue();
return message.getChannel().sendMessage(templateService.renderTemplate("modmail_failed_to_forward_message", new Object())).submit().thenApply(message1 -> null);
}
}
@@ -330,28 +325,30 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* the appropriate {@link ModMailThread} channel, the returned promise only returns if the message was dealt with on the user
* side.
* @param textChannel The {@link TextChannel} in which the {@link ModMailThread} is being handled
* @param modMailThread The {@link ModMailThread} to which the received {@link Message} is a reply to
* @param modMailThread The {@link ModMailThread} to which the received {@link Message} is a reply to, can be null, if it is null, its the initial message
* @param message The received message from the user
* @return A {@link CompletableFuture} which resolves when the post processing of the message is completed (adding read notification, and storing messageIDs)
*/
public CompletableFuture<Void> sendUserReply(TextChannel textChannel, ModMailThread modMailThread, Message message) {
Long modMailThreadId = modMailThread.getId();
public CompletableFuture<Void> sendUserReply(TextChannel textChannel, ModMailThread modMailThread, Message message, AUserInAServer userInServer) {
boolean modMailThreadExists = modMailThread != null;
FullUserInServer fullUser = FullUserInServer
.builder()
.aUserInAServer(modMailThread.getUser())
.member(botService.getMemberInServer(modMailThread.getUser()))
.aUserInAServer(userInServer)
.member(botService.getMemberInServer(userInServer))
.build();
List<FullUserInServer> subscribers = new ArrayList<>();
List<ModMailThreadSubscriber> subscriberList = modMailSubscriberManagementService.getSubscribersForThread(modMailThread);
subscriberList.forEach(modMailThreadSubscriber -> {
FullUserInServer subscriber = FullUserInServer
.builder()
.aUserInAServer(modMailThreadSubscriber.getSubscriber())
.member(botService.getMemberInServer(modMailThreadSubscriber.getSubscriber()))
.build();
subscribers.add(subscriber);
});
if(modMailThreadExists) {
List<ModMailThreadSubscriber> subscriberList = modMailSubscriberManagementService.getSubscribersForThread(modMailThread);
subscriberList.forEach(modMailThreadSubscriber -> {
FullUserInServer subscriber = FullUserInServer
.builder()
.aUserInAServer(modMailThreadSubscriber.getSubscriber())
.member(botService.getMemberInServer(modMailThreadSubscriber.getSubscriber()))
.build();
subscribers.add(subscriber);
});
}
ModMailUserReplyModel modMailUserReplyModel = ModMailUserReplyModel
.builder()
.modMailThread(modMailThread)
@@ -361,178 +358,116 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_user_message", modMailUserReplyModel);
List<CompletableFuture<Message>> completableFutures = channelService.sendMessageToSendToChannel(messageToSend, textChannel);
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).thenAccept(aVoid ->
self.postProcessSendMessages(modMailThreadId, message, completableFutures)
);
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]))
.thenCompose(aVoid -> messageService.addReactionToMessageWithFuture("readReaction", textChannel.getGuild().getIdLong(), message))
.thenAccept(aVoid -> {
if(modMailThreadExists) {
self.postProcessSendMessages(textChannel, completableFutures.get(0).join());
}
});
}
/**
* This message handles the post processing of the messages received by the user. This includes: saving the messageIDs
* in the database, updating the state of the {@link ModMailThread} and adding the read reaction to the user message
* @param modMailThreadId The ID of the {@link ModMailThread} for which the message was directed to
* @param message The actual {@link Message} instance received from the user.
* @param completableFutures The list of {@link CompletableFuture} which were rendered from the sent message
* and posted in the {@link ModMailThread} by Abstracto
*/
@Transactional
public void postProcessSendMessages(Long modMailThreadId, Message message, List<CompletableFuture<Message>> completableFutures) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
public void postProcessSendMessages(TextChannel textChannel, Message message) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(textChannel.getIdLong());
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
List<Message> messages = new ArrayList<>();
completableFutures.forEach(messageCompletableFuture -> {
try {
Message messageToAdd = messageCompletableFuture.get();
messages.add(messageToAdd);
} catch (InterruptedException | ExecutionException e) {
log.error("Error while executing future to retrieve reaction.", e);
Thread.currentThread().interrupt();
}
self.saveMessageIds(messages, modMailThread, modMailThread.getUser(), false, false);
// update the state of the thread
modMailThreadManagementService.setModMailThreadState(modMailThread, ModMailThreadState.USER_REPLIED);
// add the reaction to show that the message has been processed
messageService.addReactionToMessage("readReaction", modMailThread.getServer().getId(), message);
});
modMailMessageManagementService.addMessageToThread(modMailThread, message, modMailThread.getUser(), false, false);
// update the state of the thread
modMailThreadManagementService.setModMailThreadState(modMailThread, ModMailThreadState.USER_REPLIED);
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);
throw new ModMailThreadNotFoundException(textChannel.getIdLong());
}
}
@Override
public void relayMessageToDm(ModMailThread modMailThread, String text, Message message, boolean anonymous, MessageChannel feedBack) {
public CompletableFuture<Void> relayMessageToDm(ModMailThread modMailThread, String text, Message message, boolean anonymous, MessageChannel feedBack, List<UndoActionInstance> undoActions) {
Long modMailThreadId = modMailThread.getId();
User userById = botService.getInstance().getUserById(modMailThread.getUser().getUserReference().getId());
if(userById != null) {
userById.openPrivateChannel().queue(privateChannel ->
self.sendReply(modMailThreadId, text, message, privateChannel, anonymous, feedBack)
,throwable ->
log.warn("Failed to open private channel with user {}", userById.getIdLong())
AUserInAServer moderator = userInServerManagementService.loadUser(message.getMember());
Member userInGuild = botService.getMemberInServer(modMailThread.getUser());
FullUserInServer fullThreadUser = FullUserInServer
.builder()
.aUserInAServer(modMailThread.getUser())
.member(userInGuild)
.build();
ModMailModeratorReplyModel.ModMailModeratorReplyModelBuilder modMailModeratorReplyModelBuilder = ModMailModeratorReplyModel
.builder()
.text(text)
.modMailThread(modMailThread)
.postedMessage(message)
.anonymous(anonymous)
.threadUser(fullThreadUser);
if(anonymous) {
modMailModeratorReplyModelBuilder.moderator(botService.getBotInGuild(modMailThread.getServer()));
} else {
Member moderatorMember = botService.getMemberInServer(moderator);
modMailModeratorReplyModelBuilder.moderator(moderatorMember);
}
ModMailModeratorReplyModel modMailUserReplyModel = modMailModeratorReplyModelBuilder.build();
CompletableFuture<Message> future = messageService.sendEmbedToUserWithMessage(userById, "modmail_staff_message", modMailUserReplyModel);
return future.thenAccept(sendMessage ->
self.saveSendMessagesAndUpdateState(modMailThreadId, anonymous, moderator, sendMessage)
);
}
}
/**
* Notifies the staff members in the thread about any exception occurring when executing a command in a {@link ModMailThread}.
* This takes a custom template which is rendered and a generic model is provided.
* @param template The key of the {@link dev.sheldan.abstracto.templating.model.database.Template} to use
* @param userInServerId The ID of the user in the server which the {@link ModMailThread} is about
* @param modMailTreadId The ID of the {@link ModMailThread} in which an exception occurred
* @param channel The {@link MessageChannel} in to which the exception message should be sent to
* @param throwable The instance of the {@link Throwable} which happened.
* @throws UserInServerNotFoundException in case the {@link AUserInAServer} was not found by the ID
* @throws ModMailThreadNotFoundException in case the {@link ModMailThread} was not found by the ID
*/
@Transactional
public void sendModMailFailure(String template, Long userInServerId, Long modMailTreadId, MessageChannel channel, Throwable throwable) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailTreadId);
AUserInAServer aUserInAServer = userInServerManagementService.loadUser(userInServerId).orElseThrow(() -> new UserInServerNotFoundException(userInServerId));
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
try {
FullUserInServer fullUser = FullUserInServer
.builder()
.aUserInAServer(aUserInAServer)
.member(botService.getMemberInServer(aUserInAServer))
.build();
ModMailExceptionModel modMailExceptionModel = ModMailExceptionModel
.builder()
.modMailThread(modMailThread)
.user(fullUser)
.throwable(throwable)
.build();
channelService.sendEmbedTemplateInChannel(template, modMailExceptionModel, channel);
} catch (Exception e) {
log.error("Failed to notify about mod mail exception.", e);
}
} else {
throw new ModMailThreadNotFoundException(modMailTreadId);
}
throw new AbstractoRunTimeException("User in server not found.");
}
@Override
public void closeModMailThread(ModMailThread modMailThread, MessageChannel feedBack, String note, boolean notifyUser) {
public CompletableFuture<Void> closeModMailThread(ModMailThread modMailThread, String note, boolean notifyUser, List<UndoActionInstance> undoActions) {
AFeatureMode aFeatureMode = featureModeService.getFeatureMode(ModMailFeatures.MOD_MAIL, modMailThread.getServer());
boolean loggingMode = aFeatureMode.getMode().equalsIgnoreCase(ModMailMode.LOGGING.getKey());
closeModMailThread(modMailThread, feedBack, note, notifyUser, loggingMode);
return closeModMailThread(modMailThread, note, notifyUser, loggingMode, undoActions);
}
@Override
public void closeModMailThread(ModMailThread modMailThread, MessageChannel feedBack, String note, boolean notifyUser, boolean logThread) {
public CompletableFuture<Void> closeModMailThread(ModMailThread modMailThread, String note, boolean notifyUser, boolean logThread, List<UndoActionInstance> undoActions) {
Long modMailThreadId = modMailThread.getId();
log.info("Starting closing procedure for thread {}", modMailThread.getId());
List<ModMailMessage> modMailMessages = modMailThread.getMessages();
List<UndoActionInstance> undoActions = new ArrayList<>();
if(logThread) {
List<CompletableFuture<Message>> messages = modMailMessageService.loadModMailMessages(modMailMessages);
log.trace("Loading {} mod mail thread messages.", messages.size());
for (int i = 0; i < messages.size(); i++) {
CompletableFuture<Message> messageCompletableFuture = messages.get(i);
Long messageId = modMailMessages.get(i).getMessageId();
messageCompletableFuture.exceptionally(throwable -> {
log.warn("Failed to load message {} in mod mail thread {}", messageId, modMailThreadId);
return null;
});
}
CompletableFuture.allOf(messages.toArray(new CompletableFuture[0])).whenComplete((avoid, throwable) -> {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
if(modMailThreadOpt.isPresent()) {
ModMailThread innerModMailThread = modMailThreadOpt.get();
log.trace("Loaded {} mod mail thread messages", messages.size());
if(throwable != null) {
log.warn("Failed to load some mod mail messages for mod mail thread {}. Still trying to post the ones we got.", modMailThreadId, throwable);
}
logMessagesToModMailLog(feedBack, note, notifyUser, modMailThreadId, undoActions, messages, innerModMailThread);
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);
}
});
CompletableFuture<Void> messagesFuture = FutureUtils.toSingleFutureGeneric(messages);
return messagesFuture.handle((aVoid, throwable) ->
self.logMessagesToModMailLog(note, notifyUser, modMailThreadId, undoActions, messages)
).toCompletableFuture().thenCompose(o -> o);
} else {
self.afterSuccessfulLog(modMailThreadId, feedBack, notifyUser, undoActions);
return self.afterSuccessfulLog(modMailThreadId, notifyUser, undoActions);
}
}
/**
* 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 feedBack The {@link MessageChannel} in which possible feedback about exceptions is sent to
* @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
* @param innerModMailThread An instance of {@link ModMailThread} which is getting closed
*/
private void logMessagesToModMailLog(MessageChannel feedBack, String note, Boolean notifyUser, Long modMailThreadId, List<UndoActionInstance> undoActions, List<CompletableFuture<Message>> messages, ModMailThread innerModMailThread) {
Long userInServerId = innerModMailThread.getUser().getUserInServerId();
@Transactional
public CompletableFuture<Void> logMessagesToModMailLog(String note, Boolean notifyUser, Long modMailThreadId, List<UndoActionInstance> undoActions, List<CompletableFuture<Message>> messages) {
try {
CompletableFutureList<Message> list = self.logModMailThread(modMailThreadId, messages, note);
list.getMainFuture().thenRun(() -> {
CompletableFutureList<Message> list = self.logModMailThread(modMailThreadId, messages, note, undoActions);
return list.getMainFuture().thenCompose(avoid -> {
list.getFutures().forEach(messageCompletableFuture -> {
try {
Message message = messageCompletableFuture.get();
undoActions.add(UndoActionInstance.getMessageDeleteAction(message.getChannel().getIdLong(), message.getIdLong()));
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to post logging messages.", e);
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("Failed to handle the mod mail log messages.", e);
}
Message message = messageCompletableFuture.join();
undoActions.add(UndoActionInstance.getMessageDeleteAction(messageCompletableFuture.join().getChannel().getIdLong(), message.getIdLong()));
});
self.afterSuccessfulLog(modMailThreadId, feedBack, notifyUser, undoActions);
return self.afterSuccessfulLog(modMailThreadId, notifyUser, undoActions);
});
list.getMainFuture().exceptionally(innerThrowable -> {
sendModMailFailure(MODMAIL_EXCEPTION_GENERIC_TEMPLATE, userInServerId, modMailThreadId, feedBack, innerThrowable);
log.error("Failed to log messages for mod mail thread {}.", modMailThreadId, innerThrowable);
return null;
});
} catch (PostTargetNotFoundException po) {
log.error("Failed to log mod mail messages", po);
sendModMailFailure("modmail_exception_post_target_not_defined", userInServerId, modMailThreadId, feedBack, po);
} catch (Exception e) {
log.error("Failed to log mod mail messages", e);
sendModMailFailure(MODMAIL_EXCEPTION_GENERIC_TEMPLATE, userInServerId, modMailThreadId, feedBack, e);
throw new AbstractoRunTimeException(e);
}
}
@@ -540,46 +475,27 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* This message is executed after the thread has been logged and notifies the user about the closed {@link ModMailThread}
* which a configurable closing text. This method then calls the method to delete the channel.
* @param modMailThreadId The ID of the {@link ModMailThread} which is being closed.
* @param feedBack The {@link MessageChannel} in which exceptions should be sent to
* @param notifyUser Whether or not the user should be notified
* @param undoActions The list of {@link UndoActionInstance} to execute in case of exceptions
* @throws ModMailThreadNotFoundException in case the {@link ModMailThread} is not found by the ID
*/
@Transactional
public void afterSuccessfulLog(Long modMailThreadId, MessageChannel feedBack, Boolean notifyUser, List<UndoActionInstance> undoActions) {
public CompletableFuture<Void> afterSuccessfulLog(Long modMailThreadId, Boolean notifyUser, List<UndoActionInstance> undoActions) {
log.trace("Mod mail logging for thread {} has completed. Starting post logging activities.", modMailThreadId);
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
User user = botService.getMemberInServer(modMailThread.getUser()).getUser();
user.openPrivateChannel().queue(privateChannel -> {
try {
List<CompletableFuture<Message>> messageFutures = new ArrayList<>();
if(notifyUser){
log.trace("Notifying user {}", user.getIdLong());
HashMap<String, String> closingMessage = new HashMap<>();
String defaultValue = templateService.renderSimpleTemplate("modmail_closing_user_message_description");
closingMessage.put("closingMessage", configService.getStringValue(MODMAIL_CLOSING_MESSAGE_TEXT, modMailThread.getServer().getId(), defaultValue));
messageFutures.addAll(channelService.sendEmbedTemplateInChannel("modmail_closing_user_message", closingMessage , privateChannel));
} else {
log.trace("*Not* notifying user {}", user.getIdLong());
messageFutures.add(CompletableFuture.completedFuture(null));
}
CompletableFuture.allOf(messageFutures.toArray(new CompletableFuture[0])).whenComplete((result, throwable) -> {
if(throwable != null) {
log.warn("Failed to send closing message to user {} after closing mod mail thread {}", user.getIdLong(), modMailThread.getId(), throwable);
}
self.deleteChannelAndClose(modMailThreadId, feedBack, undoActions);
});
} catch (Exception e) {
log.error("Failed to render closing user message", e);
Long userInServerId = modMailThread.getUser().getUserInServerId();
sendModMailFailure(MODMAIL_EXCEPTION_GENERIC_TEMPLATE, userInServerId, modMailThreadId, feedBack, e);
}
}, throwable -> {
log.error("Failed to load private channel with user {}", user.getIdLong(), throwable);
undoActionService.performActions(undoActions);
});
if(notifyUser) {
ModMailThread modMailThread = modMailThreadOpt.get();
User user = botService.getMemberInServer(modMailThread.getUser()).getUser();
HashMap<String, String> closingMessage = new HashMap<>();
String defaultValue = templateService.renderSimpleTemplate("modmail_closing_user_message_description");
closingMessage.put("closingMessage", configService.getStringValue(MODMAIL_CLOSING_MESSAGE_TEXT, modMailThread.getServer().getId(), defaultValue));
return messageService.sendEmbedToUser(user, "modmail_closing_user_message", closingMessage).thenAccept(message ->
self.deleteChannelAndClose(modMailThreadId, undoActions)
);
} else {
return deleteChannelAndClose(modMailThreadId, undoActions);
}
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);
}
@@ -589,40 +505,31 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* Deletes the actual {@link MessageChannel} in which the {@link ModMailThread} happened. This method then calls the
* method to update the stats in the database
* @param modMailThreadId The ID of the {@link ModMailThread} to delete the {@link MessageChannel} from
* @param feedBack The {@link MessageChannel} in which exceptions should be sent to
* @param undoActions The list of {@link UndoActionInstance} to execute in case of exceptions
* @throws ModMailThreadNotFoundException in case the {@link ModMailThread} is not found by the ID
*/
@Transactional
public void deleteChannelAndClose(Long modMailThreadId, MessageChannel feedBack, List<UndoActionInstance> undoActions) {
public CompletableFuture<Void> deleteChannelAndClose(Long modMailThreadId, List<UndoActionInstance> undoActions) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
String failureMessage = "Failed to delete text channel containing mod mail thread {}";
try {
channelService.deleteTextChannel(modMailThread.getChannel()).thenRun(() -> {
try {
self.closeModMailThreadInDb(modMailThreadId);
} catch (Exception e) {
undoActionService.performActions(undoActions);
}
}).exceptionally(throwable2 -> {
undoActionService.performActions(undoActions);
log.error(failureMessage, modMailThread.getId(), throwable2);
return null;
return channelService.deleteTextChannel(modMailThread.getChannel()).thenAccept(avoid -> {
undoActions.clear();
self.closeModMailThreadInDb(modMailThreadId);
});
} catch (InsufficientPermissionException ex) {
log.error(failureMessage, modMailThreadId, ex);
undoActionService.performActions(undoActions);
sendModMailFailure("modmail_exception_cannot_delete_channel", modMailThread.getUser().getUserInServerId(), modMailThreadId, feedBack, ex);
String message = "Failed To delete mod mail thread channel because no permissions.";
throw new AbstractoTemplatedException(message, "modmail_exception_cannot_delete_channel", ex);
} catch (Exception ex) {
log.error(failureMessage, modMailThreadId, ex);
undoActionService.performActions(undoActions);
throw new AbstractoRunTimeException(ex);
}
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);
}
}
/**
@@ -638,7 +545,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* the result values of the individual futures after they are done.
*/
@Transactional
public CompletableFutureList<Message> logModMailThread(Long modMailThreadId, List<CompletableFuture<Message>> messages, String note) {
public CompletableFutureList<Message> logModMailThread(Long modMailThreadId, List<CompletableFuture<Message>> messages, String note, List<UndoActionInstance> undoActions) {
log.info("Logging mod mail thread {}.", modMailThreadId);
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
if(modMailThreadOpt.isPresent()) {
@@ -671,6 +578,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
});
List<CompletableFuture<Message>> completableFutures = new ArrayList<>();
// TODO dont use this
modMailThread.setClosed(Instant.now());
ModMailClosingHeaderModel headerModel = ModMailClosingHeaderModel
.builder()
@@ -680,14 +588,10 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
log.trace("Sending close header and individual mod mail messages to mod mail log target.");
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_close_header", headerModel);
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 send
completableFutures.addAll(closeHeaderFutures);
completableFutures.addAll(self.sendMessagesToPostTarget(modMailThread, loggedMessages));
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]));
return CompletableFutureList
.<Message>builder()
.mainFuture(voidCompletableFuture)
.futures(completableFutures)
.build();
return new CompletableFutureList<>(completableFutures);
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);
}
@@ -704,6 +608,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
log.info("Setting thread {} to closed in db.", modMailThread.getId());
modMailThread.setClosed(Instant.now());
modMailThreadManagementService.setModMailThreadState(modMailThread, ModMailThreadState.CLOSED);
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);
@@ -727,104 +632,23 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
return messageFutures;
}
/**
* Sends the reply which was done by a staff member to the private channel with the {@link Member} and calls the
* method for saving the messages and updating the status
* @param modMailThreadId The ID of the {@link ModMailThread} for which the reply was created for
* @param text The text the reply should contain
* @param message The original message which triggered the command to create the reply. This is necessary for attachments
* @param privateChannel The instance of a {@link PrivateChannel} which was opened with the user and is used to send messages
* @param anonymous Whether or not the reply should be anonymous
* @param feedBack The {@link MessageChannel} in which exceptions should be sent to
* @throws ModMailThreadNotFoundException in case the {@link ModMailThread} is not found by the ID
*/
@Transactional
public void sendReply(Long modMailThreadId, String text, Message message, PrivateChannel privateChannel, Boolean anonymous, MessageChannel feedBack) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
AUserInAServer moderator = userInServerManagementService.loadUser(message.getMember());
Member userInGuild = botService.getMemberInServer(modMailThread.getUser());
FullUserInServer fullThreadUser = FullUserInServer
.builder()
.aUserInAServer(modMailThread.getUser())
.member(userInGuild)
.build();
ModMailModeratorReplyModel.ModMailModeratorReplyModelBuilder modMailModeratorReplyModelBuilder = ModMailModeratorReplyModel
.builder()
.text(text)
.modMailThread(modMailThread)
.postedMessage(message)
.anonymous(anonymous)
.threadUser(fullThreadUser);
if(anonymous) {
modMailModeratorReplyModelBuilder.moderator(botService.getBotInGuild(modMailThread.getServer()));
} else {
Member moderatorMember = botService.getMemberInServer(moderator);
modMailModeratorReplyModelBuilder.moderator(moderatorMember);
}
ModMailModeratorReplyModel modMailUserReplyModel = modMailModeratorReplyModelBuilder.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_staff_message", modMailUserReplyModel);
Long userInServerId = modMailThread.getUser().getUserInServerId();
List<CompletableFuture<Message>> completableFutures = channelService.sendMessageToSendToChannel(messageToSend, privateChannel);
CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).thenAccept(aVoid ->
self.saveSendMessagesAndUpdateState(modMailThreadId, anonymous, moderator, completableFutures)
).exceptionally(throwable -> {
log.error("Failed to send message to user in server {}", userInServerId);
sendModMailFailure("modmail_exception_cannot_message_user", userInServerId, modMailThread.getId(), feedBack, throwable);
return null;
});
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);
}
}
/**
* Evaluates the promises which were created when sending the messages to the private channel and stores the message IDs
* and updates the state of the {@link ModMailThread}.
* @param modMailThreadId The ID of the {@link ModMailThread} for which the messages were sent for
* @param anonymous Whether or not the messages were send anonymous
* @param moderator The original {@link AUserInAServer} which authored the messages
* @param completableFutures The list of {@link CompletableFuture} which contain the {@link Message} which were sent to the member
* @throws ModMailThreadNotFoundException in case the {@link ModMailThread} is not found by the ID
*/
@Transactional
public void saveSendMessagesAndUpdateState(Long modMailThreadId, Boolean anonymous, AUserInAServer moderator, List<CompletableFuture<Message>> completableFutures) {
public void saveSendMessagesAndUpdateState(Long modMailThreadId, Boolean anonymous, AUserInAServer moderator, Message message) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
List<Message> messages = new ArrayList<>();
completableFutures.forEach(messageCompletableFuture -> {
try {
Message messageToAdd = messageCompletableFuture.get();
messages.add(messageToAdd);
} catch (InterruptedException | ExecutionException e) {
log.error("A future when sending the message to the user was interrupted.", e);
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("Failed to handle the send staff message.", e);
}
});
self.saveMessageIds(messages, modMailThread, moderator, anonymous, true);
modMailMessageManagementService.addMessageToThread(modMailThread, message, moderator, anonymous, true);
modMailThreadManagementService.setModMailThreadState(modMailThread, ModMailThreadState.MOD_REPLIED);
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);
}
}
/**
* Takes the list of {@link Message} and attaches them to the given {@link ModMailThread} as {@link ModMailMessage} which should possibly
* be logged at a later time
* @param messages The list of {@link Message} to store the IDs of
* @param modMailThread The {@link ModMailThread} which should have the messages attached
* @param author The {@link AUserInAServer} who authored the {@link Message}
* @param anonymous Whether or not the {@link Message} was anonymous
* @param inDmChannel Whether or not the {@link Message} was sent in a private channel
*/
public void saveMessageIds(List<Message> messages, ModMailThread modMailThread, AUserInAServer author, Boolean anonymous, Boolean inDmChannel) {
messages.forEach(message ->
modMailMessageManagementService.addMessageToThread(modMailThread, message, author, anonymous, inDmChannel)
);
}
}