diff --git a/abstracto-application/abstracto-modules/link-embed/link-embed-impl/src/main/java/dev/sheldan/abstracto/linkembed/listener/interaction/MessageEmbedContextCommandListener.java b/abstracto-application/abstracto-modules/link-embed/link-embed-impl/src/main/java/dev/sheldan/abstracto/linkembed/listener/interaction/MessageEmbedContextCommandListener.java index 2480746f0..0a92d2355 100644 --- a/abstracto-application/abstracto-modules/link-embed/link-embed-impl/src/main/java/dev/sheldan/abstracto/linkembed/listener/interaction/MessageEmbedContextCommandListener.java +++ b/abstracto-application/abstracto-modules/link-embed/link-embed-impl/src/main/java/dev/sheldan/abstracto/linkembed/listener/interaction/MessageEmbedContextCommandListener.java @@ -12,6 +12,7 @@ import dev.sheldan.abstracto.core.service.MessageCache; import dev.sheldan.abstracto.core.service.management.UserInServerManagementService; import dev.sheldan.abstracto.linkembed.config.LinkEmbedFeatureDefinition; import dev.sheldan.abstracto.linkembed.service.MessageEmbedService; +import java.util.concurrent.CompletableFuture; import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; @@ -46,13 +47,27 @@ public class MessageEmbedContextCommandListener implements MessageContextCommand Message targetMessage = event.getInteraction().getTarget(); Member actor = model.getEvent().getMember(); + Long messageId = targetMessage.getIdLong(); messageCache.getMessageFromCache(targetMessage) - .thenAccept(cachedMessage -> self.embedMessage(model, actor, cachedMessage)); + .thenCompose(cachedMessage -> { + try { + return self.embedMessage(model, actor, cachedMessage); + } catch (Exception ex) { + return CompletableFuture.failedFuture(ex); + } + }) + .thenAccept(unused -> { + log.info("Finished embedding message {}.", messageId); + }) + .exceptionally(throwable -> { + log.error("Failed to embed message {}.", messageId, throwable); + return null; + }); return DefaultListenerResult.PROCESSED; } @Transactional - public void embedMessage(MessageContextInteractionModel model, Member actor, CachedMessage cachedMessage) { + public CompletableFuture embedMessage(MessageContextInteractionModel model, Member actor, CachedMessage cachedMessage) { Long userEmbeddingUserInServerId = userInServerManagementService.loadOrCreateUser(actor).getUserInServerId(); GuildMemberMessageChannel context = GuildMemberMessageChannel .builder() @@ -61,7 +76,7 @@ public class MessageEmbedContextCommandListener implements MessageContextCommand .member(actor) .guildChannel(model.getEvent().getGuildChannel()) .build(); - messageEmbedService.embedLink(cachedMessage, model.getEvent().getGuildChannel(), userEmbeddingUserInServerId, context, model.getEvent().getInteraction()); + return messageEmbedService.embedLink(cachedMessage, model.getEvent().getGuildChannel(), userEmbeddingUserInServerId, context, model.getEvent().getInteraction()); } @Override diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/pom.xml b/abstracto-application/abstracto-modules/modmail/modmail-impl/pom.xml index 560ae1727..70c908d87 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-impl/pom.xml +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/pom.xml @@ -44,6 +44,12 @@ ${project.version} + + dev.sheldan.abstracto.scheduling + scheduling-int + ${project.version} + + dev.sheldan.abstracto.core metrics-int diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/command/SetThreadPause.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/command/SetThreadPause.java new file mode 100644 index 000000000..1d87e9323 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/command/SetThreadPause.java @@ -0,0 +1,112 @@ +package dev.sheldan.abstracto.modmail.command; + +import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand; +import dev.sheldan.abstracto.core.command.condition.CommandCondition; +import dev.sheldan.abstracto.core.command.config.CommandConfiguration; +import dev.sheldan.abstracto.core.command.config.HelpInfo; +import dev.sheldan.abstracto.core.command.config.Parameter; +import dev.sheldan.abstracto.core.command.execution.CommandResult; +import dev.sheldan.abstracto.core.config.FeatureDefinition; +import dev.sheldan.abstracto.core.interaction.InteractionService; +import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig; +import dev.sheldan.abstracto.core.interaction.slash.SlashCommandPrivilegeLevels; +import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService; +import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition; +import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition; +import dev.sheldan.abstracto.modmail.config.ModMailSlashCommandNames; +import dev.sheldan.abstracto.modmail.exception.ModMailThreadClosedException; +import dev.sheldan.abstracto.modmail.model.database.ModMailThread; +import dev.sheldan.abstracto.modmail.model.database.ModMailThreadState; +import dev.sheldan.abstracto.modmail.service.ModMailThreadService; +import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + + +@Component +public class SetThreadPause extends AbstractConditionableCommand { + + private static final String SET_THREAD_PAUSE_COMMAND = "setThreadPause"; + private static final String SET_THREAD_PAUSE_RESPONSE = "setThreadPause_response"; + private static final String PAUSED_PARAMETER = "paused"; + + @Autowired + private ModMailContextCondition requiresModMailCondition; + + @Autowired + private ModMailThreadManagementService modMailThreadManagementService; + + @Autowired + private ModMailThreadService modMailThreadService; + + @Autowired + private InteractionService interactionService; + + @Autowired + private SlashCommandParameterService slashCommandParameterService; + + @Override + public CompletableFuture executeSlash(SlashCommandInteractionEvent event) { + ModMailThread modMailThread = modMailThreadManagementService.getByChannelId(event.getChannel().getIdLong()); + if(ModMailThreadState.CLOSED.equals(modMailThread.getState()) || ModMailThreadState.CLOSING.equals(modMailThread.getState())) { + throw new ModMailThreadClosedException(); + } + Boolean paused = slashCommandParameterService.getCommandOption(PAUSED_PARAMETER, event, Boolean.class); + modMailThreadService.setPauseOfThreadTo(modMailThread, paused); + return interactionService.replyEmbed(SET_THREAD_PAUSE_RESPONSE, event) + .thenApply(interactionHook -> CommandResult.fromSuccess()); + } + + @Override + public CommandConfiguration getConfiguration() { + HelpInfo helpInfo = HelpInfo + .builder() + .templated(true) + .build(); + + Parameter newStateParameter = Parameter + .builder() + .name(PAUSED_PARAMETER) + .type(Boolean.class) + .templated(true) + .build(); + + List parameters = Arrays.asList(newStateParameter); + + SlashCommandConfig slashCommandConfig = SlashCommandConfig + .builder() + .enabled(true) + .rootCommandName(ModMailSlashCommandNames.MODMAIL) + .defaultPrivilege(SlashCommandPrivilegeLevels.INVITER) + .commandName(SET_THREAD_PAUSE_COMMAND) + .build(); + + return CommandConfiguration.builder() + .name(SET_THREAD_PAUSE_COMMAND) + .slashCommandConfig(slashCommandConfig) + .module(ModMailModuleDefinition.MODMAIL) + .help(helpInfo) + .slashCommandOnly(true) + .supportsEmbedException(true) + .templated(true) + .parameters(parameters) + .causesReaction(true) + .build(); + } + + @Override + public FeatureDefinition getFeature() { + return ModMailFeatureDefinition.MOD_MAIL; + } + + @Override + public List getConditions() { + List conditions = super.getConditions(); + conditions.add(requiresModMailCondition); + return conditions; + } +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/command/SnoozeThreadReminder.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/command/SnoozeThreadReminder.java new file mode 100644 index 000000000..5955815ae --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/command/SnoozeThreadReminder.java @@ -0,0 +1,115 @@ +package dev.sheldan.abstracto.modmail.command; + +import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand; +import dev.sheldan.abstracto.core.command.condition.CommandCondition; +import dev.sheldan.abstracto.core.command.config.CommandConfiguration; +import dev.sheldan.abstracto.core.command.config.HelpInfo; +import dev.sheldan.abstracto.core.command.config.Parameter; +import dev.sheldan.abstracto.core.command.execution.CommandResult; +import dev.sheldan.abstracto.core.config.FeatureDefinition; +import dev.sheldan.abstracto.core.interaction.InteractionService; +import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig; +import dev.sheldan.abstracto.core.interaction.slash.SlashCommandPrivilegeLevels; +import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService; +import dev.sheldan.abstracto.core.utils.ParseUtils; +import dev.sheldan.abstracto.modmail.condition.ModMailContextCondition; +import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition; +import dev.sheldan.abstracto.modmail.config.ModMailSlashCommandNames; +import dev.sheldan.abstracto.modmail.exception.ModMailThreadClosedException; +import dev.sheldan.abstracto.modmail.model.database.ModMailThread; +import dev.sheldan.abstracto.modmail.model.database.ModMailThreadState; +import dev.sheldan.abstracto.modmail.service.ModMailThreadService; +import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + + +@Component +public class SnoozeThreadReminder extends AbstractConditionableCommand { + + private static final String SNOOZE_THREAD_REMINDER_COMMAND = "snoozeThreadReminder"; + private static final String SNOOZE_THREAD_REMINDER_RESPONSE = "snoozeThreadReminder_response"; + private static final String DURATION_PARAMETER = "duration"; + + @Autowired + private ModMailContextCondition requiresModMailCondition; + + @Autowired + private ModMailThreadManagementService modMailThreadManagementService; + + @Autowired + private ModMailThreadService modMailThreadService; + + @Autowired + private InteractionService interactionService; + + @Autowired + private SlashCommandParameterService slashCommandParameterService; + + @Override + public CompletableFuture executeSlash(SlashCommandInteractionEvent event) { + ModMailThread modMailThread = modMailThreadManagementService.getByChannelId(event.getChannel().getIdLong()); + if(ModMailThreadState.CLOSED.equals(modMailThread.getState()) || ModMailThreadState.CLOSING.equals(modMailThread.getState())) { + throw new ModMailThreadClosedException(); + } + String durationString = slashCommandParameterService.getCommandOption(DURATION_PARAMETER, event, Duration.class, String.class); + Duration duration = ParseUtils.parseDuration(durationString); + modMailThreadService.snoozeThreadReminder(modMailThread, duration); + return interactionService.replyEmbed(SNOOZE_THREAD_REMINDER_RESPONSE, event) + .thenApply(interactionHook -> CommandResult.fromSuccess()); + } + + @Override + public CommandConfiguration getConfiguration() { + HelpInfo helpInfo = HelpInfo + .builder() + .templated(true) + .build(); + + Parameter durationParameter = Parameter + .builder() + .name(DURATION_PARAMETER) + .type(Duration.class) + .templated(true) + .build(); + + List parameters = Arrays.asList(durationParameter); + + SlashCommandConfig slashCommandConfig = SlashCommandConfig + .builder() + .enabled(true) + .rootCommandName(ModMailSlashCommandNames.MODMAIL) + .defaultPrivilege(SlashCommandPrivilegeLevels.INVITER) + .commandName(SNOOZE_THREAD_REMINDER_COMMAND) + .build(); + + return CommandConfiguration.builder() + .name(SNOOZE_THREAD_REMINDER_COMMAND) + .slashCommandConfig(slashCommandConfig) + .module(ModMailModuleDefinition.MODMAIL) + .help(helpInfo) + .slashCommandOnly(true) + .supportsEmbedException(true) + .templated(true) + .parameters(parameters) + .causesReaction(true) + .build(); + } + + @Override + public FeatureDefinition getFeature() { + return ModMailFeatureDefinition.MOD_MAIL; + } + + @Override + public List getConditions() { + List conditions = super.getConditions(); + conditions.add(requiresModMailCondition); + return conditions; + } +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/job/ModmailThreadActionJob.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/job/ModmailThreadActionJob.java new file mode 100644 index 000000000..af4b7ca7a --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/job/ModmailThreadActionJob.java @@ -0,0 +1,32 @@ +package dev.sheldan.abstracto.modmail.job; + +import dev.sheldan.abstracto.modmail.service.ModMailThreadServiceBean; +import lombok.extern.slf4j.Slf4j; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.PersistJobDataAfterExecution; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.quartz.QuartzJobBean; +import org.springframework.stereotype.Component; + +@Slf4j +@DisallowConcurrentExecution +@Component +@PersistJobDataAfterExecution +public class ModmailThreadActionJob extends QuartzJobBean { + + @Autowired + private ModMailThreadServiceBean modMailThreadServiceBean; + + @Override + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + try { + log.info("Check modmail threads to perform action for."); + modMailThreadServiceBean.checkModmailActionsForNeededActions(); + } catch (Exception exception) { + log.error("Modmail thread action job failed.", exception); + } + + } +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailAutoCloseListener.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailAutoCloseListener.java new file mode 100644 index 000000000..c0667bb69 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailAutoCloseListener.java @@ -0,0 +1,117 @@ +package dev.sheldan.abstracto.modmail.listener; + +import dev.sheldan.abstracto.core.config.ListenerPriority; +import dev.sheldan.abstracto.core.service.ConfigService; +import dev.sheldan.abstracto.core.service.FeatureModeService; +import dev.sheldan.abstracto.core.service.GuildService; +import dev.sheldan.abstracto.core.templating.service.TemplateService; +import dev.sheldan.abstracto.core.utils.ParseUtils; +import dev.sheldan.abstracto.modmail.config.ModMailFeatureConfig; +import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition; +import dev.sheldan.abstracto.modmail.config.ModMailMode; +import dev.sheldan.abstracto.modmail.model.ClosingContext; +import dev.sheldan.abstracto.modmail.model.database.ModMailThread; +import dev.sheldan.abstracto.modmail.model.database.ModMailThreadState; +import dev.sheldan.abstracto.modmail.model.listener.ModmailThreadActionListenerModel; +import dev.sheldan.abstracto.modmail.service.ModMailThreadService; +import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.entities.Guild; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class ModmailAutoCloseListener implements ModmailThreadActionListener { + + @Autowired + private ModMailThreadManagementService modMailThreadManagementService; + + @Autowired + private ConfigService configService; + + @Autowired + private FeatureModeService featureModeService; + + @Autowired + private ModMailThreadService modMailThreadService; + + @Autowired + private ModmailReminderListener self; + + @Autowired + private GuildService guildService; + + @Autowired + private TemplateService templateService; + + private static final String AUTO_CLOSE_NOTE_TEMPLATE_KEY = "modmail_auto_closing_note_text"; + + @Override + public Integer getPriority() { + return ListenerPriority.HIGH; + } + + @Override + public ModmailThreadActionListenerResult execute(ModmailThreadActionListenerModel model) { + ModmailThreadActionListenerResult result; + if(!featureModeService.featureModeActive(ModMailFeatureDefinition.MOD_MAIL, model.getServerId(), ModMailMode.THREAD_AUTO_CLOSE)) { + result = ModmailThreadActionListenerResult.IGNORED; + } else { + String closeDuration = configService.getStringValueOrConfigDefault(ModMailFeatureConfig.MOD_MAIL_AUTO_CLOSE_DURATION, model.getServerId()); + Duration duration = ParseUtils.parseDuration(closeDuration); + Instant timeInPastDuration = Instant.now().minus(duration); + ModMailThread thread = modMailThreadManagementService.getById(model.getThreadId()); + if(thread.getState() == ModMailThreadState.PAUSED) { + log.info("Thread {} is paused - not closing.", thread.getId()); + return ModmailThreadActionListenerResult.IGNORED; + } + Instant timeStampToConsider = getTimeStampToConsider(thread); + boolean mustBeClosed = timeInPastDuration.isAfter(timeStampToConsider); + if (mustBeClosed) { + closeThread(thread) + .thenAccept(unused -> { + self.updateSnoozeTimer(model.getThreadId(), duration); + log.info("Automatically closed thread {}", model.getThreadId()); + }) + .exceptionally(throwable -> { + log.warn("Failed to automatically close thread {}.", model.getThreadId(), throwable); + return null; + }); + result = ModmailThreadActionListenerResult.FINAL; + } else { + result = ModmailThreadActionListenerResult.IGNORED; + } + } + + return result; + } + + private static Instant getTimeStampToConsider(ModMailThread thread) { + if(thread.getUpdated() != null) { + return thread.getUpdated(); + } + return thread.getCreated(); + } + + private CompletableFuture closeThread(ModMailThread modMailThread) { + Guild guild = guildService.getGuildById(modMailThread.getServer().getId()); + if(guild != null) { + String closingNote = templateService.renderTemplate(AUTO_CLOSE_NOTE_TEMPLATE_KEY, new Object(), modMailThread.getServer().getId()); + ClosingContext closingContext = ClosingContext + .builder() + .notifyUser(true) + .log(true) + .closingMember(guild.getSelfMember()) + .note(closingNote) + .build(); + return modMailThreadService.closeModMailThreadEvaluateLogging(modMailThread, closingContext, new ArrayList<>()); + } else { + return CompletableFuture.completedFuture(null); + } + } +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailReminderListener.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailReminderListener.java new file mode 100644 index 000000000..28ba4f947 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailReminderListener.java @@ -0,0 +1,144 @@ +package dev.sheldan.abstracto.modmail.listener; + +import dev.sheldan.abstracto.core.config.ListenerPriority; +import dev.sheldan.abstracto.core.models.template.display.MemberDisplay; +import dev.sheldan.abstracto.core.models.template.display.RoleDisplay; +import dev.sheldan.abstracto.core.service.ChannelService; +import dev.sheldan.abstracto.core.service.ConfigService; +import dev.sheldan.abstracto.core.service.FeatureModeService; +import dev.sheldan.abstracto.core.templating.model.MessageToSend; +import dev.sheldan.abstracto.core.templating.service.TemplateService; +import dev.sheldan.abstracto.core.utils.FutureUtils; +import dev.sheldan.abstracto.core.utils.ParseUtils; +import dev.sheldan.abstracto.modmail.config.ModMailFeatureConfig; +import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition; +import dev.sheldan.abstracto.modmail.config.ModMailMode; +import dev.sheldan.abstracto.modmail.model.database.ModMailRole; +import dev.sheldan.abstracto.modmail.model.database.ModMailThread; +import dev.sheldan.abstracto.modmail.model.database.ModMailThreadState; +import dev.sheldan.abstracto.modmail.model.listener.ModmailThreadActionListenerModel; +import dev.sheldan.abstracto.modmail.model.template.ModmailThreadReminderModel; +import dev.sheldan.abstracto.modmail.service.management.ModMailRoleManagementService; +import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Slf4j +public class ModmailReminderListener implements ModmailThreadActionListener { + + @Autowired + private ChannelService channelService; + + @Autowired + private TemplateService templateService; + + @Autowired + private ModMailThreadManagementService modMailThreadManagementService; + + @Autowired + private ConfigService configService; + + @Autowired + private FeatureModeService featureModeService; + + @Autowired + private ModMailRoleManagementService modMailRoleManagementService; + + @Autowired + private ModmailReminderListener self; + + private static final String MODMAIL_THREAD_REMINDER_TEMPLATE_KEY = "modmail_thread_reminder_notification"; + + @Override + public Integer getPriority() { + return ListenerPriority.MEDIUM; + } + + @Override + public ModmailThreadActionListenerResult execute(ModmailThreadActionListenerModel model) { + ModmailThreadActionListenerResult result; + if(!featureModeService.featureModeActive(ModMailFeatureDefinition.MOD_MAIL, model.getServerId(), ModMailMode.THREAD_REMINDER)) { + result = ModmailThreadActionListenerResult.IGNORED; + } else { + String reminderDurationString = configService.getStringValueOrConfigDefault(ModMailFeatureConfig.MOD_MAIL_REMINDER_DURATION, model.getServerId()); + Duration duration = ParseUtils.parseDuration(reminderDurationString); + Instant timeInPastDuration = Instant.now().minus(duration); + ModMailThread thread = modMailThreadManagementService.getById(model.getThreadId()); + if(List.of(ModMailThreadState.CLOSED, ModMailThreadState.CLOSING).contains(thread.getState())) { + log.debug("Thread {} is closed - ignoring.", model.getThreadId()); + return ModmailThreadActionListenerResult.IGNORED; + } + Instant timeStampToConsider = getTimestampToUse(thread); + boolean mustBeReminded = timeInPastDuration.isAfter(timeStampToConsider); + if (mustBeReminded) { + sendReminder(thread) + .thenAccept(unused -> { + self.updateSnoozeTimer(model.getThreadId(), duration); + log.info("Sent reminder about thread {}", model.getThreadId()); + }) + .exceptionally(throwable -> { + log.warn("Failed to send reminder about thread {}.", model.getThreadId(), throwable); + return null; + }); + result = ModmailThreadActionListenerResult.PROCESSED; + } else { + result = ModmailThreadActionListenerResult.IGNORED; + } + } + + return result; + } + + + private static Instant getTimestampToUse(ModMailThread thread) { + if (thread.getRemindersSnoozedUntil() != null) { + return thread.getRemindersSnoozedUntil(); + } + return getUpdatedOrCrated(thread); + } + + private static Instant getUpdatedOrCrated(ModMailThread thread) { + if(thread.getUpdated() != null) { + return thread.getUpdated(); + } + return thread.getCreated(); + } + + private CompletableFuture sendReminder(ModMailThread modMailThread) { + List modmailRolesToPing = modMailRoleManagementService.getRolesForServer(modMailThread.getServer()); + List rolesToDisplay = modmailRolesToPing.stream().map(role -> RoleDisplay.fromARole(role.getRole())).toList(); + Instant autoCloseInstant; + if(featureModeService.featureModeActive(ModMailFeatureDefinition.MOD_MAIL, modMailThread.getServer().getId(), ModMailMode.THREAD_AUTO_CLOSE) + && !modMailThread.getState().equals(ModMailThreadState.PAUSED)) { + String closeDurationString = configService.getStringValueOrConfigDefault(ModMailFeatureConfig.MOD_MAIL_AUTO_CLOSE_DURATION, modMailThread.getServer().getId()); + Duration autoCloseDuration = ParseUtils.parseDuration(closeDurationString); + autoCloseInstant = getUpdatedOrCrated(modMailThread).plus(autoCloseDuration); + } else { + autoCloseInstant = null; + } + ModmailThreadReminderModel model = ModmailThreadReminderModel + .builder() + .updated(getUpdatedOrCrated(modMailThread)) + .created(modMailThread.getCreated()) + .paused(modMailThread.getState().equals(ModMailThreadState.PAUSED)) + .autoCloseInstant(autoCloseInstant) + .pingRoles(rolesToDisplay) + .memberDisplay(MemberDisplay.fromAUserInAServer(modMailThread.getUser())) + .build(); + MessageToSend messageToSend = templateService.renderEmbedTemplate(MODMAIL_THREAD_REMINDER_TEMPLATE_KEY, model, modMailThread.getServer().getId()); + return FutureUtils.toSingleFutureGeneric(channelService.sendMessageEmbedToSendToAChannel(messageToSend, modMailThread.getChannel())); + } + + @Transactional + public void updateSnoozeTimer(long modmailThreadId, Duration duration) { + ModMailThread thread = modMailThreadManagementService.getById(modmailThreadId); + thread.setRemindersSnoozedUntil(Instant.now().plus(duration)); + } +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/repository/ModMailThreadRepository.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/repository/ModMailThreadRepository.java index 29314d51a..2c1c9badf 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/repository/ModMailThreadRepository.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/repository/ModMailThreadRepository.java @@ -36,6 +36,7 @@ public interface ModMailThreadRepository extends JpaRepository findByUserAndState(AUserInAServer userInAServer, ModMailThreadState state); + List findByStateNot(ModMailThreadState state); @Override Optional findById(@NonNull Long aLong); diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/service/ModMailThreadServiceBean.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/service/ModMailThreadServiceBean.java index 450faf33b..749f08bc6 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/service/ModMailThreadServiceBean.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/service/ModMailThreadServiceBean.java @@ -15,6 +15,7 @@ import dev.sheldan.abstracto.core.service.*; import dev.sheldan.abstracto.core.service.management.ChannelManagementService; import dev.sheldan.abstracto.core.service.management.ServerManagementService; import dev.sheldan.abstracto.core.service.management.UserInServerManagementService; +import dev.sheldan.abstracto.core.utils.BeanUtils; import dev.sheldan.abstracto.core.utils.CompletableFutureList; import dev.sheldan.abstracto.core.utils.FutureUtils; import dev.sheldan.abstracto.core.utils.SnowflakeUtils; @@ -26,9 +27,11 @@ import dev.sheldan.abstracto.modmail.config.ModMailPostTargets; import dev.sheldan.abstracto.modmail.exception.ModMailCategoryIdException; import dev.sheldan.abstracto.modmail.exception.ModMailThreadChannelNotFound; import dev.sheldan.abstracto.modmail.exception.ModMailThreadNotFoundException; +import dev.sheldan.abstracto.modmail.listener.ModmailThreadActionListener; import dev.sheldan.abstracto.modmail.model.ClosingContext; import dev.sheldan.abstracto.modmail.model.dto.ServiceChoicesPayload; import dev.sheldan.abstracto.modmail.model.database.*; +import dev.sheldan.abstracto.modmail.model.listener.ModmailThreadActionListenerModel; import dev.sheldan.abstracto.modmail.model.listener.ModmailThreadCreatedSendMessageModel; import dev.sheldan.abstracto.modmail.model.template.*; import dev.sheldan.abstracto.modmail.service.management.ModMailMessageManagementService; @@ -166,6 +169,9 @@ public class ModMailThreadServiceBean implements ModMailThreadService { @Autowired private ApplicationEventPublisher eventPublisher; + @Autowired + private List threadActionListeners; + public static final String MODMAIL_THREAD_METRIC = "modmail.threads"; public static final String MODMAIL_MESSAGE_METRIC = "modmail.messges"; public static final String ACTION = "action"; @@ -269,6 +275,38 @@ public class ModMailThreadServiceBean implements ModMailThreadService { return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(MODMAIL_THREAD_CREATED_TEMPLATE_KEY, model, interactionHook)); } + @Transactional + public void checkModmailActionsForNeededActions() { + List allOpenThreads = modMailThreadManagementService.getAllOpenThreads(); + allOpenThreads.forEach(thread -> { + ModmailThreadActionListenerModel model = ModmailThreadActionListenerModel + .builder() + .threadId(thread.getId()) + .state(thread.getState()) + .appeal(thread.getAppeal()) + .serverId(thread.getServer().getId()) + .serverUser(ServerUser.fromAUserInAServer(thread.getUser())) + .messageCount(thread.getMessages() != null ? thread.getMessages().size() : 0) + .updated(thread.getUpdated()) + .created(thread.getCreated()) + .subscriberCount(thread.getSubscribers() != null ? thread.getSubscribers().size() : 0) + .build(); + for (ModmailThreadActionListener modmailThreadActionListener : threadActionListeners) { + try { + log.info("Executing action {} for thread {}.", modmailThreadActionListener.getClass().getSimpleName(), model.getThreadId()); + ModmailThreadActionListener.ModmailThreadActionListenerResult result = modmailThreadActionListener.execute(model); + if(ModmailThreadActionListener.ModmailThreadActionListenerResult.FINAL == result) { + log.info("Listener {} terminated for thread {}.", modmailThreadActionListener.getClass().getSimpleName(), model.getThreadId()); + break; + } + } catch (Exception exception) { + log.error("Action failed to execute.", exception); + } + + } + }); + } + /** * 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. @@ -808,6 +846,27 @@ public class ModMailThreadServiceBean implements ModMailThreadService { return isModMailThread(channel); } + @Override + public void snoozeThreadReminder(ModMailThread thread, Duration snoozeDuration) { + Instant snoozeTargetDate = Instant.now().plus(snoozeDuration); + log.info("Snoozing Thread {} until {}.", thread.getId(), snoozeTargetDate); + thread.setRemindersSnoozedUntil(snoozeTargetDate); + } + + @Override + public void setPauseOfThreadTo(ModMailThread thread, boolean paused) { + if(thread.getState().equals(ModMailThreadState.PAUSED) && !paused) { + thread.setState(thread.getPreviousState()); + thread.setPreviousState(null); + } + if(!thread.getState().equals(ModMailThreadState.PAUSED) && paused) { + thread.setPreviousState(thread.getState()); + thread.setState(ModMailThreadState.PAUSED); + } + + log.info("Thread {} has state {} and previous state {}.", thread.getId(), thread.getState(), thread.getPreviousState()); + } + /** * 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. @@ -1127,5 +1186,6 @@ public class ModMailThreadServiceBean implements ModMailThreadService { metricService.registerCounter(MODMAIL_THREAD_CLOSED_COUNTER, "Mod mail threads closed"); metricService.registerCounter(MDOMAIL_THREAD_MESSAGE_RECEIVED, "Mod mail messages received"); metricService.registerCounter(MDOMAIL_THREAD_MESSAGE_SENT, "Mod mail messages sent"); + BeanUtils.sortPrioritizedListeners(threadActionListeners); } } diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/service/management/ModMailThreadManagementServiceBean.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/service/management/ModMailThreadManagementServiceBean.java index 3970f4fd6..c01310ed5 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/service/management/ModMailThreadManagementServiceBean.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/java/dev/sheldan/abstracto/modmail/service/management/ModMailThreadManagementServiceBean.java @@ -89,6 +89,11 @@ public class ModMailThreadManagementServiceBean implements ModMailThreadManageme return modMailThreadRepository.findByUser(aUserInAServer); } + @Override + public List getAllOpenThreads() { + return modMailThreadRepository.findByStateNot(ModMailThreadState.CLOSED); + } + @Override public ModMailThread getLatestModMailThread(AUserInAServer aUserInAServer) { return modMailThreadRepository.findTopByUserOrderByClosedDesc(aUserInAServer); @@ -123,7 +128,11 @@ public class ModMailThreadManagementServiceBean implements ModMailThreadManageme @Override public void setModMailThreadState(ModMailThread modMailThread, ModMailThreadState newState) { - modMailThread.setState(newState); + if(modMailThread.getState().equals(ModMailThreadState.PAUSED)) { + modMailThread.setPreviousState(newState); + } else { + modMailThread.setState(newState); + } modMailThread.setUpdated(Instant.now()); modMailThreadRepository.save(modMailThread); } diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/collection.xml b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/collection.xml new file mode 100644 index 000000000..9b55885aa --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/collection.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/command.xml b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/command.xml new file mode 100644 index 000000000..66a73da98 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/command.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/data.xml b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/data.xml new file mode 100644 index 000000000..ee68e601b --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/data.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/modmail_action_job.xml b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/modmail_action_job.xml new file mode 100644 index 000000000..173c549c1 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/seedData/modmail_action_job.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/tables/modmail_thread.xml b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/tables/modmail_thread.xml new file mode 100644 index 000000000..d3f6abc48 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/tables/modmail_thread.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/tables/tables.xml b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/tables/tables.xml new file mode 100644 index 000000000..dfe80c149 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/1.6.22/tables/tables.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/modMail-changeLog.xml b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/modMail-changeLog.xml index 957b7175c..18a4ff458 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/modMail-changeLog.xml +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/migrations/modMail-changeLog.xml @@ -5,4 +5,5 @@ + \ No newline at end of file diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/modmail-config.properties b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/modmail-config.properties index ba76cda53..482db26d9 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/modmail-config.properties +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/main/resources/modmail-config.properties @@ -4,6 +4,12 @@ abstracto.systemConfigs.modMailClosingText.stringValue=Thread has been closed. abstracto.systemConfigs.modmailCategory.name=modmailCategory abstracto.systemConfigs.modmailCategory.longValue=0 +abstracto.systemConfigs.modMailReminderDuration.name=modMailReminderDuration +abstracto.systemConfigs.modMailReminderDuration.stringValue=3d + +abstracto.systemConfigs.modMailAutoCloseDuration.name=modMailAutoCloseDuration +abstracto.systemConfigs.modMailAutoCloseDuration.stringValue=14d + abstracto.featureFlags.modmail.featureName=modmail abstracto.featureFlags.modmail.enabled=false @@ -28,4 +34,12 @@ abstracto.featureModes.modMailAppeals.mode=modMailAppeals abstracto.featureModes.modMailAppeals.enabled=false abstracto.systemConfigs.modMailAppealServer.name=modMailAppealServer -abstracto.systemConfigs.modMailAppealServer.longValue=0 \ No newline at end of file +abstracto.systemConfigs.modMailAppealServer.longValue=0 + +abstracto.featureModes.threadReminder.featureName=modmail +abstracto.featureModes.threadReminder.mode=threadReminder +abstracto.featureModes.threadReminder.enabled=false + +abstracto.featureModes.threadAutoClose.featureName=modmail +abstracto.featureModes.threadAutoClose.mode=threadAutoClose +abstracto.featureModes.threadAutoClose.enabled=false \ No newline at end of file diff --git a/abstracto-application/abstracto-modules/modmail/modmail-impl/src/test/java/dev/sheldan/abstracto/modmail/listener/ModmailReminderListenerTest.java b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/test/java/dev/sheldan/abstracto/modmail/listener/ModmailReminderListenerTest.java new file mode 100644 index 000000000..bedfc1e47 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-impl/src/test/java/dev/sheldan/abstracto/modmail/listener/ModmailReminderListenerTest.java @@ -0,0 +1,134 @@ +package dev.sheldan.abstracto.modmail.listener; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.sheldan.abstracto.core.models.database.AServer; +import dev.sheldan.abstracto.core.models.database.AUser; +import dev.sheldan.abstracto.core.models.database.AUserInAServer; +import dev.sheldan.abstracto.core.service.ChannelService; +import dev.sheldan.abstracto.core.service.ConfigService; +import dev.sheldan.abstracto.core.service.FeatureModeService; +import dev.sheldan.abstracto.core.templating.model.MessageToSend; +import dev.sheldan.abstracto.core.templating.service.TemplateService; +import dev.sheldan.abstracto.modmail.config.ModMailFeatureConfig; +import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition; +import dev.sheldan.abstracto.modmail.config.ModMailMode; +import dev.sheldan.abstracto.modmail.model.database.ModMailThread; +import dev.sheldan.abstracto.modmail.model.listener.ModmailThreadActionListenerModel; +import dev.sheldan.abstracto.modmail.service.management.ModMailRoleManagementService; +import dev.sheldan.abstracto.modmail.service.management.ModMailThreadManagementService; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ModmailReminderListenerTest { + @InjectMocks + private ModmailReminderListener unitUnderTest; + + @Mock + private ChannelService channelService; + + @Mock + private TemplateService templateService; + + @Mock + private ModMailThreadManagementService modMailThreadManagementService; + + @Mock + private ConfigService configService; + + @Mock + private FeatureModeService featureModeService; + + @Mock + private ModMailRoleManagementService modMailRoleManagementService; + + private static final long SERVER_ID = 1L; + private static final long THREAD_ID = 2L; + private static final long USER_ID = 3L; + + @Before + public void setup() { + when(featureModeService.featureModeActive(ModMailFeatureDefinition.MOD_MAIL, SERVER_ID, ModMailMode.THREAD_REMINDER)).thenReturn(true); + when(configService.getStringValueOrConfigDefault(ModMailFeatureConfig.MOD_MAIL_REMINDER_DURATION, SERVER_ID)).thenReturn("5m"); + MessageToSend messageToSend = MessageToSend + .builder() + .build(); + when(templateService.renderEmbedTemplate(anyString(), any(), any())).thenReturn(messageToSend); + when(modMailRoleManagementService.getRolesForServer(any())).thenReturn(List.of()); + } + + @Test + public void executeInitialReminder() { + Instant updatedTimeStamp = Instant.now().minus(10, ChronoUnit.MINUTES); + Instant snoozedUntil = Instant.now().minus(1, ChronoUnit.SECONDS); + executeTest(updatedTimeStamp, snoozedUntil, true); + } + + @Test + public void shouldNotExecuteAfterSnoozing() { + Instant updatedTimeStamp = Instant.now().minus(10, ChronoUnit.MINUTES); + Instant snoozedUntil = Instant.now().plus(5, ChronoUnit.MINUTES); + executeTest(updatedTimeStamp, snoozedUntil, false); + } + + @Test + public void shouldExecuteAfterSnoozingButSnoozingHasPassed() { + Instant updatedTimeStamp = Instant.now().minus(25, ChronoUnit.MINUTES); + Instant snoozedUntil = Instant.now().minus(2, ChronoUnit.MINUTES); + executeTest(updatedTimeStamp, snoozedUntil, true); + } + + private void executeTest(Instant updatedTimeStamp, Instant snoozedUntil, boolean shouldExecute) { + ModMailThread thread = Mockito.mock(ModMailThread.class); + when(thread.getUpdated()).thenReturn(updatedTimeStamp); + when(thread.getRemindersSnoozedUntil()).thenReturn(snoozedUntil); + AUser user = AUser + .builder() + .id(USER_ID) + .build(); + AServer server = AServer + .builder() + .id(SERVER_ID) + .build(); + AUserInAServer aUserInAServer = AUserInAServer + .builder() + .serverReference(server) + .userReference(user) + .userInServerId(USER_ID) + .build(); + when(thread.getServer()).thenReturn(server); + when(thread.getUser()).thenReturn(aUserInAServer); + when(modMailThreadManagementService.getById(THREAD_ID)).thenReturn(thread); + + ModmailThreadActionListenerModel model = getModel(); + + unitUnderTest.execute(model); + if(shouldExecute) { + verify(channelService).sendMessageEmbedToSendToAChannel(any(), any()); + } else { + verify(channelService, times(0)).sendMessageEmbedToSendToAChannel(any(), any()); + } + } + + private static ModmailThreadActionListenerModel getModel() { + return ModmailThreadActionListenerModel + .builder() + .serverId(SERVER_ID) + .threadId(THREAD_ID) + .build(); + } +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/config/ModMailFeatureConfig.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/config/ModMailFeatureConfig.java index e66d6742a..d56c12e0d 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/config/ModMailFeatureConfig.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/config/ModMailFeatureConfig.java @@ -22,6 +22,8 @@ public class ModMailFeatureConfig implements FeatureConfig { public static final String MOD_MAIL_CLOSING_TEXT_SYSTEM_CONFIG_KEY = "modMailClosingText"; public static final String MOD_MAIL_APPEAL_SERVER = "modMailAppealServer"; + public static final String MOD_MAIL_REMINDER_DURATION = "modMailReminderDuration"; + public static final String MOD_MAIL_AUTO_CLOSE_DURATION = "modMailAutoCloseDuration"; @Autowired private ModMailFeatureValidator modMailFeatureValidator; @@ -55,13 +57,18 @@ public class ModMailFeatureConfig implements FeatureConfig { return List.of(ModMailMode.LOGGING, ModMailMode.SEPARATE_MESSAGE, ModMailMode.THREAD_CONTAINER, - ModMailMode.MOD_MAIL_APPEALS + ModMailMode.MOD_MAIL_APPEALS, + ModMailMode.THREAD_AUTO_CLOSE, + ModMailMode.THREAD_REMINDER ); } @Override public List getRequiredSystemConfigKeys() { - return List.of(MOD_MAIL_CLOSING_TEXT_SYSTEM_CONFIG_KEY, MOD_MAIL_APPEAL_SERVER); + return List.of(MOD_MAIL_CLOSING_TEXT_SYSTEM_CONFIG_KEY, + MOD_MAIL_APPEAL_SERVER, + MOD_MAIL_REMINDER_DURATION, + MOD_MAIL_AUTO_CLOSE_DURATION); } @Override diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/config/ModMailMode.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/config/ModMailMode.java index 4d70cdea0..4f20e7baf 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/config/ModMailMode.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/config/ModMailMode.java @@ -12,6 +12,8 @@ public enum ModMailMode implements FeatureMode { LOGGING("log"), SEPARATE_MESSAGE("threadMessage"), THREAD_CONTAINER("threadContainer"), + THREAD_REMINDER("threadReminder"), + THREAD_AUTO_CLOSE("threadAutoClose"), MOD_MAIL_APPEALS("modMailAppeals"); private final String key; diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailThreadActionListener.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailThreadActionListener.java new file mode 100644 index 000000000..ff93e2433 --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/listener/ModmailThreadActionListener.java @@ -0,0 +1,14 @@ +package dev.sheldan.abstracto.modmail.listener; + +import dev.sheldan.abstracto.core.Prioritized; +import dev.sheldan.abstracto.core.listener.AbstractoListener; +import dev.sheldan.abstracto.core.listener.ListenerExecutionResult; +import dev.sheldan.abstracto.modmail.model.listener.ModmailThreadActionListenerModel; + +public interface ModmailThreadActionListener extends + AbstractoListener, Prioritized { + + enum ModmailThreadActionListenerResult implements ListenerExecutionResult { + FINAL, IGNORED, PROCESSED + } +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/database/ModMailThread.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/database/ModMailThread.java index f5a81f5a0..c93fb0aef 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/database/ModMailThread.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/database/ModMailThread.java @@ -59,6 +59,9 @@ public class ModMailThread implements Serializable { @Column(name = "closed") private Instant closed; + @Column(name = "reminders_snoozed_until") + private Instant remindersSnoozedUntil; + @Column(name = "appeal", nullable = false) private Boolean appeal; @@ -92,4 +95,8 @@ public class ModMailThread implements Serializable { @Column(name = "state") private ModMailThreadState state; + @Enumerated(EnumType.STRING) + @Column(name = "previous_state") + private ModMailThreadState previousState; + } diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/database/ModMailThreadState.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/database/ModMailThreadState.java index f9e588568..435691dce 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/database/ModMailThreadState.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/database/ModMailThreadState.java @@ -13,6 +13,10 @@ public enum ModMailThreadState { * Staff member responded to the mod mail thread */ MOD_REPLIED, + /** + * Staff member paused the thread to not be closed + */ + PAUSED, /** * The thread was closed by a staff member and the channel was removed */ diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/listener/ModmailThreadActionListenerModel.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/listener/ModmailThreadActionListenerModel.java new file mode 100644 index 000000000..3f78e4b5a --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/listener/ModmailThreadActionListenerModel.java @@ -0,0 +1,27 @@ +package dev.sheldan.abstracto.modmail.model.listener; + +import dev.sheldan.abstracto.core.listener.ListenerModel; +import dev.sheldan.abstracto.core.models.ServerUser; +import dev.sheldan.abstracto.modmail.model.database.ModMailThreadState; +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class ModmailThreadActionListenerModel implements ListenerModel { + private long threadId; + private ServerUser serverUser; + private long serverId; + private ModMailThreadState state; + private Boolean appeal; + private Instant created; + private Instant updated; + private long messageCount; + private long subscriberCount; + private long channelId; + + +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/template/ModmailThreadReminderModel.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/template/ModmailThreadReminderModel.java new file mode 100644 index 000000000..b916afc5e --- /dev/null +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/model/template/ModmailThreadReminderModel.java @@ -0,0 +1,21 @@ +package dev.sheldan.abstracto.modmail.model.template; + +import dev.sheldan.abstracto.core.models.template.display.MemberDisplay; +import dev.sheldan.abstracto.core.models.template.display.RoleDisplay; +import java.time.Instant; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class ModmailThreadReminderModel { + private List pingRoles; + private MemberDisplay memberDisplay; + private Instant autoCloseInstant; + private boolean paused; + private Instant created; + private Instant updated; +} diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/service/ModMailThreadService.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/service/ModMailThreadService.java index 69f581b93..239423462 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/service/ModMailThreadService.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/service/ModMailThreadService.java @@ -8,6 +8,7 @@ import dev.sheldan.abstracto.core.models.database.AUserInAServer; import dev.sheldan.abstracto.core.templating.model.MessageToSend; import dev.sheldan.abstracto.modmail.model.ClosingContext; import dev.sheldan.abstracto.modmail.model.database.ModMailThread; +import java.time.Duration; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.interactions.InteractionHook; @@ -103,6 +104,8 @@ public interface ModMailThreadService { boolean isModMailThread(AChannel channel); boolean isModMailThread(Long channelId); + void snoozeThreadReminder(ModMailThread thread, Duration snoozeDuration); + void setPauseOfThreadTo(ModMailThread thread, boolean paused); CompletableFuture rejectAppeal(ModMailThread modMailThread, String reason, Member memberPerforming); } diff --git a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/service/management/ModMailThreadManagementService.java b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/service/management/ModMailThreadManagementService.java index 0dff1dad0..a2a85e00d 100644 --- a/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/service/management/ModMailThreadManagementService.java +++ b/abstracto-application/abstracto-modules/modmail/modmail-int/src/main/java/dev/sheldan/abstracto/modmail/service/management/ModMailThreadManagementService.java @@ -91,6 +91,7 @@ public interface ModMailThreadManagementService { * @return A list of {@link ModMailThread} which contains all the current mod mail threads for the member, should be at most one */ List getModMailThreadForUser(AUserInAServer aUserInAServer); + List getAllOpenThreads(); /** * Retrieves the *latest* {@link ModMailThread} of the {@link AUserInAServer}, which means, the latest thread which is in the state diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/InteractionServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/InteractionServiceBean.java index 46c1c793f..674ca8b5b 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/InteractionServiceBean.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/InteractionServiceBean.java @@ -67,72 +67,79 @@ public class InteractionServiceBean implements InteractionService { @Override public List> sendMessageToInteraction(MessageToSend messageToSend, InteractionHook interactionHook) { List> futures = new ArrayList<>(); - List> allMessageActions = new ArrayList<>(); - int iterations = Math.min(messageToSend.getMessages().size(), messageToSend.getEmbeds().size()); - for (int i = 0; i < iterations; i++) { - metricService.incrementCounter(MESSAGE_SEND_METRIC); - String text = messageToSend.getMessages().get(i); - MessageEmbed embed = messageToSend.getEmbeds().get(i); - WebhookMessageCreateAction messageAction = interactionHook.sendMessage(text).addEmbeds(embed); - allMessageActions.add(messageAction); - } - // one of these loops will get additional iterations, if the number is different, not both - for (int i = iterations; i < messageToSend.getMessages().size(); i++) { - metricService.incrementCounter(MESSAGE_SEND_METRIC); - String text = messageToSend.getMessages().get(i); - WebhookMessageCreateAction messageAction = interactionHook.sendMessage(text); - allMessageActions.add(messageAction); - } - for (int i = iterations; i < messageToSend.getEmbeds().size(); i++) { - metricService.incrementCounter(MESSAGE_SEND_METRIC); - MessageEmbed embed = messageToSend.getEmbeds().get(i); - WebhookMessageCreateAction messageAction = interactionHook.sendMessageEmbeds(embed); - allMessageActions.add(messageAction); - } + if(messageToSend.getUseComponentsV2()) { + WebhookMessageEditAction action = interactionHook.editOriginalComponents(messageToSend.getComponents()).useComponentsV2(); + if(messageToSend.getEphemeral() != null) { + interactionHook.setEphemeral(messageToSend.getEphemeral()); + } + return List.of(action.submit()); + } else { + List> allMessageActions = new ArrayList<>(); + int iterations = Math.min(messageToSend.getMessages().size(), messageToSend.getEmbeds().size()); + for (int i = 0; i < iterations; i++) { + metricService.incrementCounter(MESSAGE_SEND_METRIC); + String text = messageToSend.getMessages().get(i); + MessageEmbed embed = messageToSend.getEmbeds().get(i); + WebhookMessageCreateAction messageAction = interactionHook.sendMessage(text).addEmbeds(embed); + allMessageActions.add(messageAction); + } + // one of these loops will get additional iterations, if the number is different, not both + for (int i = iterations; i < messageToSend.getMessages().size(); i++) { + metricService.incrementCounter(MESSAGE_SEND_METRIC); + String text = messageToSend.getMessages().get(i); + WebhookMessageCreateAction messageAction = interactionHook.sendMessage(text); + allMessageActions.add(messageAction); + } + for (int i = iterations; i < messageToSend.getEmbeds().size(); i++) { + metricService.incrementCounter(MESSAGE_SEND_METRIC); + MessageEmbed embed = messageToSend.getEmbeds().get(i); + WebhookMessageCreateAction messageAction = interactionHook.sendMessageEmbeds(embed); + allMessageActions.add(messageAction); + } - List actionRows = messageToSend.getActionRows(); - if(!actionRows.isEmpty()) { - AServer server = serverManagementService.loadServer(interactionHook.getInteraction().getGuild()); - allMessageActions.set(0, allMessageActions.get(0).addComponents(actionRows)); - actionRows.forEach(components -> components.getComponents().forEach(component -> { - if(component instanceof ActionComponent) { - String id = ((ActionComponent)component).getCustomId(); - MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id); - if(payload != null && payload.getPersistCallback()) { - componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType()); + List actionRows = messageToSend.getActionRows(); + if(!actionRows.isEmpty()) { + AServer server = serverManagementService.loadServer(interactionHook.getInteraction().getGuild()); + allMessageActions.set(0, allMessageActions.get(0).addComponents(actionRows)); + actionRows.forEach(components -> components.getComponents().forEach(component -> { + if(component instanceof ActionComponent) { + String id = ((ActionComponent)component).getCustomId(); + MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id); + if(payload != null && payload.getPersistCallback()) { + componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType()); + } } - } - })); - } - - if(messageToSend.getEphemeral()) { - Interaction interaction = interactionHook.getInteraction(); - interactionHook.setEphemeral(messageToSend.getEphemeral()); - if(ContextUtils.hasGuild(interaction)) { - log.info("Sending ephemeral message to interaction in guild {} in channel {} for user {}.", + })); + } + if(messageToSend.getEphemeral()) { + Interaction interaction = interactionHook.getInteraction(); + interactionHook.setEphemeral(messageToSend.getEphemeral()); + if(ContextUtils.hasGuild(interaction)) { + log.info("Sending ephemeral message to interaction in guild {} in channel {} for user {}.", interaction.getGuild().getIdLong(), interaction.getChannel().getId(), interaction.getUser().getIdLong()); + } + metricService.incrementCounter(EPHEMERAL_MESSAGES_SEND); } - metricService.incrementCounter(EPHEMERAL_MESSAGES_SEND); - } - if(messageToSend.hasFilesToSend()) { - List attachedFiles = messageToSend + if(messageToSend.hasFilesToSend()) { + List attachedFiles = messageToSend .getAttachedFiles() .stream() .map(AttachedFile::convertToFileUpload) .collect(Collectors.toList()); - if(!allMessageActions.isEmpty()) { - // in case there has not been a message, we need to increment it - allMessageActions.set(0, allMessageActions.get(0).setFiles(attachedFiles)); - } else { - metricService.incrementCounter(MESSAGE_SEND_METRIC); - allMessageActions.add(interactionHook.sendFiles(attachedFiles)); + if(!allMessageActions.isEmpty()) { + // in case there has not been a message, we need to increment it + allMessageActions.set(0, allMessageActions.get(0).setFiles(attachedFiles)); + } else { + metricService.incrementCounter(MESSAGE_SEND_METRIC); + allMessageActions.add(interactionHook.sendFiles(attachedFiles)); + } } + Set allowedMentions = allowedMentionService.getAllowedMentionsFor(interactionHook.getInteraction().getMessageChannel(), messageToSend); + allMessageActions.forEach(messageAction -> futures.add(messageAction.setAllowedMentions(allowedMentions).setEphemeral(messageToSend.getEphemeral()).submit())); + return futures; } - Set allowedMentions = allowedMentionService.getAllowedMentionsFor(interactionHook.getInteraction().getMessageChannel(), messageToSend); - allMessageActions.forEach(messageAction -> futures.add(messageAction.setAllowedMentions(allowedMentions).setEphemeral(messageToSend.getEphemeral()).submit())); - return futures; } @Override