updated javadoc for experience tracking

added/changed some javadoc for mod mail
some small interface changes
changed a few methods to actually use the exists mechanic, instead of checking for null values
added some templates instead of hard coded strings
fixed some bugs related to mod mail
This commit is contained in:
Sheldan
2020-06-13 00:51:50 +02:00
parent e76351ddc1
commit 19a7d09576
111 changed files with 1238 additions and 162 deletions

View File

@@ -8,7 +8,7 @@ import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureEnum;
import dev.sheldan.abstracto.modmail.commands.condition.RequiresModMailCondition;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
@@ -19,11 +19,15 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* Sends the reply from the staff member to the user, but marks the reply as anonymous. The original author is still
* tracked internally.
*/
@Component
public class AnonReply extends AbstractConditionableCommand {
@Autowired
private RequiresModMailCondition requiresModMailCondition;
private ModMailContextCondition requiresModMailCondition;
@Autowired
private ModMailThreadService modMailThreadService;
@@ -34,6 +38,7 @@ public class AnonReply extends AbstractConditionableCommand {
@Override
public CommandResult execute(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());

View File

@@ -8,7 +8,7 @@ import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureEnum;
import dev.sheldan.abstracto.modmail.commands.condition.RequiresModMailCondition;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
@@ -21,11 +21,16 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
/**
* Closes the mod mail thread: logs the messages to the log post target, if the feature has the appropriate
* {@link dev.sheldan.abstracto.core.config.FeatureMode}, deletes the {@link net.dv8tion.jda.api.entities.MessageChannel}.
* This command takes an optional parameter, the note, which will be replaced with a default value, if not present
*/
@Component
public class Close extends AbstractConditionableCommand {
@Autowired
private RequiresModMailCondition requiresModMailCondition;
private ModMailContextCondition requiresModMailCondition;
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;
@@ -41,6 +46,7 @@ public class Close extends AbstractConditionableCommand {
@Transactional
public CommandResult execute(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);

View File

@@ -8,7 +8,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureEnum;
import dev.sheldan.abstracto.core.config.FeatureMode;
import dev.sheldan.abstracto.modmail.commands.condition.RequiresModMailCondition;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.config.ModMailMode;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
@@ -21,11 +21,16 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* 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 dev.sheldan.abstracto.modmail.config.ModMailFeature}
* in the 'LOGGING' mode, because else the normal close command behaves the same way.
*/
@Component
public class CloseNoLog extends AbstractConditionableCommand {
@Autowired
private RequiresModMailCondition requiresModMailCondition;
private ModMailContextCondition requiresModMailCondition;
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;
@@ -39,6 +44,7 @@ public class CloseNoLog extends AbstractConditionableCommand {
@Override
public CommandResult execute(CommandContext commandContext) {
ModMailThread thread = modMailThreadManagementService.getByChannel(commandContext.getUserInitiatedContext().getChannel());
// we dont have a note, therefore we cant pass any, the method handles this accordingly
modMailThreadService.closeModMailThread(thread, commandContext.getChannel(), null, false, false);
return CommandResult.fromSuccess();
}
@@ -67,6 +73,9 @@ public class CloseNoLog extends AbstractConditionableCommand {
return conditions;
}
/**
* This command is only available in the LOGGING mod mail feature mode
*/
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(ModMailMode.LOGGING);

View File

@@ -8,7 +8,7 @@ import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureEnum;
import dev.sheldan.abstracto.modmail.commands.condition.RequiresModMailCondition;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
@@ -20,11 +20,15 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* This command can be used to close the mod mail thread without sending a 'closing' message to the user.
* This behaves the same way as the default close commmand otherwise
*/
@Component
public class CloseSilently extends AbstractConditionableCommand {
@Autowired
private RequiresModMailCondition requiresModMailCondition;
private ModMailContextCondition requiresModMailCondition;
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;
@@ -37,6 +41,7 @@ public class CloseSilently extends AbstractConditionableCommand {
@Override
public CommandResult execute(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);

View File

@@ -26,6 +26,10 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* This command is used to create a thread with a member directly. If a thread already exists, this will post a link to
* the {@link net.dv8tion.jda.api.entities.MessageChannel}
*/
@Component
public class Contact extends AbstractConditionableCommand {
@@ -38,9 +42,6 @@ public class Contact extends AbstractConditionableCommand {
@Autowired
private UserInServerManagementService userManagementService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@@ -48,12 +49,13 @@ public class Contact extends AbstractConditionableCommand {
public CommandResult execute(CommandContext commandContext) {
Member targetUser = (Member) commandContext.getParameters().getParameters().get(0);
AUserInAServer user = userManagementService.loadUser(targetUser);
ModMailThread existingThread = modMailThreadManagementService.getOpenModmailThreadForUser(user);
if(existingThread != null) {
// if this AUserInAServer already has an open thread, we should instead post a message
// containing a link to the channel, instead of opening a new one
if(modMailThreadManagementService.hasOpenModMailThreadForUser(user)) {
ModMailThreadExistsModel model = (ModMailThreadExistsModel) ContextConverter.fromCommandContext(commandContext, ModMailThreadExistsModel.class);
ModMailThread existingThread = modMailThreadManagementService.getOpenModMailThreadForUser(user);
model.setExistingModMailThread(existingThread);
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_thread_already_exists", model);
channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel());
channelService.sendEmbedTemplateInChannel("modmail_thread_already_exists", model, commandContext.getChannel());
} else {
FullUser fullUser = FullUser
.builder()

View File

@@ -2,6 +2,8 @@ package dev.sheldan.abstracto.modmail.commands;
import dev.sheldan.abstracto.core.command.config.ModuleInfo;
import dev.sheldan.abstracto.core.command.config.ModuleInterface;
import dev.sheldan.abstracto.templating.service.TemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@@ -9,9 +11,13 @@ public class ModMailModuleInterface implements ModuleInterface {
public static final String MODMAIL = "modMail";
@Autowired
private TemplateService templateService;
@Override
public ModuleInfo getInfo() {
return ModuleInfo.builder().name(MODMAIL).description("Commands to be used for modmail.").build();
String description = templateService.renderSimpleTemplate("modmail_help_module_info");
return ModuleInfo.builder().name(MODMAIL).description(description).build();
}

View File

@@ -16,6 +16,11 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* This command is used to remove a role from the roles to be notified when a new {@link dev.sheldan.abstracto.modmail.models.database.ModMailThread}
* is opened. The method this command executes also automatically dis-allows all mod mail related {@link dev.sheldan.abstracto.core.command.Command}
* for this role.
*/
@Component
public class RemoveModMailRole extends AbstractConditionableCommand {
@@ -25,7 +30,7 @@ public class RemoveModMailRole extends AbstractConditionableCommand {
@Override
public CommandResult execute(CommandContext commandContext) {
ARole role = (ARole) commandContext.getParameters().getParameters().get(0);
modMailRoleService.removeRoleFromModMailRoles(role, commandContext.getUserInitiatedContext().getServer());
modMailRoleService.removeRoleFromModMailRoles(role);
return CommandResult.fromSuccess();
}

View File

@@ -8,7 +8,7 @@ import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureEnum;
import dev.sheldan.abstracto.modmail.commands.condition.RequiresModMailCondition;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
@@ -19,11 +19,14 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* Sends the reply from the staff member to the user, and shows the actual author in the created embed.
*/
@Component
public class Reply extends AbstractConditionableCommand {
@Autowired
private RequiresModMailCondition requiresModMailCondition;
private ModMailContextCondition requiresModMailCondition;
@Autowired
private ModMailThreadService modMailThreadService;

View File

@@ -15,6 +15,10 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* This command is used to change the category used to create new mod mail threads. This does not migrate the
* existing mod mail threads.
*/
@Component
public class SetModMailCategory extends AbstractConditionableCommand {
@@ -24,7 +28,7 @@ public class SetModMailCategory extends AbstractConditionableCommand {
@Override
public CommandResult execute(CommandContext commandContext) {
Long categoryId = (Long) commandContext.getParameters().getParameters().get(0);
modMailThreadService.setModMailCategoryTo(commandContext.getUserInitiatedContext().getServer(), categoryId);
modMailThreadService.setModMailCategoryTo(commandContext.getGuild(), categoryId);
return CommandResult.fromSuccess();
}

View File

@@ -16,6 +16,11 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* This command is used to add roles to the roles being pinged when a new mod mail thread is opened.
* The method this command uses automatically adds the mentioned roles to the roles which are allowed to execute
* the mod mail related commands.
*/
@Component
public class SetModMailRole extends AbstractConditionableCommand {
@@ -25,7 +30,7 @@ public class SetModMailRole extends AbstractConditionableCommand {
@Override
public CommandResult execute(CommandContext commandContext) {
ARole role = (ARole) commandContext.getParameters().getParameters().get(0);
modMailRoleService.addRoleToModMailRoles(role, commandContext.getUserInitiatedContext().getServer());
modMailRoleService.addRoleToModMailRoles(role);
return CommandResult.fromSuccess();
}

View File

@@ -7,7 +7,7 @@ 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.FeatureEnum;
import dev.sheldan.abstracto.modmail.commands.condition.RequiresModMailCondition;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailSubscriptionService;
@@ -18,11 +18,16 @@ import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* This command subscribes the member executing this command to the {@link ModMailThread} in which the command was executed in.
* In case the member is already subscribed an error message is displayed.
*/
@Component
public class Subscribe extends AbstractConditionableCommand {
@Autowired
private RequiresModMailCondition requiresModMailCondition;
private ModMailContextCondition requiresModMailCondition;
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;

View File

@@ -7,7 +7,7 @@ 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.FeatureEnum;
import dev.sheldan.abstracto.modmail.commands.condition.RequiresModMailCondition;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.ModMailSubscriptionService;
@@ -18,11 +18,16 @@ import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* This command un-subscribes the member executing this command from the {@link ModMailThread} in which the command was executed in.
* In case the member is not subscribed an error message is displayed.
*/
@Component
public class UnSubscribe extends AbstractConditionableCommand {
@Autowired
private RequiresModMailCondition requiresModMailCondition;
private ModMailContextCondition requiresModMailCondition;
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;

View File

@@ -1,26 +1,35 @@
package dev.sheldan.abstracto.modmail.commands.condition;
import dev.sheldan.abstracto.core.command.Command;
import dev.sheldan.abstracto.core.command.condition.CommandCondition;
import dev.sheldan.abstracto.core.command.condition.ConditionResult;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService;
import dev.sheldan.abstracto.templating.service.TemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* This {@link dev.sheldan.abstracto.core.command.condition.CommandCondition} checks the channel it is executed in
* and checks if the channel is a valid and open mod mail thread.
*/
@Component
public class RequiresModMailCondition implements CommandCondition {
public class RequiresModMailCondition implements ModMailContextCondition {
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;
@Autowired
private TemplateService templateService;
@Override
public ConditionResult shouldExecute(CommandContext commandContext, Command command) {
ModMailThread thread = modMailThreadManagementService.getByChannel(commandContext.getUserInitiatedContext().getChannel());
if(thread != null) {
return ConditionResult.builder().result(true).build();
}
return ConditionResult.builder().result(false).reason("Not in a mod mail thread.").build();
String text = templateService.renderSimpleTemplate("modmail_not_in_modmail_thread");
return ConditionResult.builder().result(false).reason(text).build();
}
}

View File

@@ -10,6 +10,11 @@ import org.springframework.stereotype.Component;
import static dev.sheldan.abstracto.modmail.service.ModMailThreadServiceBean.MODMAIL_CLOSING_MESSAGE_TEXT;
/**
* This listener is used to used to set the initial values of some server specific values, so we dont need to fall
* back to the default values. The values might not be functional, for example mod mail category id, but their existence
* makes things easier
*/
@Component
public class ModMailConfigListener implements ServerConfigListener {

View File

@@ -10,6 +10,10 @@ import org.springframework.transaction.annotation.Transactional;
import static dev.sheldan.abstracto.modmail.service.ModMailThreadServiceBean.MODMAIL_CLOSING_MESSAGE_TEXT;
/**
* This listener is executed when the Spring context starts up and is used to set some default values related
* to mod mail.
*/
@Component
public class ModMailDefaultConfigListener {

View File

@@ -3,6 +3,8 @@ package dev.sheldan.abstracto.modmail.listener;
import dev.sheldan.abstracto.core.config.FeatureEnum;
import dev.sheldan.abstracto.core.listener.PrivateMessageReceivedListener;
import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserManagementService;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.models.database.ModMailThread;
@@ -14,6 +16,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 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
* user should be prompted for a new mod mail thread.
*/
@Component
@Slf4j
public class ModMailMessageListener implements PrivateMessageReceivedListener {
@@ -27,12 +36,17 @@ public class ModMailMessageListener implements PrivateMessageReceivedListener {
@Autowired
private UserManagementService userManagementService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Override
@Transactional
public void execute(Message message) {
AUser user = userManagementService.loadUser(message.getAuthor().getIdLong());
ModMailThread existingThread = modMailThreadManagementService.getOpenModmailThreadForUser(user);
if(existingThread != null) {
if(modMailThreadManagementService.hasOpenModMailThread(user)) {
// 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);
} else {
modMailThreadService.createModMailPrompt(user, message);

View File

@@ -7,6 +7,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository to manage the stored {@link ModMailMessage} instances
*/
@Repository
public interface ModMailMessageRepository extends JpaRepository<ModMailMessage, Long> {
List<ModMailMessage> findByThreadReference(ModMailThread modMailThread);

View File

@@ -8,6 +8,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository to manage the stored {@link ModMailRole} instances
*/
@Repository
public interface ModMailRoleRepository extends JpaRepository<ModMailRole, Long> {
boolean existsByServerAndRole(AServer server, ARole role);

View File

@@ -8,6 +8,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository to manage the stored {@link ModMailThreadSubscriber} instances
*/
@Repository
public interface ModMailSubscriberRepository extends JpaRepository<ModMailThreadSubscriber, Long> {
List<ModMailThreadSubscriber> findByThreadReference(ModMailThread thread);

View File

@@ -11,13 +11,18 @@ import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository to manage the stored {@link ModMailThread} instances
*/
@Repository
public interface ModMailThreadRepository extends JpaRepository<ModMailThread, Long> {
ModMailThread findByChannel(AChannel channel);
List<ModMailThread> findByUser(AUserInAServer aUserInAServer);
ModMailThread findTopByUserOrderByClosedDesc(AUserInAServer aUserInAServer);
ModMailThread findByUser_UserReferenceAndStateNot(AUser user, ModMailThreadState state);
List<ModMailThread> findByUser_UserReferenceAndStateNot(AUser user, ModMailThreadState state);
boolean existsByUser_UserReferenceAndStateNot(AUser user, ModMailThreadState state);
List<ModMailThread> findByServerAndState(AServer server, ModMailThreadState state);
ModMailThread findByUserAndStateNot(AUserInAServer userInAServer, ModMailThreadState state);
boolean existsByUserAndStateNot(AUserInAServer userInAServer, ModMailThreadState state);
List<ModMailThread> findByUserAndState(AUserInAServer userInAServer, ModMailThreadState state);
}

View File

@@ -28,12 +28,14 @@ public class ModMailMessageServiceBean implements ModMailMessageService {
if(modMailMessages.isEmpty()) {
return new ArrayList<>();
}
// all message must be from the same thread
ModMailThread thread = modMailMessages.get(0).getThreadReference();
List<ServerChannelMessage> messageIds = new ArrayList<>();
modMailMessages.forEach(modMailMessage -> {
ServerChannelMessage.ServerChannelMessageBuilder serverChannelMessageBuilder = ServerChannelMessage
.builder()
.messageId(modMailMessage.getMessageId());
// if its not from a private chat, we need to set the server and channel ID in order to fetch the data
if(Boolean.FALSE.equals(modMailMessage.getDmChannel())) {
serverChannelMessageBuilder
.channelId(modMailMessage.getThreadReference().getChannel().getId())
@@ -42,6 +44,10 @@ public class ModMailMessageServiceBean implements ModMailMessageService {
messageIds.add(serverChannelMessageBuilder.build());
});
List<CompletableFuture<Message>> 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(new CompletableFuture<>()));
Optional<TextChannel> textChannelFromServer = botService.getTextChannelFromServer(thread.getServer().getId(), thread.getChannel().getId());
if(textChannelFromServer.isPresent()) {
@@ -49,6 +55,8 @@ public class ModMailMessageServiceBean implements ModMailMessageService {
botService.getInstance().openPrivateChannelById(thread.getUser().getUserReference().getId()).queue(privateChannel -> {
Iterator<CompletableFuture<Message>> iterator = messageFutures.iterator();
messageIds.forEach(serverChannelMessage -> {
// TODO fix out of order promises
// depending what the source of the message is, we need to fetch the message from the correct channel
if(serverChannelMessage.getChannelId() == null){
privateChannel.retrieveMessageById(serverChannelMessage.getMessageId()).queue(message -> iterator.next().complete(message), throwable -> {
log.info("Failed to load message in private channel with user {}", thread.getUser().getUserReference().getId());

View File

@@ -3,7 +3,6 @@ package dev.sheldan.abstracto.modmail.service;
import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.modmail.config.ModMailFeatures;
import dev.sheldan.abstracto.modmail.service.management.ModMailRoleManagementService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -22,16 +21,16 @@ public class ModMailRoleServiceBean implements ModMailRoleService {
private FeatureManagementService featureManagementService;
@Override
public void addRoleToModMailRoles(ARole role, AServer server) {
if(!modMailRoleManagementService.isRoleAlreadyAssigned(role, server)) {
modMailRoleManagementService.addRoleToModMailRoles(role, server);
public void addRoleToModMailRoles(ARole role) {
if(!modMailRoleManagementService.isRoleAlreadyAssigned(role, role.getServer())) {
modMailRoleManagementService.addRoleToModMailRoles(role, role.getServer());
}
commandService.allowFeatureForRole(ModMailFeatures.MOD_MAIL, role);
}
@Override
public void removeRoleFromModMailRoles(ARole role, AServer server) {
modMailRoleManagementService.removeRoleFromModMailRoles(role, server);
public void removeRoleFromModMailRoles(ARole role) {
modMailRoleManagementService.removeRoleFromModMailRoles(role, role.getServer());
commandService.disAllowFeatureForRole(ModMailFeatures.MOD_MAIL, role);
}
}

View File

@@ -19,7 +19,7 @@ public class ModMailSubscriptionServiceBean implements ModMailSubscriptionServic
if(!modMailSubscriberManagementService.isSubscribedToThread(aUserInAServer, modMailThread)){
modMailSubscriberManagementService.createSubscriber(aUserInAServer, modMailThread);
} else {
throw new AlreadySubscribedException("The user is already subscribed to the thread.");
throw new AlreadySubscribedException();
}
}
@@ -28,7 +28,7 @@ public class ModMailSubscriptionServiceBean implements ModMailSubscriptionServic
if(modMailSubscriberManagementService.isSubscribedToThread(aUserInAServer, modMailThread)){
modMailSubscriberManagementService.removeSubscriber(aUserInAServer, modMailThread);
} else {
throw new NotSubscribedException("The user is not subscribed to the thread.");
throw new NotSubscribedException();
}
}
}

View File

@@ -2,7 +2,10 @@ package dev.sheldan.abstracto.modmail.service;
import com.jagrosh.jdautilities.commons.waiter.EventWaiter;
import com.jagrosh.jdautilities.menu.ButtonMenu;
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.FullUser;
import dev.sheldan.abstracto.core.models.UndoActionInstance;
@@ -13,6 +16,7 @@ import dev.sheldan.abstracto.core.service.management.UserInServerManagementServi
import dev.sheldan.abstracto.core.service.management.UserInServerService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.modmail.config.*;
import dev.sheldan.abstracto.modmail.exception.ModMailCategoryIdException;
import dev.sheldan.abstracto.modmail.exception.ModMailThreadNotFoundException;
import dev.sheldan.abstracto.modmail.models.database.*;
import dev.sheldan.abstracto.modmail.models.dto.ServerChoice;
@@ -21,6 +25,7 @@ import dev.sheldan.abstracto.modmail.service.management.ModMailMessageManagement
import dev.sheldan.abstracto.modmail.service.management.ModMailRoleManagementService;
import dev.sheldan.abstracto.modmail.service.management.ModMailSubscriberManagementService;
import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService;
import dev.sheldan.abstracto.modmail.validator.ModMailFeatureValidator;
import dev.sheldan.abstracto.templating.model.MessageToSend;
import dev.sheldan.abstracto.templating.service.TemplateService;
import lombok.extern.slf4j.Slf4j;
@@ -39,9 +44,19 @@ import java.util.concurrent.ExecutionException;
@Slf4j
public class ModMailThreadServiceBean implements ModMailThreadService {
/**
* The config key to use for the closing text
*/
public static final String MODMAIL_CLOSING_MESSAGE_TEXT = "modMailClosingText";
/**
* The config key to use for the ID of the category to create {@link MessageChannel} in
*/
public static final String MODMAIL_CATEGORY = "modmailCategory";
/**
* The template key used for default mod mail exceptions
*/
public static final String MODMAIL_EXCEPTION_GENERIC_TEMPLATE = "modmail_exception_generic";
@Autowired
private ModMailThreadManagementService modMailThreadManagementService;
@@ -99,10 +114,15 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Autowired
private FeatureModeService featureModeService;
@Autowired
private ModMailFeatureValidator modMailFeatureValidator;
@Autowired
private ModMailThreadServiceBean self;
/**
* The emoji used when the user can decide for a server to open a mod mail thread in.
*/
private List<String> NUMBER_EMOJI = Arrays.asList("\u0031\u20e3", "\u0032\u20e3", "\u0033\u20e3",
"\u0034\u20e3", "\u0035\u20e3", "\u0036\u20e3",
"\u0037\u20e3", "\u0038\u20e3", "\u0039\u20e3",
@@ -111,21 +131,32 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Override
public void createModMailThreadForUser(FullUser aUserInAServer, Message initialMessage, MessageChannel feedBackChannel, boolean userInitiated) {
Long categoryId = configService.getLongValue(MODMAIL_CATEGORY, aUserInAServer.getAUserInAServer().getServerReference().getId());
Long serverId = aUserInAServer.getAUserInAServer().getServerReference().getId();
Long categoryId = configService.getLongValue(MODMAIL_CATEGORY, serverId);
User user = aUserInAServer.getMember().getUser();
CompletableFuture<TextChannel> textChannel = channelService.createTextChannel(user.getName() + user.getDiscriminator(), aUserInAServer.getAUserInAServer().getServerReference(), categoryId);
CompletableFuture<TextChannel> textChannelFuture = channelService.createTextChannel(user.getName() + user.getDiscriminator(), aUserInAServer.getAUserInAServer().getServerReference(), categoryId);
textChannel.thenAccept(channel -> {
Long userInServerId = aUserInAServer.getAUserInAServer().getUserInServerId();
textChannelFuture.thenAccept(channel -> {
List<UndoActionInstance> undoActions = new ArrayList<>();
undoActions.add(UndoActionInstance.getChannelDeleteAction(aUserInAServer.getAUserInAServer().getServerReference().getId(), channel.getIdLong()));
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", aUserInAServer.getAUserInAServer(), null, feedBackChannel, throwable);
sendModMailFailure("modmail_exception_failed_to_create_mod_mail_thread", userInServerId, null, feedBackChannel, throwable);
return null;
});
}
/**
* 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 FullUser} 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 oepend via a command
* @param channel The created {@link TextChannel} in which the mod mail thread is dealth with
* @param userInitiated Whether or not the thread was initiated by a member
* @param undoActions The list of actions to undo, in case an exception occurs
*/
@Transactional
public void performModMailThreadSetup(FullUser aUserInAServer, Message initialMessage, TextChannel channel, boolean userInitiated, List<UndoActionInstance> undoActions) {
try {
@@ -148,6 +179,12 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
}
/**
* Sends the message containing the pings to notify the staff members to handle the opened {@link ModMailThread}
* @param aUserInAServer The {@link FullUser} 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(FullUser aUserInAServer, ModMailThread thread, List<UndoActionInstance> undoActions) {
List<ModMailRole> rolesToPing = modMailRoleManagementService.getRolesForServer(thread.getServer());
@@ -167,6 +204,12 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
});
}
/**
* 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 FullUser} which the thread is about
* @return The created instance of {@link ModMailThread}
*/
public ModMailThread createThreadObject(TextChannel channel, FullUser user) {
AChannel channel2 = channelManagementService.createChannel(channel.getIdLong(), AChannelType.TEXT, user.getAUserInAServer().getServerReference());
log.info("Creating mod mail thread in channel {} with db channel {}", channel.getIdLong(), channel2.getId());
@@ -174,28 +217,26 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
@Override
public boolean hasOpenThread(AUserInAServer aUserInAServer) {
return modMailThreadManagementService.getOpenModmailThreadForUser(aUserInAServer) != null;
}
@Override
public boolean hasOpenThread(AUser user) {
return modMailThreadManagementService.getOpenModmailThreadForUser(user) != null;
}
@Override
public void setModMailCategoryTo(AServer server, Long categoryId) {
configService.setLongValue(MODMAIL_CATEGORY, server.getId(), categoryId);
public void setModMailCategoryTo(Guild guild, Long categoryId) {
FeatureValidationResult result = FeatureValidationResult.builder().build();
modMailFeatureValidator.validateModMailCategory(result, guild, categoryId);
if(result.getValidationResult()) {
throw new ModMailCategoryIdException(categoryId);
}
configService.setLongValue(MODMAIL_CATEGORY, guild.getIdLong(), categoryId);
}
@Override
public void createModMailPrompt(AUser user, Message initialMessage) {
List<AUserInAServer> knownServers = userInServerManagementService.getUserInAllServers(user.getId());
// do nothing if we dont know the user
if(!knownServers.isEmpty()) {
List<ServerChoice> availableGuilds = new ArrayList<>();
HashMap<String, AUserInAServer> choices = new HashMap<>();
for (int i = 0; i < knownServers.size(); i++) {
AUserInAServer aUserInAServer = knownServers.get(i);
// only take the servers in which mod mail is actually enabled, would not make much sense to make the
// other servers available
if(featureFlagService.isFeatureEnabled(modMailFeature, aUserInAServer.getServerReference())) {
AServer serverReference = aUserInAServer.getServerReference();
FullGuild guild = FullGuild
@@ -203,12 +244,14 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
.guild(botService.getGuildByIdNullable(serverReference.getId()))
.server(serverReference)
.build();
// TODO support more than this limited amount of servers
String reactionEmote = NUMBER_EMOJI.get(i);
ServerChoice serverChoice = ServerChoice.builder().guild(guild).reactionEmote(reactionEmote).build();
choices.put(reactionEmote, aUserInAServer);
availableGuilds.add(serverChoice);
}
}
// if more than 1 server is available, show a choice dialog
if(availableGuilds.size() > 1) {
ModMailServerChooserModel modMailServerChooserModel = ModMailServerChooserModel
.builder()
@@ -227,14 +270,29 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
})
.build();
menu.display(initialMessage.getChannel());
} else if(availableGuilds.size() == 1) {
// if exactly one server is available, open the thread directly
AUserInAServer chosenServer = choices.get(availableGuilds.get(0).getReactionEmote());
Member memberInServer = botService.getMemberInServer(chosenServer);
FullUser fullUser = FullUser.builder().member(memberInServer).aUserInAServer(chosenServer).build();
self.createModMailThreadForUser(fullUser, initialMessage, initialMessage.getChannel(), true);
} else {
// in case there is no server available, send an error message
channelService.sendEmbedTemplateInChannel("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());
}
}
/**
* Method used to send the header of a newly created mod mail thread. This message contains information about
* 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, FullUser aUserInAServer, List<UndoActionInstance> undoActions) {
ModMailThread latestThread = modMailThreadManagementService.getLatestModMailThread(aUserInAServer.getAUserInAServer());
List<ModMailThread> oldThreads = modMailThreadManagementService.getModMailThreadForUser(aUserInAServer.getAUserInAServer());
@@ -267,6 +325,15 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
}
/**
* 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
* 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 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();
FullUser fullUser = FullUser
@@ -300,6 +367,14 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
/**
* 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);
@@ -315,7 +390,9 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
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);
});
} else {
@@ -336,9 +413,21 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
}
/**
* 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, AUserInAServer aUserInAServer, Long modMailTreadId, MessageChannel channel, Throwable throwable) {
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 {
@@ -363,10 +452,10 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
@Override
public synchronized void closeModMailThread(ModMailThread modMailThread, MessageChannel feedBack, String note, boolean notifyUser) {
public void closeModMailThread(ModMailThread modMailThread, MessageChannel feedBack, String note, boolean notifyUser) {
AFeatureMode aFeatureMode = featureModeService.getFeatureMode(ModMailFeatures.MOD_MAIL, modMailThread.getServer());
boolean loggingMode = aFeatureMode.getMode().equalsIgnoreCase(ModMailMode.LOGGING.getKey());
closeModMailThread(modMailThread, feedBack, note, notifyUser, loggingMode);
closeModMailThread(modMailThread, feedBack, note, notifyUser, loggingMode);
}
@Override
@@ -404,7 +493,19 @@ 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 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();
try {
CompletableFutureList<Message> list = self.logModMailThread(modMailThreadId, messages, note);
list.getMainFuture().thenRun(() -> {
@@ -422,19 +523,28 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
self.afterSuccessfulLog(modMailThreadId, feedBack, notifyUser, undoActions);
});
list.getMainFuture().exceptionally(innerThrowable -> {
sendModMailFailure(MODMAIL_EXCEPTION_GENERIC_TEMPLATE, innerModMailThread.getUser(), modMailThreadId, feedBack, 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", innerModMailThread.getUser(), modMailThreadId, feedBack, 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, innerModMailThread.getUser(), modMailThreadId, feedBack, e);
sendModMailFailure(MODMAIL_EXCEPTION_GENERIC_TEMPLATE, userInServerId, modMailThreadId, feedBack, e);
}
}
/**
* 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) {
log.trace("Mod mail logging for thread {} has completed. Starting post logging activities.", modMailThreadId);
@@ -463,7 +573,8 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
});
} catch (Exception e) {
log.error("Failed to render closing user message", e);
sendModMailFailure(MODMAIL_EXCEPTION_GENERIC_TEMPLATE, modMailThread.getUser(), modMailThreadId, feedBack, 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);
@@ -474,6 +585,14 @@ 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) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
@@ -495,7 +614,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
} catch (InsufficientPermissionException ex) {
log.error(failureMessage, modMailThreadId, ex);
undoActionService.performActions(undoActions);
sendModMailFailure("modmail_exception_cannot_delete_channel", modMailThread.getUser(), modMailThreadId, feedBack, ex);
sendModMailFailure("modmail_exception_cannot_delete_channel", modMailThread.getUser().getUserInServerId(), modMailThreadId, feedBack, ex);
} catch (Exception ex) {
log.error(failureMessage, modMailThreadId, ex);
undoActionService.performActions(undoActions);
@@ -506,13 +625,25 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
/**
* Takes the list of {@link CompletableFuture} which are returned from retrieving the {@link Message} to log,
* and creates the models necessary to render the log entries. This message also sends the closing header in the
* 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}
* @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, List<CompletableFuture<Message>> messages, String note) {
log.info("Logging mod mail thread {}.", modMailThreadId);
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
List<ModMailLoggedMessage> loggedMessages = new ArrayList<>();
List<ModMailLoggedMessageModel> loggedMessages = new ArrayList<>();
messages.forEach(future -> {
try {
if(!future.isCompletedExceptionally()) {
@@ -521,15 +652,15 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
ModMailMessage modmailMessage = modMailThread.getMessages()
.stream()
.filter(modMailMessage -> modMailMessage.getMessageId().equals(loadedMessage.getIdLong()))
.findFirst().get();
ModMailLoggedMessage modMailLoggedMessage =
ModMailLoggedMessage
.findFirst().orElseThrow(() -> new AbstractoRunTimeException("Could not find desired message in list of messages in thread. This should not happen, as we just retrieved them from the same place."));
ModMailLoggedMessageModel modMailLoggedMessageModel =
ModMailLoggedMessageModel
.builder()
.message(loadedMessage)
.modMailMessage(modmailMessage)
.author(userInServerService.getFullUser(modmailMessage.getAuthor()))
.build();
loggedMessages.add(modMailLoggedMessage);
loggedMessages.add(modMailLoggedMessageModel);
}
}
} catch (InterruptedException | ExecutionException e) {
@@ -562,6 +693,11 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
}
/**
* Sets the {@link ModMailThread} in the database to CLOSED.
* @param modMailThreadId The ID of the {@link ModMailThread} to update the state of
* @throws ModMailThreadNotFoundException in case the {@link ModMailThread} is not found by the ID
*/
@Transactional
public void closeModMailThreadInDb(Long modMailThreadId) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
@@ -574,7 +710,14 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
}
public List<CompletableFuture<Message>> sendMessagesToPostTarget(ModMailThread modMailThread, List<ModMailLoggedMessage> loadedMessages) {
/**
* Renders the retrieved {@link Message} which are in {@link ModMailLoggedMessageModel} into {@link MessageToSend} and
* sends this to the appropriate logging {@link PostTarget}
* @param modMailThread The {@link ModMailThread} to which the loaded messages belong to
* @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) {
List<CompletableFuture<Message>> messageFutures = new ArrayList<>();
loadedMessages.forEach(message -> {
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_close_logged_message", message);
@@ -584,6 +727,17 @@ 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);
@@ -591,7 +745,6 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
ModMailThread modMailThread = modMailThreadOpt.get();
AUserInAServer moderator = userInServerManagementService.loadUser(message.getMember());
Member userInGuild = botService.getMemberInServer(modMailThread.getUser());
Member moderatorMember = botService.getMemberInServer(moderator);
FullUser fullThreadUser = FullUser
.builder()
.aUserInAServer(modMailThread.getUser())
@@ -607,16 +760,18 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
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 {}", modMailThread.getUser().getUserReference().getId());
sendModMailFailure("modmail_exception_cannot_message_user", modMailThread.getUser(), modMailThread.getId(), feedBack, 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 {
@@ -625,6 +780,15 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
/**
* 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) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getById(modMailThreadId);
@@ -649,6 +813,15 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
}
/**
* 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)

View File

@@ -47,15 +47,25 @@ public class ModMailThreadManagementServiceBean implements ModMailThreadManageme
}
@Override
public ModMailThread getOpenModmailThreadForUser(AUserInAServer userInAServer) {
public ModMailThread getOpenModMailThreadForUser(AUserInAServer userInAServer) {
return modMailThreadRepository.findByUserAndStateNot(userInAServer, ModMailThreadState.CLOSED);
}
@Override
public ModMailThread getOpenModmailThreadForUser(AUser user) {
public boolean hasOpenModMailThreadForUser(AUserInAServer userInAServer) {
return modMailThreadRepository.existsByUserAndStateNot(userInAServer, ModMailThreadState.CLOSED);
}
@Override
public List<ModMailThread> getOpenModMailThreadsForUser(AUser user) {
return modMailThreadRepository.findByUser_UserReferenceAndStateNot(user, ModMailThreadState.CLOSED);
}
@Override
public boolean hasOpenModMailThread(AUser user) {
return modMailThreadRepository.existsByUser_UserReferenceAndStateNot(user, ModMailThreadState.CLOSED);
}
@Override
public List<ModMailThread> getModMailThreadForUser(AUserInAServer aUserInAServer) {
return modMailThreadRepository.findByUser(aUserInAServer);
@@ -66,6 +76,12 @@ public class ModMailThreadManagementServiceBean implements ModMailThreadManageme
return modMailThreadRepository.findTopByUserOrderByClosedDesc(aUserInAServer);
}
/**
* The status of the created instance is INITIAL.
* @param userInAServer The {@link AUserInAServer} for which the thread was created for
* @param channel An instance of {@link AChannel} in which the conversation with the member is handled
* @return the created {@link ModMailThread} instance
*/
@Override
public ModMailThread createModMailThread(AUserInAServer userInAServer, AChannel channel) {
ModMailThread thread = ModMailThread

View File

@@ -3,6 +3,10 @@ package dev.sheldan.abstracto.modmail.setup;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.templating.Templatable;
/**
* This exception is thrown when the provided category used for creating mod mail thread is not valid.
* (If it does not exist in the guild)
*/
public class InvalidCategoryException extends AbstractoRunTimeException implements Templatable {
public InvalidCategoryException() {
super("");

View File

@@ -7,19 +7,34 @@ import dev.sheldan.abstracto.modmail.service.ModMailThreadServiceBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* This delayed action is responsible for setting the system configuration of the mod mail category for a given server
*/
@Component
public class ModMailCategoryDelayedAction implements DelayedAction {
@Autowired
private ConfigService configService;
/**
* Sets the config key of MODMAIL_CATEGORY to the given category ID contained in the {@link DelayedActionConfig}
* @param delayedActionConfig An instance of {@link ModMailCategoryDelayedActionConfig} containing the ID
* of the {@link net.dv8tion.jda.api.entities.Category} and the ID of the
* {@link net.dv8tion.jda.api.entities.Guild} to change
*/
@Override
public void execute(DelayedActionConfig delayedActionConfig) {
ModMailCategoryDelayedActionConfig concrete = (ModMailCategoryDelayedActionConfig) delayedActionConfig;
configService.setConfigValue(ModMailThreadServiceBean.MODMAIL_CATEGORY, concrete.getServerId(), concrete.getValue());
configService.setLongValue(ModMailThreadServiceBean.MODMAIL_CATEGORY, concrete.getServerId(), concrete.getCategoryId());
}
/**
* This delayed action only reacts to delayed action configurations of typ {@link ModMailCategoryDelayedActionConfig}.
* As this the instance bound to this {@link DelayedAction}
* @param delayedActionConfig An instance of check whether or not this {@link DelayedAction} should be executed for this
* {@link DelayedActionConfig}
* @return Whether or not the passed {@link DelayedActionConfig} is going to be handled by this class.
*/
@Override
public boolean handles(DelayedActionConfig delayedActionConfig) {
return delayedActionConfig instanceof ModMailCategoryDelayedActionConfig;

View File

@@ -1,19 +1,24 @@
package dev.sheldan.abstracto.modmail.setup;
import dev.sheldan.abstracto.core.interactive.DelayedActionConfig;
import dev.sheldan.abstracto.core.models.database.AConfig;
import dev.sheldan.abstracto.modmail.models.template.ModMailCategoryActionModel;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Category;
/**
* This represents both an instance of a {@link DelayedActionConfig} used to be executed in the
* {@link dev.sheldan.abstracto.core.service.DelayedActionService} and, as all {@link DelayedActionConfig}, as a
* model when all setup steps are presented, and the member executing the setup command needs to confirm the changes.
* This model is responsible to contain the values needed to displayed the mod mail category changes.
*/
@Getter
@Setter
@Builder
public class ModMailCategoryDelayedActionConfig implements DelayedActionConfig {
private Long serverId;
private AConfig value;
private Long categoryId;
private Category category;
@Override
@@ -26,7 +31,7 @@ public class ModMailCategoryDelayedActionConfig implements DelayedActionConfig {
return ModMailCategoryActionModel
.builder()
.category(this.category)
.categoryId(value.getLongValue())
.categoryId(categoryId)
.build();
}
}

View File

@@ -5,7 +5,6 @@ import dev.sheldan.abstracto.core.interactive.*;
import dev.sheldan.abstracto.core.models.AServerChannelUserId;
import dev.sheldan.abstracto.core.models.FeatureValidationResult;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AConfig;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.BotService;
import dev.sheldan.abstracto.core.service.ConfigService;
@@ -61,6 +60,19 @@ public class ModMailCategorySetupBean implements ModMailCategorySetup {
@Autowired
private BotService botService;
/**
* This setup method loads the existing mod mail category (if anything) and populates the model used to render the prompt.
* This method will then render the prompt and wait for the users input, if the provided input was a valid
* category ID in the current server, this method returns the proper result. If anything else is put in (except the input
* triggering a cancellation) this this method will jump back to this step and prompt the user again.
* @param user The {@link AServerChannelUserId} context required in order to execute the step. This is needed
* to check if the returned message is from the same user and to see for which server
* we need to change the mod mail category for
* @param parameter This is a parameter which contains the previous message triggering the setup step.
* This is necessary, because sometimes the {@link Message} executing the setup was also triggering
* the first {@link SetupStep}, so, if we are aware of it, we can ignore it
* @return A {@link CompletableFuture} containing the {@link SetupStepResult}. This might be cancelled or successful.
*/
@Override
public CompletableFuture<SetupStepResult> execute(AServerChannelUserId user, SetupStepParameter parameter) {
String messageTemplateKey = "setup_modmail_category_message";
@@ -78,6 +90,7 @@ public class ModMailCategorySetupBean implements ModMailCategorySetup {
CompletableFuture<SetupStepResult> future = new CompletableFuture<>();
AUserInAServer aUserInAServer = userInServerManagementService.loadUser(user.getGuildId(), user.getUserId());
// very odd case, if the channel the command was executed in, was not found in the database.
if(channel.isPresent()) {
Runnable finalAction = getTimeoutRunnable(user.getGuildId(), user.getChannelId());
Consumer<MessageReceivedEvent> configAction = (MessageReceivedEvent event) -> {
@@ -85,21 +98,23 @@ public class ModMailCategorySetupBean implements ModMailCategorySetup {
SetupStepResult result;
Message message = event.getMessage();
// this checks whether or not the user wanted to cancel the setup
if(checkForExit(message)) {
result = SetupStepResult.fromCancelled();
} else {
String messageContent = event.getMessage().getContentRaw();
// directly parse the long from the message, for *now*, only the category ID is supported
Long categoryId = Long.parseLong(messageContent);
Guild guild = botService.getGuildByIdNullable(user.getGuildId());
FeatureValidationResult featureValidationResult = FeatureValidationResult.builder().validationResult(true).build();
// directly validate whether or not the given category ID is a valid value
modMailFeatureValidator.validateModMailCategory(featureValidationResult, guild, categoryId);
if(Boolean.FALSE.equals(featureValidationResult.getValidationResult())) {
AConfig fakeValue = configService.getFakeConfigForValue(ModMailThreadServiceBean.MODMAIL_CATEGORY, user.getGuildId(), messageContent);
if(Boolean.TRUE.equals(featureValidationResult.getValidationResult())) {
ModMailCategoryDelayedActionConfig build = ModMailCategoryDelayedActionConfig
.builder()
.serverId(user.getGuildId())
.category(guild.getCategoryById(categoryId))
.value(fakeValue)
.categoryId(categoryId)
.build();
List<DelayedActionConfig> delayedSteps = Arrays.asList(build);
result = SetupStepResult
@@ -108,6 +123,7 @@ public class ModMailCategorySetupBean implements ModMailCategorySetup {
.delayedActionConfigList(delayedSteps)
.build();
} else {
// exceptions this exception is used to effectively fail the setup step
throw new InvalidCategoryException();
}
@@ -115,7 +131,7 @@ public class ModMailCategorySetupBean implements ModMailCategorySetup {
future.complete(result);
} catch (Exception e) {
log.error("Failed to handle post target step.", e);
log.error("Failed to handle mod mail category step.", e);
future.completeExceptionally(new SetupStepException(e));
}
};

View File

@@ -6,7 +6,7 @@ import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.BotService;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.FeatureValidatorService;
import dev.sheldan.abstracto.modmail.models.template.ModMailCategoryValidationError;
import dev.sheldan.abstracto.modmail.models.template.ModMailCategoryValidationErrorModel;
import dev.sheldan.abstracto.modmail.service.ModMailThreadServiceBean;
import net.dv8tion.jda.api.entities.Category;
import net.dv8tion.jda.api.entities.Guild;
@@ -15,6 +15,11 @@ import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* This component is used to validate whether or not the mod mail feature has a mod mail category configured, which points to
* a category on the server it is configured for. This and other {@link dev.sheldan.abstracto.core.service.FeatureValidator}
* are used to fully validate the mod mail feature.
*/
@Component
public class ModMailFeatureValidatorBean implements ModMailFeatureValidator {
@@ -27,6 +32,12 @@ public class ModMailFeatureValidatorBean implements ModMailFeatureValidator {
@Autowired
private FeatureValidatorService featureValidatorService;
/**
* Checks if the mod mail category contains a value and whether or not this valid also points to a {@link Category}
* @param featureConfig The instance of {@link FeatureConfig} of mod mail
* @param server The {@link AServer} to check the config for
* @param validationResult The current {@link FeatureValidationResult} used to accumulate the wrong values
*/
@Override
public void featureIsSetup(FeatureConfig featureConfig, AServer server, FeatureValidationResult validationResult) {
Optional<Guild> guildById = botService.getGuildById(server.getId());
@@ -40,15 +51,21 @@ public class ModMailFeatureValidatorBean implements ModMailFeatureValidator {
}
}
/**
* Validates the category and checks if the given ID is a valid category in the given {@link Guild}
* @param validationResult The object in which the result of the validation will be stored
* @param guild The {@link Guild} to check for the category
* @param modMailCategory The configured ID of the category
*/
public void validateModMailCategory(FeatureValidationResult validationResult, Guild guild, Long modMailCategory) {
Category categoryById = guild.getCategoryById(modMailCategory);
if(categoryById == null) {
validationResult.setValidationResult(false);
ModMailCategoryValidationError newError = ModMailCategoryValidationError
ModMailCategoryValidationErrorModel newError = ModMailCategoryValidationErrorModel
.builder()
.currentCategoryId(modMailCategory)
.build();
validationResult.getValidationErrors().add(newError);
validationResult.getValidationErrorModels().add(newError);
}
}
}

View File

@@ -1 +1 @@
<#assign categoryName><#if category?has_content>${category.name}<#else><#include "setup_modmail_category_message_no_category">></#if></#assign><#include "setup_modmail_category_message_display">
<#assign categoryName><#if category?has_content>${category.name}<#else><#include "setup_modmail_category_message_no_category"></#if></#assign><#include "setup_modmail_category_message_display">