diff --git a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/command/Mute.java b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/command/Mute.java index e931dd64b..9f5364770 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/command/Mute.java +++ b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/command/Mute.java @@ -10,6 +10,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.CommandResult; import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.models.ServerChannelMessage; +import dev.sheldan.abstracto.core.templating.service.TemplateService; import dev.sheldan.abstracto.moderation.config.ModerationModuleDefinition; import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition; import dev.sheldan.abstracto.moderation.model.template.command.MuteContext; @@ -30,15 +31,21 @@ import static dev.sheldan.abstracto.moderation.service.MuteService.MUTE_EFFECT_K @Component public class Mute extends AbstractConditionableCommand { + public static final String MUTE_DEFAULT_REASON_TEMPLATE = "mute_default_reason"; + @Autowired private MuteService muteService; + @Autowired + private TemplateService templateService; + @Override public CompletableFuture executeAsync(CommandContext commandContext) { List parameters = commandContext.getParameters().getParameters(); Member member = (Member) parameters.get(0); Duration duration = (Duration) parameters.get(1); - String reason = (String) parameters.get(2); + String defaultReason = templateService.renderSimpleTemplate(MUTE_DEFAULT_REASON_TEMPLATE, commandContext.getGuild().getIdLong()); + String reason = parameters.size() == 3 ? (String) parameters.get(2) : defaultReason; ServerChannelMessage context = ServerChannelMessage .builder() .serverId(commandContext.getGuild().getIdLong()) diff --git a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/repository/WarnRepository.java b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/repository/WarnRepository.java index 6a80d993c..82b5fde35 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/repository/WarnRepository.java +++ b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/repository/WarnRepository.java @@ -1,11 +1,11 @@ package dev.sheldan.abstracto.moderation.repository; +import dev.sheldan.abstracto.core.models.ServerSpecificId; import dev.sheldan.abstracto.core.models.database.AServer; import dev.sheldan.abstracto.core.models.database.AUserInAServer; import dev.sheldan.abstracto.moderation.model.database.Warning; import org.jetbrains.annotations.NotNull; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.lang.NonNull; import org.springframework.stereotype.Repository; import java.time.Instant; @@ -13,8 +13,9 @@ import java.util.List; import java.util.Optional; @Repository -public interface WarnRepository extends JpaRepository { +public interface WarnRepository extends JpaRepository { List findAllByWarnedUser_ServerReferenceAndDecayedFalseAndWarnDateLessThan(AServer server, Instant cutOffDate); + List findAllByWarnedUser_ServerReferenceAndDecayedFalseAndWarnDateGreaterThan(AServer server, Instant cutOffDate); List findAllByWarnedUser_ServerReference(AServer server); @@ -24,10 +25,6 @@ public interface WarnRepository extends JpaRepository { List findByWarnedUser(AUserInAServer aUserInAServer); - @NotNull - @Override - Optional findById(@NonNull Long aLong); - @NotNull Optional findByWarnId_IdAndWarnId_ServerId(Long warnId, Long serverId); } diff --git a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/MuteServiceBean.java b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/MuteServiceBean.java index efd08aea7..e4b9bd5f5 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/MuteServiceBean.java +++ b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/MuteServiceBean.java @@ -181,7 +181,7 @@ public class MuteServiceBean implements MuteService { .messageId(muteContext.getContext().getMessageId()) .build(); AUserInAServer userInServerBeingMuted = userInServerManagementService.loadOrCreateUser(muteContext.getMutedUser()); - AUserInAServer userInServerMuting = userInServerManagementService.loadOrCreateUser(muteContext.getMutedUser()); + AUserInAServer userInServerMuting = userInServerManagementService.loadOrCreateUser(muteContext.getMutingUser()); muteManagementService.createMute(userInServerBeingMuted, userInServerMuting, muteContext.getReason(), muteContext.getMuteTargetDate(), origin, triggerKey, muteContext.getMuteId()); } diff --git a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/WarnServiceBean.java b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/WarnServiceBean.java index 53a7ed6a5..abdab5095 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/WarnServiceBean.java +++ b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/WarnServiceBean.java @@ -21,11 +21,13 @@ import dev.sheldan.abstracto.moderation.model.template.command.WarnContext; import dev.sheldan.abstracto.moderation.model.template.command.WarnNotification; import dev.sheldan.abstracto.moderation.model.template.job.WarnDecayLogModel; import dev.sheldan.abstracto.moderation.model.template.job.WarnDecayWarning; +import dev.sheldan.abstracto.moderation.model.template.listener.WarnDecayMemberNotificationModel; import dev.sheldan.abstracto.moderation.service.management.WarnManagementService; import dev.sheldan.abstracto.core.templating.model.MessageToSend; import dev.sheldan.abstracto.core.templating.service.TemplateService; import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.ISnowflake; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +39,8 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.stream.Collectors; @Slf4j @Component @@ -85,6 +89,7 @@ public class WarnServiceBean implements WarnService { public static final String WARN_NOTIFICATION_TEMPLATE = "warn_notification"; public static final String WARNINGS_COUNTER_KEY = "WARNINGS"; public static final String WARN_DECAY_LOG_TEMPLATE_KEY = "warn_decay_log"; + public static final String WARN_DECAY_NOTIFICATION_TEMPLATE_KEY = "warn_decay_member_notification"; @Override public CompletableFuture notifyAndLogFullUserWarning(WarnContext context) { @@ -131,6 +136,7 @@ public class WarnServiceBean implements WarnService { Instant cutOffDay = Instant.now().minus(days, ChronoUnit.DAYS); log.info("Decaying warnings on server {} which are older than {}.", server.getId(), cutOffDay); List warningsToDecay = warnManagementService.getActiveWarningsInServerOlderThan(server, cutOffDay); + List userIds = new ArrayList<>(getUserIdsForWarnings(warningsToDecay)); List warningIds = flattenWarnings(warningsToDecay); Long serverId = server.getId(); CompletableFuture completableFuture; @@ -141,11 +147,57 @@ public class WarnServiceBean implements WarnService { log.debug("Not logging automatic warn decay, because feature {} has its mode {} disabled in server {}.", ModerationFeatureDefinition.AUTOMATIC_WARN_DECAY, WarnDecayMode.AUTOMATIC_WARN_DECAY_LOG, server.getId()); completableFuture = CompletableFuture.completedFuture(null); } + if(featureModeService.featureModeActive(ModerationFeatureDefinition.AUTOMATIC_WARN_DECAY, server, WarnDecayMode.NOTIFY_MEMBER_WARNING_DECAYS)) { + CompletableFuture> membersInServerAsync = memberService.getMembersInServerAsync(server.getId(), userIds); + membersInServerAsync + .thenAccept(members -> self.sendMemberNotifications(serverId, warningIds, members, cutOffDay)).exceptionally(throwable -> { + log.error("Failed to notify members about warn decays.", throwable); + return null; + }); + log.info("Notifying members about warn decay in server {}.", server.getId()); + } else { + log.info("Not notifying members about warn decay in server {}.", server.getId()); + } return completableFuture.thenAccept(aVoid -> self.decayWarnings(warningIds, serverId) ); } + @Transactional + public CompletableFuture sendMemberNotifications(Long serverId, List warnIds, List members, Instant cutOffDay) { + List decayingWarnings = warnManagementService.getWarningsViaId(warnIds, serverId); + AServer server = decayingWarnings.get(0).getWarnedUser().getServerReference(); + List> notificationFutures = new ArrayList<>(); + Map userIdToMember = members.stream().collect(Collectors.toMap(ISnowflake::getIdLong, Function.identity())); + decayingWarnings.forEach(warning -> { + Long userId = warning.getWarnedUser().getUserReference().getId(); + Long warningId = warning.getWarnId().getId(); + if(userIdToMember.containsKey(userId)) { + Member memberToSendTo = userIdToMember.get(userId); + List remainingWarnings = warnManagementService.getActiveWarningsInServerYoungerThan(server, cutOffDay); + WarnDecayMemberNotificationModel model = + WarnDecayMemberNotificationModel + .builder() + .warnDate(warning.getWarnDate()) + .warnReason(warning.getReason()) + .remainingWarningsCount(remainingWarnings.size()) + .build(); + MessageToSend messageToSend = templateService.renderEmbedTemplate(WARN_DECAY_NOTIFICATION_TEMPLATE_KEY, model, serverId); + log.info("Notifying user {} in server {} about decayed warning {}.", userId, serverId, warningId); + notificationFutures.add(messageService.sendMessageToSendToUser(memberToSendTo.getUser(), messageToSend).exceptionally(throwable -> { + log.error("Failed to send warn decay message to user {} in server {} to notify about decay warning {}.", userId, server, warningId); + return null; + })); + } else { + log.warn("Could not find user {} in server {}. Not notifying about decayed warning {}.", userId, serverId, warningId); + } + }); + CompletableFuture future = new CompletableFuture(); + FutureUtils.toSingleFutureGeneric(notificationFutures) + .whenComplete((unused, throwable) -> future.complete(null)); + return future; + } + private List flattenWarnings(List warningsToDecay) { List warningIds = new ArrayList<>(); warningsToDecay.forEach(warning -> @@ -154,6 +206,12 @@ public class WarnServiceBean implements WarnService { return warningIds; } + private Set getUserIdsForWarnings(List warnings) { + Set userIds = new HashSet<>(); + warnings.forEach(warning -> userIds.add(warning.getWarnedUser().getUserReference().getId())); + return userIds; + } + @Transactional public void decayWarnings(List warningIds, Long serverId) { Instant now = Instant.now(); diff --git a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/management/WarnManagementServiceBean.java b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/management/WarnManagementServiceBean.java index 71c14bddc..e6b1d146e 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/management/WarnManagementServiceBean.java +++ b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/java/dev/sheldan/abstracto/moderation/service/management/WarnManagementServiceBean.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Component; import java.time.Instant; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Component @Slf4j @@ -43,6 +44,11 @@ public class WarnManagementServiceBean implements WarnManagementService { return warnRepository.findAllByWarnedUser_ServerReferenceAndDecayedFalseAndWarnDateLessThan(server, date); } + @Override + public List getActiveWarningsInServerYoungerThan(AServer server, Instant date) { + return warnRepository.findAllByWarnedUser_ServerReferenceAndDecayedFalseAndWarnDateGreaterThan(server, date); + } + @Override public Long getTotalWarnsForUser(AUserInAServer aUserInAServer) { return warnRepository.countByWarnedUser(aUserInAServer); @@ -73,6 +79,15 @@ public class WarnManagementServiceBean implements WarnManagementService { return findByIdOptional(id, serverId).orElseThrow(() -> new AbstractoRunTimeException("Warning not found.")); } + @Override + public List getWarningsViaId(List warnIds, Long serverId) { + List serverWarnIds = warnIds + .stream() + .map(aLong -> new ServerSpecificId(serverId, aLong)) + .collect(Collectors.toList()); + return warnRepository.findAllById(serverWarnIds); + } + @Override public void deleteWarning(Warning warning) { log.info("Deleting warning with id {} in server {}.", warning.getWarnId().getId(), warning.getWarnId().getServerId()); diff --git a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/resources/moderation-config.properties b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/resources/moderation-config.properties index 7d59e6b15..bbed5c414 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/resources/moderation-config.properties +++ b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/main/resources/moderation-config.properties @@ -46,6 +46,10 @@ abstracto.featureModes.automaticWarnDecayLogging.featureName=warnDecay abstracto.featureModes.automaticWarnDecayLogging.mode=automaticWarnDecayLogging abstracto.featureModes.automaticWarnDecayLogging.enabled=true +abstracto.featureModes.notifyMemberWarningDecays.featureName=warnDecay +abstracto.featureModes.notifyMemberWarningDecays.mode=notifyMemberWarningDecays +abstracto.featureModes.notifyMemberWarningDecays.enabled=true + abstracto.featureModes.manualUnMuteLogging.featureName=muting abstracto.featureModes.manualUnMuteLogging.mode=manualUnMuteLogging abstracto.featureModes.manualUnMuteLogging.enabled=true diff --git a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/test/java/dev/sheldan/abstracto/moderation/command/mute/MuteTest.java b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/test/java/dev/sheldan/abstracto/moderation/command/mute/MuteTest.java index eed4d3032..0d3ddac86 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-impl/src/test/java/dev/sheldan/abstracto/moderation/command/mute/MuteTest.java +++ b/abstracto-application/abstracto-modules/moderation/moderation-impl/src/test/java/dev/sheldan/abstracto/moderation/command/mute/MuteTest.java @@ -2,8 +2,10 @@ package dev.sheldan.abstracto.moderation.command.mute; import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.CommandResult; +import dev.sheldan.abstracto.core.templating.service.TemplateService; import dev.sheldan.abstracto.core.test.command.CommandConfigValidator; import dev.sheldan.abstracto.core.test.command.CommandTestUtilities; +import dev.sheldan.abstracto.moderation.command.Ban; import dev.sheldan.abstracto.moderation.command.Mute; import dev.sheldan.abstracto.moderation.model.template.command.MuteContext; import dev.sheldan.abstracto.moderation.service.MuteService; @@ -29,6 +31,9 @@ public class MuteTest { @Mock private MuteService muteService; + @Mock + private TemplateService templateService; + @Captor private ArgumentCaptor muteLogArgumentCaptor; diff --git a/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/config/feature/mode/WarnDecayMode.java b/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/config/feature/mode/WarnDecayMode.java index a8a5c023c..780e6b27d 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/config/feature/mode/WarnDecayMode.java +++ b/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/config/feature/mode/WarnDecayMode.java @@ -6,7 +6,9 @@ import lombok.Getter; @Getter public enum WarnDecayMode implements FeatureMode { - AUTOMATIC_WARN_DECAY_LOG("automaticWarnDecayLogging"); + AUTOMATIC_WARN_DECAY_LOG("automaticWarnDecayLogging"), + NOTIFY_MEMBER_WARNING_DECAYS("notifyMemberWarningDecays") + ; private final String key; diff --git a/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/model/template/listener/WarnDecayMemberNotificationModel.java b/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/model/template/listener/WarnDecayMemberNotificationModel.java new file mode 100644 index 000000000..a488a07d2 --- /dev/null +++ b/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/model/template/listener/WarnDecayMemberNotificationModel.java @@ -0,0 +1,16 @@ +package dev.sheldan.abstracto.moderation.model.template.listener; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.Instant; + +@Getter +@Setter +@Builder +public class WarnDecayMemberNotificationModel { + private Instant warnDate; + private String warnReason; + private Integer remainingWarningsCount; +} diff --git a/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/service/management/WarnManagementService.java b/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/service/management/WarnManagementService.java index dc8cf27d4..3c5b0c131 100644 --- a/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/service/management/WarnManagementService.java +++ b/abstracto-application/abstracto-modules/moderation/moderation-int/src/main/java/dev/sheldan/abstracto/moderation/service/management/WarnManagementService.java @@ -11,11 +11,13 @@ import java.util.Optional; public interface WarnManagementService { Warning createWarning(AUserInAServer warnedAUser, AUserInAServer warningAUser, String reason, Long warnId); List getActiveWarningsInServerOlderThan(AServer server, Instant date); + List getActiveWarningsInServerYoungerThan(AServer server, Instant date); Long getTotalWarnsForUser(AUserInAServer aUserInAServer); List getAllWarnsForUser(AUserInAServer aUserInAServer); List getAllWarningsOfServer(AServer server); Long getActiveWarnsForUser(AUserInAServer aUserInAServer); Optional findByIdOptional(Long id, Long serverId); Warning findById(Long id, Long serverId); + List getWarningsViaId(List warnIds, Long serverId); void deleteWarning(Warning warn); }