Compare commits

...

34 Commits

Author SHA1 Message Date
release-bot
dd21390a60 [maven-release-plugin] prepare release v1.6.24 2026-04-26 21:14:20 +00:00
Sheldan
790cca7278 [AB-xxx] changing duration for reminder snooze, so that it doesnt add the duration _after_ the snooze again, the snooze is intended to be the duration at which the reminders start again
fixing message embed cleanup job not being able to deal with missing channels
2026-04-26 22:58:08 +02:00
release-bot
52f6fd148e Commit from GitHub Actions (Publishes a new version of abstracto) 2026-04-25 21:41:51 +00:00
release-bot
708703fc1f [maven-release-plugin] prepare for next development iteration 2026-04-25 21:30:06 +00:00
release-bot
c5f47cf6e5 [maven-release-plugin] prepare release v1.6.23 2026-04-25 21:30:04 +00:00
Sheldan
f157b1edd7 [AB-xxx] refactoring to use a separate updated column for the auto closing
wrapping modmail thread actions into a separate transaction
2026-04-25 23:27:22 +02:00
release-bot
a7bab8fa3e Commit from GitHub Actions (Publishes a new version of abstracto) 2026-04-24 17:44:48 +00:00
release-bot
d9ba8850af [maven-release-plugin] prepare for next development iteration 2026-04-24 17:29:47 +00:00
release-bot
d14a8c6d7c [maven-release-plugin] prepare release v1.6.22 2026-04-24 17:29:44 +00:00
Sheldan
7627ee72e6 [AB-xxx] adding modmail thread reminders and auto closing
fixing sending cv2 components to interactions
fixing error handling for message embeds
2026-04-24 18:23:07 +02:00
release-bot
d8bb4a365d Commit from GitHub Actions (Publishes a new version of abstracto) 2026-03-29 09:18:03 +00:00
release-bot
e614a97ea2 [maven-release-plugin] prepare for next development iteration 2026-03-29 09:07:22 +00:00
release-bot
194930f9db [maven-release-plugin] prepare release v1.6.21 2026-03-29 09:07:20 +00:00
Sheldan
7f12ac7107 [AB-xxx] adding command to automatically ban all honeypot users
refactored honeypot listener into a service
fixing transfer credits not requiring the inputs
2026-03-29 11:02:47 +02:00
Sheldan
81b1688d97 [AB-xxx] adding unit test and improvement for runtime experience storage 2026-03-05 21:11:36 +01:00
Sheldan
3500ec4123 [AB-xxx] small code improvements 2026-03-05 20:44:02 +01:00
release-bot
4495b12e84 Commit from GitHub Actions (Publishes a new version of abstracto) 2026-02-15 22:09:42 +00:00
release-bot
1f1cb0ed34 [maven-release-plugin] prepare for next development iteration 2026-02-15 21:55:44 +00:00
release-bot
a778518475 [maven-release-plugin] prepare release v1.6.20 2026-02-15 21:55:42 +00:00
Sheldan
fa62353aee [AB-xxx] adding support to use @time inputs for duration and instant command parameters 2026-02-15 22:44:57 +01:00
Sheldan
2f125d0101 [AB-xxx] do not delete embedding message if the message embedding fails
fixing code for top level container config (not sure how that worked before)
2026-01-25 00:02:30 +01:00
Sheldan
4dc92fe49b [AB-xxx] adding metrics for http requests 2026-01-08 00:07:16 +01:00
release-bot
5e908f6837 Commit from GitHub Actions (Publishes a new version of abstracto) 2025-12-30 12:41:23 +00:00
release-bot
95a2813c34 [maven-release-plugin] prepare for next development iteration 2025-12-30 12:27:20 +00:00
release-bot
d844e09f4d [maven-release-plugin] prepare release v1.6.19 2025-12-30 12:27:18 +00:00
Sheldan
8251074aab [AB-xxx] adding command to view system configuration per server
adding auto complete to setConfig command key parameter
2025-12-28 23:46:36 +01:00
Sheldan
4180a07243 [AB-xxx] upgrading to JDA 6.2.0
changing how assignable role places change their buttons
2025-12-28 19:17:58 +01:00
release-bot
cf9e5f67f6 Commit from GitHub Actions (Publishes a new version of abstracto) 2025-12-17 23:12:02 +00:00
release-bot
ae53784da2 [maven-release-plugin] prepare for next development iteration 2025-12-17 22:59:30 +00:00
release-bot
342f0b6043 [maven-release-plugin] prepare release v1.6.18 2025-12-17 22:59:28 +00:00
Sheldan
274dd77eeb [AB-xxx] adding ability to add additional channels to a post target
adding auto complete to post target command
2025-12-17 23:54:54 +01:00
release-bot
0f6adacd90 Commit from GitHub Actions (Publishes a new version of abstracto) 2025-11-29 13:17:32 +00:00
release-bot
2dd5841397 [maven-release-plugin] prepare for next development iteration 2025-11-29 13:06:06 +00:00
release-bot
a2f49a0de9 [maven-release-plugin] prepare release v1.6.17 2025-11-29 13:06:05 +00:00
198 changed files with 2463 additions and 638 deletions

2
.env
View File

@@ -1,2 +1,2 @@
REGISTRY_PREFIX=harbor.sheldan.dev/abstracto/
VERSION=1.6.16
VERSION=1.6.23

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>anti-raid</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -108,6 +108,6 @@ public class MassPingServiceBean implements MassPingService {
.memberDisplay(MemberDisplay.fromMember(member))
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(MASS_PING_MUTE_NOTIFICATION_TEMPLATE_KEY, model, member.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, AntiRaidPostTarget.MASS_PING_LOG, member.getGuild().getIdLong()));
return FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(messageToSend, AntiRaidPostTarget.MASS_PING_LOG, member.getGuild().getIdLong()));
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>anti-raid</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>assignable-roles</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -31,7 +31,6 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.entities.emoji.CustomEmoji;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.components.buttons.ButtonStyle;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -60,9 +59,6 @@ public class AssignableRolePlaceServiceBean implements AssignableRolePlaceServic
@Autowired
private ChannelService channelService;
@Autowired
private GuildService guildService;
@Autowired
private EmoteService emoteService;
@@ -131,9 +127,18 @@ public class AssignableRolePlaceServiceBean implements AssignableRolePlaceServic
String buttonId = componentService.generateComponentId();
String emoteMarkdown = emoji != null ? emoji.getFormatted() : null;
if (assignableRolePlace.getMessageId() != null) {
AssignablePostMessage model = prepareAssignablePostMessageModel(assignableRolePlace);
model.getRoles().add(AssignablePostRole
.builder()
.componentId(buttonId)
.emoteMarkDown(emoteMarkdown)
.description(description)
.build());
MessageToSend messageToSend = templateService.renderEmbedTemplate(ASSIGNABLE_ROLES_POST_TEMPLATE_KEY, model, assignableRolePlace.getServer().getId());
log.debug("Assignable role place {} has already message post with ID {} - updating.", assignableRolePlace.getId(), assignableRolePlace.getMessageId());
return componentService.addButtonToMessage(assignableRolePlace.getMessageId(), textChannel, buttonId, description, emoteMarkdown, ButtonStyle.SECONDARY)
.thenAccept(message -> self.persistAssignableRoleAddition(placeId, role, description, emoji, buttonId));
return messageService.editMessageInChannel(textChannel, messageToSend, assignableRolePlace.getMessageId()).thenAccept(unused -> {
self.persistAssignableRoleAddition(placeId, role, description, emoji, buttonId);
});
} else {
log.info("Assignable role place {} is not yet setup - only adding role to the database.", assignableRolePlace.getId());
self.persistAssignableRoleAddition(placeId, role, description, emoji, buttonId);
@@ -174,7 +179,7 @@ public class AssignableRolePlaceServiceBean implements AssignableRolePlaceServic
return channelService.retrieveMessageInChannel(assignableRolePlace.getServer().getId(), assignableRolePlace.getChannel().getId(), assignableRolePlace.getMessageId())
.thenCompose(message -> {
log.debug("Updating message {} to remove component with ID {}.", message.getIdLong(), componentId);
return componentService.removeComponentWithId(message, componentId, true);
return componentService.removeComponentById(message, componentId).thenAccept(message1 -> {});
}
);
}
@@ -269,8 +274,8 @@ public class AssignableRolePlaceServiceBean implements AssignableRolePlaceServic
log.info("Deactivating assignable role place {} in server {}", place.getId(), place.getServer().getId());
return channelService.retrieveMessageInChannel(place.getServer().getId(), place.getChannel().getId(), place.getMessageId())
.thenCompose(message ->
componentService.disableAllButtons(message)
);
componentService.disableAllComponents(message)
).thenAccept(message -> {});
}
@Override
@@ -283,8 +288,8 @@ public class AssignableRolePlaceServiceBean implements AssignableRolePlaceServic
log.info("Activating assignable role place {} in server {}", place.getId(), place.getServer().getId());
return channelService.retrieveMessageInChannel(place.getServer().getId(), place.getChannel().getId(), place.getMessageId())
.thenCompose(message ->
componentService.enableAllButtons(message)
);
componentService.enableAllComponents(message)
).thenAccept(message -> {});
}
@Override

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>assignable-roles</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>assignable-roles-int</artifactId>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>custom-command</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>custom-command</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>dynamic-activity</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>dynamic-activity</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>entertainment</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -74,7 +74,6 @@ public class TransferCredits extends AbstractConditionableCommand {
.name(MEMBER_PARAMETER)
.templated(true)
.type(Member.class)
.optional(true)
.build();
Parameter amountParameter = Parameter
@@ -82,7 +81,6 @@ public class TransferCredits extends AbstractConditionableCommand {
.name(AMOUNT_PARAMETER)
.templated(true)
.type(Integer.class)
.optional(true)
.build();
List<Parameter> parameters = Arrays.asList(memberParameter, amountParameter);

View File

@@ -200,8 +200,8 @@ public class EntertainmentServiceBean implements EntertainmentService {
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageEmbedToSendToAChannel(messageToSend, pressF.getPressFChannel()))
.thenCompose(unused -> messageService.loadMessage(serverId, channelId, messageId).thenCompose(message -> {
log.info("Clearing buttons from pressF {} in with message {} in channel {} in server {}.", pressFId, pressFId, channelId, serverId);
return componentService.clearButtons(message);
}));
return componentService.clearComponents(message);
})).thenAccept(message -> {});
} else {
throw new AbstractoRunTimeException(String.format("PressF with id %s not found.", pressFId));
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>entertainment</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>experience-tracking</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -313,107 +313,107 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
Long userInServerId = userInAServer.getUserInServerId();
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInAServer.getUserInServerId());
AUserExperience aUserExperience = aUserExperienceOptional.orElseGet(() -> userExperienceManagementService.createUserInServer(userInAServer));
if(Boolean.FALSE.equals(aUserExperience.getExperienceGainDisabled())) {
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
Long minExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MIN_EXP_KEY, serverId);
Long maxExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MAX_EXP_KEY, serverId);
Double multiplier = configService.getDoubleValueOrConfigDefault(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, serverId);
Long experienceRange = maxExp - minExp + 1;
Long gainedExperience = (secureRandom.nextInt(experienceRange.intValue()) + minExp);
gainedExperience = (long) Math.floor(gainedExperience * multiplier);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
log.debug("Handling {}. The user gains {}.", userInServerId, gainedExperience);
Long oldExperience = aUserExperience.getExperience();
Long newExperienceCount = oldExperience + gainedExperience;
aUserExperience.setExperience(newExperienceCount);
AExperienceLevel newLevel = calculateLevel(levels, newExperienceCount);
RoleCalculationResult result = RoleCalculationResult
.builder()
.build();
boolean userChangesLevel = !Objects.equals(newLevel.getLevel(), aUserExperience.getCurrentLevel().getLevel());
Integer oldLevel = aUserExperience.getCurrentLevel() != null ? aUserExperience.getCurrentLevel().getLevel() : 0;
if(userChangesLevel) {
log.info("User {} in server {} changed level. New {}, Old {}.", member.getIdLong(),
member.getGuild().getIdLong(), newLevel.getLevel(),
oldLevel);
aUserExperience.setCurrentLevel(newLevel);
AExperienceRole calculatedNewRole = experienceRoleService.calculateRole(roles, newLevel.getLevel());
Long oldRoleId = aUserExperience.getCurrentExperienceRole() != null && aUserExperience.getCurrentExperienceRole().getRole() != null ? aUserExperience.getCurrentExperienceRole().getRole().getId() : null;
Long newRoleId = calculatedNewRole != null && calculatedNewRole.getRole() != null ? calculatedNewRole.getRole().getId() : null;
result.setOldRoleId(oldRoleId);
result.setNewRoleId(newRoleId);
if(message != null
&& aUserExperience.getLevelUpNotification()
&& featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, serverId, ExperienceFeatureMode.LEVEL_UP_NOTIFICATION)) {
LevelUpNotificationModel model = LevelUpNotificationModel
.builder()
.memberDisplay(MemberDisplay.fromMember(member))
.oldExperience(oldExperience)
.newExperience(newExperienceCount)
.newLevel(newLevel.getLevel())
.oldLevel(oldLevel)
.newRole(oldRoleId != null ? RoleDisplay.fromRole(oldRoleId) : null)
.newRole(newRoleId != null ? RoleDisplay.fromRole(newRoleId) : null)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("experience_level_up_notification", model, serverId);
FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, message.getChannel())).thenAccept(unused -> {
log.info("Sent level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to send level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
return null;
});
}
aUserExperience.setCurrentExperienceRole(calculatedNewRole);
}
aUserExperience.setMessageCount(aUserExperience.getMessageCount() + 1L);
if(userChangesLevel && featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, server, ExperienceFeatureMode.LEVEL_ACTION)) {
levelActionService.applyLevelActionsToUser(aUserExperience, oldLevel)
.thenAccept(unused -> {
log.info("Executed level actions for user {}.", userInServerId);
})
.exceptionally(throwable -> {
log.warn("Failed to execute level actions for user {}.", userInServerId, throwable);
return null;
});
}
if(aUserExperienceOptional.isEmpty()) {
userExperienceManagementService.saveUser(aUserExperience);
}
if(!Objects.equals(result.getOldRoleId(), result.getNewRoleId())) {
if(result.getOldRoleId() != null && result.getNewRoleId() != null) {
roleService.updateRolesIds(member, Arrays.asList(result.getOldRoleId()), Arrays.asList(result.getNewRoleId())).thenAccept(unused -> {
log.debug("Removed role {} from and added role {} to member {} in server {}.", result.getOldRoleId(), result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from and add role {} to member {} in server {}.", result.getOldRoleId(), result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
} else {
if(result.getOldRoleId() != null) {
roleService.removeRoleFromMemberAsync(member, result.getOldRoleId()).thenAccept(unused -> {
log.debug("Removed role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
if(result.getNewRoleId() != null) {
roleService.addRoleToMemberAsync(member, result.getNewRoleId()).thenAccept(unused -> {
log.debug("Added role {} to member {} in server {}.", result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to add role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
}
}
} else {
if (aUserExperience.getExperienceGainDisabled().equals(Boolean.TRUE)) {
log.debug("Experience gain was disabled. User did not gain any experience.");
return;
}
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
Long minExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MIN_EXP_KEY, serverId);
Long maxExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MAX_EXP_KEY, serverId);
Double multiplier = configService.getDoubleValueOrConfigDefault(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, serverId);
Long experienceRange = maxExp - minExp + 1;
Long gainedExperience = (secureRandom.nextInt(experienceRange.intValue()) + minExp);
gainedExperience = (long) Math.floor(gainedExperience * multiplier);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
log.debug("Handling {}. The user gains {}.", userInServerId, gainedExperience);
Long oldExperience = aUserExperience.getExperience();
Long newExperienceCount = oldExperience + gainedExperience;
aUserExperience.setExperience(newExperienceCount);
AExperienceLevel newLevel = calculateLevel(levels, newExperienceCount);
RoleCalculationResult result = RoleCalculationResult
.builder()
.build();
boolean userChangesLevel = !Objects.equals(newLevel.getLevel(), aUserExperience.getCurrentLevel().getLevel());
Integer oldLevel = aUserExperience.getCurrentLevel() != null ? aUserExperience.getCurrentLevel().getLevel() : 0;
if(userChangesLevel) {
log.info("User {} in server {} changed level. New {}, Old {}.", member.getIdLong(),
member.getGuild().getIdLong(), newLevel.getLevel(),
oldLevel);
aUserExperience.setCurrentLevel(newLevel);
AExperienceRole calculatedNewRole = experienceRoleService.calculateRole(roles, newLevel.getLevel());
Long oldRoleId = aUserExperience.getCurrentExperienceRole() != null && aUserExperience.getCurrentExperienceRole().getRole() != null ? aUserExperience.getCurrentExperienceRole().getRole().getId() : null;
Long newRoleId = calculatedNewRole != null && calculatedNewRole.getRole() != null ? calculatedNewRole.getRole().getId() : null;
result.setOldRoleId(oldRoleId);
result.setNewRoleId(newRoleId);
if(message != null
&& aUserExperience.getLevelUpNotification()
&& featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, serverId, ExperienceFeatureMode.LEVEL_UP_NOTIFICATION)) {
LevelUpNotificationModel model = LevelUpNotificationModel
.builder()
.memberDisplay(MemberDisplay.fromMember(member))
.oldExperience(oldExperience)
.newExperience(newExperienceCount)
.newLevel(newLevel.getLevel())
.oldLevel(oldLevel)
.newRole(oldRoleId != null ? RoleDisplay.fromRole(oldRoleId) : null)
.newRole(newRoleId != null ? RoleDisplay.fromRole(newRoleId) : null)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("experience_level_up_notification", model, serverId);
FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, message.getChannel())).thenAccept(unused -> {
log.info("Sent level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to send level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
return null;
});
}
aUserExperience.setCurrentExperienceRole(calculatedNewRole);
}
aUserExperience.setMessageCount(aUserExperience.getMessageCount() + 1L);
if(userChangesLevel && featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, server, ExperienceFeatureMode.LEVEL_ACTION)) {
levelActionService.applyLevelActionsToUser(aUserExperience, oldLevel)
.thenAccept(unused -> {
log.info("Executed level actions for user {}.", userInServerId);
})
.exceptionally(throwable -> {
log.warn("Failed to execute level actions for user {}.", userInServerId, throwable);
return null;
});
}
if(aUserExperienceOptional.isEmpty()) {
userExperienceManagementService.saveUser(aUserExperience);
}
if(!Objects.equals(result.getOldRoleId(), result.getNewRoleId())) {
if(result.getOldRoleId() != null && result.getNewRoleId() != null) {
roleService.updateRolesIds(member, List.of(result.getOldRoleId()), List.of(result.getNewRoleId())).thenAccept(unused -> {
log.debug("Removed role {} from and added role {} to member {} in server {}.", result.getOldRoleId(), result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from and add role {} to member {} in server {}.", result.getOldRoleId(), result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
} else {
if(result.getOldRoleId() != null) {
roleService.removeRoleFromMemberAsync(member, result.getOldRoleId()).thenAccept(unused -> {
log.debug("Removed role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
if(result.getNewRoleId() != null) {
roleService.addRoleToMemberAsync(member, result.getNewRoleId()).thenAccept(unused -> {
log.debug("Added role {} to member {} in server {}.", result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to add role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
}
}
}
@@ -521,7 +521,7 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
public LeaderBoardEntry getRankOfUserInServer(AUserInAServer userInAServer) {
log.debug("Retrieving rank for {}", userInAServer.getUserReference().getId());
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInAServer.getUserInServerId());
if(!aUserExperienceOptional.isPresent()) {
if(aUserExperienceOptional.isEmpty()) {
throw new NoExperienceTrackedException();
}
Integer rank = 0;

View File

@@ -52,7 +52,7 @@ public class ExperienceLevelServiceBean implements ExperienceLevelService {
if(level < 0) {
throw new IllegalArgumentException("Level should not be less to 0.");
}
return 5L * (level * level) + 50 * level + 100;
return 5L * ((long) level * level) + 50L * level + 100;
}
@Override

View File

@@ -88,7 +88,7 @@ public class ExperienceRoleServiceBean implements ExperienceRoleService {
@Override
public CompletableFuture<Void> unsetRoles(List<ARole> rolesToUnset, GuildMessageChannel messageChannel) {
List<AExperienceRole> rolesInServer = experienceRoleManagementService.getRolesInServer(rolesToUnset);
Integer totalCount = 0;
int totalCount = 0;
for (AExperienceRole aExperienceRole : rolesInServer) {
totalCount += aExperienceRole.getUsers().size();
}

View File

@@ -1,5 +1,8 @@
package dev.sheldan.abstracto.experience.service;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import org.springframework.stereotype.Component;
import java.time.Instant;
@@ -17,11 +20,9 @@ import java.util.concurrent.locks.ReentrantLock;
@Component
public class RunTimeExperienceService {
private Map<Long,Map<Long, Instant>> runtimeExperience = new HashMap<>();
@Getter
private final Map<Long,Map<Long, Instant>> runtimeExperience = new HashMap<>();
private static final Lock lock = new ReentrantLock();
public Map<Long, Map<Long, Instant>> getRuntimeExperience() {
return runtimeExperience;
}
/**
* Acquires the lock of the runtime experience data structure. Operations on it should only be done, while holding the lock
@@ -39,6 +40,7 @@ public class RunTimeExperienceService {
public void cleanupRunTimeStorage() {
Instant now = Instant.now();
Set<Long> serverIdsToRemove = new HashSet<>();
runtimeExperience.forEach((serverId, userInstantMap) -> {
List<Long> userIdsToRemove = new ArrayList<>();
userInstantMap.forEach((userId, instant) -> {
@@ -47,6 +49,10 @@ public class RunTimeExperienceService {
}
});
userIdsToRemove.forEach(userInstantMap::remove);
if(userInstantMap.isEmpty()) {
serverIdsToRemove.add(serverId);
}
});
serverIdsToRemove.forEach(runtimeExperience::remove);
}
}

View File

@@ -0,0 +1,32 @@
package dev.sheldan.abstracto.experience.service;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
public class RuntimeExperienceServiceTest {
@Test
public void shouldCleanExpiredExperience() {
RunTimeExperienceService experienceService = new RunTimeExperienceService();
Map<Long, Instant> mapValue = new HashMap<>(Map.of(2L, Instant.now().minusSeconds(5)));
experienceService.getRuntimeExperience().put(1L, mapValue);
experienceService.cleanupRunTimeStorage();
assertThat(experienceService.getRuntimeExperience()).isEmpty();
}
@Test
public void shouldLeaveExperienceIfNotYetExpired() {
RunTimeExperienceService experienceService = new RunTimeExperienceService();
Map<Long, Instant> mapValue2 = new HashMap<>(Map.of(3L, Instant.now().plusSeconds(5)));
experienceService.getRuntimeExperience().put(2L, mapValue2);
experienceService.cleanupRunTimeStorage();
assertThat(experienceService.getRuntimeExperience().get(2L)).containsKey(3L);
assertThat(experienceService.getRuntimeExperience()).hasSize(1);
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>experience-tracking</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>giveaway</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<artifactId>giveaway-impl</artifactId>

View File

@@ -134,7 +134,7 @@ public class GiveawayServiceBean implements GiveawayService {
if(giveawayCreationRequest.getTargetChannel() == null) {
log.info("Sending giveaway to post target in server {}", serverId);
postTargetService.validatePostTarget(GiveawayPostTarget.GIVEAWAYS, serverId);
messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, GiveawayPostTarget.GIVEAWAYS, serverId);
messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, GiveawayPostTarget.GIVEAWAYS, serverId).get(0);
} else {
log.info("Sending giveaway to channel {} in server {}.", giveawayCreationRequest.getTargetChannel().getId(), serverId);
messageFutures = channelService.sendMessageToSendToChannel(messageToSend, giveawayCreationRequest.getTargetChannel());

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>giveaway</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<artifactId>giveaway-int</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<artifactId>giveaway</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>image-generation</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<artifactId>image-generation-impl</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>image-generation</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<artifactId>image-generation-int</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<artifactId>image-generation</artifactId>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>invite-filter</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -254,8 +254,8 @@ public class InviteLinkFilterServiceBean implements InviteLinkFilterService {
log.info("Sending notification about {} deleted invite links in guild {} from user {} in channel {} in message {}.",
codes.size(), serverId, message.getAuthor().getIdLong(), message.getChannel().getIdLong(), message.getIdLong());
MessageToSend messageToSend = templateService.renderEmbedTemplate(INVITE_LINK_DELETED_NOTIFICATION_EMBED_TEMPLATE_KEY, model, message.getGuild().getIdLong());
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, InviteFilterPostTarget.INVITE_DELETE_LOG, serverId);
return FutureUtils.toSingleFutureGeneric(messageFutures).thenAccept(unused ->
List<List<CompletableFuture<Message>>> messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, InviteFilterPostTarget.INVITE_DELETE_LOG, serverId);
return FutureUtils.toSingleFutureGenericList(messageFutures).thenAccept(unused ->
log.debug("Successfully send notification about deleted invite link in message {}.", message.getIdLong())
);
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>invite-filter</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -18,7 +18,7 @@
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>moderation-int</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>link-embed</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -13,9 +13,13 @@ import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.MessageCache;
import dev.sheldan.abstracto.core.service.MessageService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.linkembed.config.LinkEmbedFeatureDefinition;
import dev.sheldan.abstracto.linkembed.model.MessageEmbedLink;
import dev.sheldan.abstracto.linkembed.service.MessageEmbedService;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.Event;
@@ -27,7 +31,6 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
@Component
@Slf4j
@@ -67,37 +70,51 @@ public class MessageEmbedListener implements MessageReceivedListener {
}
String messageRaw = message.getContentRaw();
List<MessageEmbedLink> links = messageEmbedService.getLinksInMessage(messageRaw);
for (MessageEmbedLink messageEmbedLink : links) {
messageRaw = messageRaw.replace(messageEmbedLink.getWholeUrl(), "");
}
boolean deleteMessage = StringUtils.isBlank(messageRaw) && !links.isEmpty() && message.getAttachments().isEmpty();
if(!links.isEmpty()) {
Long messageId = message.getIdLong();
Long channelId = message.getChannelIdLong();
Long serverId = message.getGuildIdLong();
log.debug("We found {} links to embed in message {} in channel {} in guild {}.", links.size(), message.getId(), message.getChannel().getId(), message.getGuild().getId());
Long userEmbeddingUserInServerId = userInServerManagementService.loadOrCreateUser(message.getMember()).getUserInServerId();
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (MessageEmbedLink messageEmbedLink : links) {
// potentially support foreign linked servers
// potentially support foreign linked servers?
if(!messageEmbedLink.getServerId().equals(message.getGuild().getIdLong())) {
log.info("Link for message {} was from a foreign server {}. Do not embed.", messageEmbedLink.getMessageId(), messageEmbedLink.getServerId());
continue;
}
messageRaw = messageRaw.replace(messageEmbedLink.getWholeUrl(), "");
Consumer<CachedMessage> cachedMessageConsumer = cachedMessage -> self.embedSingleLink(message, userEmbeddingUserInServerId, cachedMessage);
messageCache.getMessageFromCache(messageEmbedLink.getServerId(), messageEmbedLink.getChannelId(), messageEmbedLink.getMessageId())
.thenAccept(cachedMessageConsumer)
.exceptionally(throwable -> {
log.error("Error when embedding link for message {}", message.getId(), throwable);
return null;
});
Function<CachedMessage, CompletableFuture<Void>> cachedMessageConsumer = cachedMessage -> self.embedSingleLink(message, userEmbeddingUserInServerId, cachedMessage);
futures.add(messageCache.getMessageFromCache(messageEmbedLink.getServerId(), messageEmbedLink.getChannelId(), messageEmbedLink.getMessageId())
.thenCompose(cachedMessageConsumer));
}
if(!futures.isEmpty()) {
// only delete the message if all futures go through
new CompletableFutureList<>(futures).getMainFuture().thenAccept(unused -> {
if(deleteMessage) {
messageService.deleteMessageInChannelInServer(serverId, channelId, messageId);
}
}).thenAccept(unused -> {
log.info("Deleted embedding message server {} channel {} message {}.", serverId, channelId, messageId);
}).exceptionally(throwable -> {
log.info("Failed to delete embedding message or to embed message server {} channel {} message {}.", serverId, channelId, messageId, throwable);
return null;
});
}
}
if(StringUtils.isBlank(messageRaw) && !links.isEmpty() && message.getAttachments().isEmpty()) {
messageService.deleteMessage(message);
if(deleteMessage) {
return ConsumableListenerResult.DELETED;
}
if(!links.isEmpty()) {
} else if(!links.isEmpty()) {
return ConsumableListenerResult.PROCESSED;
}
return ConsumableListenerResult.IGNORED;
}
@Transactional
public void embedSingleLink(Message message, Long cause, CachedMessage cachedMessage) {
public CompletableFuture<Void> embedSingleLink(Message message, Long cause, CachedMessage cachedMessage) {
GuildMemberMessageChannel context = GuildMemberMessageChannel
.builder()
.guildChannel(message.getGuildChannel())
@@ -107,7 +124,7 @@ public class MessageEmbedListener implements MessageReceivedListener {
.build();
log.info("Embedding link to message {} in channel {} in server {} to channel {} and server {}.",
cachedMessage.getMessageId(), cachedMessage.getChannelId(), cachedMessage.getServerId(), message.getChannel().getId(), message.getGuild().getId());
messageEmbedService.embedLink(cachedMessage, message.getGuildChannel(), cause , context).thenAccept(unused ->
return messageEmbedService.embedLink(cachedMessage, message.getGuildChannel(), cause , context).thenAccept(unused ->
metricService.incrementCounter(MESSAGE_EMBED_CREATED)
).exceptionally(throwable -> {
log.error("Failed to embed link towards message {} in channel {} in sever {} linked from message {} in channel {} in server {}.", cachedMessage.getMessageId(), cachedMessage.getChannelId(), cachedMessage.getServerId(),

View File

@@ -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<Void> 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

View File

@@ -26,9 +26,11 @@ import dev.sheldan.abstracto.linkembed.service.management.MessageEmbedPostManage
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.interactions.commands.CommandInteraction;
import org.apache.commons.lang3.tuple.Pair;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@@ -191,11 +193,20 @@ public class MessageEmbedServiceBean implements MessageEmbedService {
.builder()
.message(embeddedMessage)
.build();
MessageToSend messageToSend =
templateService.renderEmbedTemplate(MESSAGE_EMBED_CLEANUP_REPLACEMENT_TEMPLATE, model, embeddingMessage.getServerId());
return channelService.editMessageInAChannelFuture(messageToSend, embeddingMessage.getServerId(), embeddingMessage.getChannelId(),
embeddingMessage.getMessageId());
}).toList();
Optional<GuildChannel> existingChannel =
channelService.getGuildChannelFromServerOptional(embeddingMessage.getServerId(), embeddingMessage.getChannelId());
// if the channel doesnt exist, we dont need to cleanup
if(existingChannel.isPresent()) {
MessageToSend messageToSend =
templateService.renderEmbedTemplate(MESSAGE_EMBED_CLEANUP_REPLACEMENT_TEMPLATE, model, embeddingMessage.getServerId());
return channelService.editMessageInAChannelFuture(messageToSend, embeddingMessage.getServerId(), embeddingMessage.getChannelId(),
embeddingMessage.getMessageId());
} else {
return null;
}
})
.filter(Objects::nonNull)
.toList();
return FutureUtils.toSingleFutureGeneric(editList).whenComplete((unused, throwable) -> {
if(throwable != null) {
log.warn("Failed to cleanup embedded messages..", throwable);

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>link-embed</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>logging</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -37,7 +37,7 @@ public class JoinLogger implements AsyncJoinListener {
.build();
log.debug("Logging join event for user {} in server {}.", listenerModel.getMember().getIdLong(), listenerModel.getServerId());
MessageToSend messageToSend = templateService.renderEmbedTemplate(USER_JOIN_TEMPLATE, model, listenerModel.getServerId());
FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, LoggingPostTarget.JOIN_LOG, listenerModel.getServerId()))
FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(messageToSend, LoggingPostTarget.JOIN_LOG, listenerModel.getServerId()))
.exceptionally(throwable -> {
log.error("Failed to send member joining log.", throwable);
return null;

View File

@@ -57,7 +57,7 @@ public class LeaveLogger implements AsyncLeaveListener {
.build();
log.debug("Logging leave event for user {} in server {}.", listenerModel.getUser().getIdLong(), listenerModel.getServerId());
MessageToSend messageToSend = templateService.renderEmbedTemplate(USER_LEAVE_TEMPLATE, model, listenerModel.getServerId());
FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, LoggingPostTarget.LEAVE_LOG, listenerModel.getServerId()))
FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(messageToSend, LoggingPostTarget.LEAVE_LOG, listenerModel.getServerId()))
.exceptionally(throwable -> {
log.error("Failed to send member leaving log.", throwable);
return null;

View File

@@ -69,7 +69,7 @@ public class MessageDeleteLogListener implements AsyncMessageDeletedListener {
.member(authorMember)
.build();
MessageToSend message = templateService.renderEmbedTemplate(MESSAGE_DELETED_TEMPLATE, logModel, messageFromCache.getServerId());
FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(message, LoggingPostTarget.DELETE_LOG, messageFromCache.getServerId()))
FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(message, LoggingPostTarget.DELETE_LOG, messageFromCache.getServerId()))
.exceptionally(throwable -> {
log.error("Failed to send message deleted log.", throwable);
return null;
@@ -86,7 +86,7 @@ public class MessageDeleteLogListener implements AsyncMessageDeletedListener {
.member(authorMember)
.build();
MessageToSend attachmentEmbed = templateService.renderEmbedTemplate(MESSAGE_DELETED_ATTACHMENT_TEMPLATE, attachmentLogModel, messageFromCache.getServerId());
FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(attachmentEmbed, LoggingPostTarget.DELETE_LOG, messageFromCache.getServerId()))
FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(attachmentEmbed, LoggingPostTarget.DELETE_LOG, messageFromCache.getServerId()))
.exceptionally(throwable -> {
log.error("Failed to send message deleted log.", throwable);
return null;

View File

@@ -66,7 +66,7 @@ public class MessageEditedListener implements AsyncMessageUpdatedListener {
.member(messageAfter.getMember())
.build();
MessageToSend message = templateService.renderEmbedTemplate(MESSAGE_EDITED_TEMPLATE, lodModel, model.getServerId());
FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(message, LoggingPostTarget.EDIT_LOG, model.getServerId()))
FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(message, LoggingPostTarget.EDIT_LOG, model.getServerId()))
.exceptionally(throwable -> {
log.error("Failed to send message edited log.", throwable);
return null;
@@ -90,7 +90,7 @@ public class MessageEditedListener implements AsyncMessageUpdatedListener {
.build();
MessageToSend attachmentEmbed = templateService.renderEmbedTemplate(MESSAGE_EDITED_ATTACHMENT_REMOVED_TEMPLATE,
attachmentModel, messageBefore.getServerId());
FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(attachmentEmbed, LoggingPostTarget.DELETE_LOG, messageBefore.getServerId()))
FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(attachmentEmbed, LoggingPostTarget.DELETE_LOG, messageBefore.getServerId()))
.exceptionally(throwable -> {
log.error("Failed to send message edited attachment log.", throwable);
return null;

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>logging</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>moderation</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -0,0 +1,153 @@
package dev.sheldan.abstracto.moderation.command;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
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.service.ConfigService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import dev.sheldan.abstracto.moderation.config.ModerationModuleDefinition;
import dev.sheldan.abstracto.moderation.config.ModerationSlashCommandNames;
import dev.sheldan.abstracto.moderation.config.feature.HoneyPotFeatureConfig;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.listener.HoneyPotServiceBean;
import dev.sheldan.abstracto.moderation.model.template.command.HoneyPotBanResponseModel;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class HoneyPotBan extends AbstractConditionableCommand {
private static final String DURATION_PARAMETER = "duration";
private static final String HONEYPOT_BAN_RESPONSE = "honeypotBan_response";
private static final String HONEYPOT_BAN_COMMAND = "honeypotBan";
@Autowired
private HoneyPotBan self;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private HoneyPotServiceBean honeyPotServiceBean;
@Autowired
private InteractionService interactionService;
@Autowired
private ConfigService configService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Duration duration;
if(slashCommandParameterService.hasCommandOption(DURATION_PARAMETER, event)) {
String durationStr = slashCommandParameterService.getCommandOption(DURATION_PARAMETER, event, Duration.class, String.class);
duration = ParseUtils.parseDuration(durationStr);
} else {
Long ignoredSeconds =
configService.getLongValueOrConfigDefault(HoneyPotFeatureConfig.HONEYPOT_IGNORED_JOIN_DURATION_SECONDS, event.getGuild().getIdLong());
duration = Duration.ofSeconds(ignoredSeconds);
}
return event.deferReply(false).submit()
.thenCompose(hook -> self.banEveryHoneypotMember(hook, event.getGuild(), duration))
.thenCompose(banResponse -> self.sendResponse(banResponse))
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Transactional
public CompletableFuture<Void> sendResponse(Pair<InteractionHook, Integer> banResponse) {
HoneyPotBanResponseModel responseModel = HoneyPotBanResponseModel
.builder()
.bannedMemberCount(banResponse.getRight())
.build();
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(HONEYPOT_BAN_RESPONSE, responseModel, banResponse.getLeft()));
}
@Transactional
public CompletableFuture<Pair<InteractionHook, Integer>> banEveryHoneypotMember(InteractionHook hook, Guild guild, Duration duration) {
Instant maxJoinAge;
if(duration != null) {
maxJoinAge = Instant.now().minus(duration);
} else {
maxJoinAge = Instant.now();
}
List<Member> currentMembersWithHoneypotRole = honeyPotServiceBean.getCurrentMembersWithHoneypotRole(guild)
.stream().filter(member -> member.getTimeJoined().toInstant().isBefore(maxJoinAge))
.toList();
Role honeyPotRole = guild.getRoleById(honeyPotServiceBean.getHoneyPotRoleId(guild.getIdLong()));
List<CompletableFuture<Void>> futures = currentMembersWithHoneypotRole.stream().map(member ->
honeyPotServiceBean.banForHoneyPot(member, honeyPotRole)
).toList();
Integer memberCount = currentMembersWithHoneypotRole.size();
CompletableFutureList<Void> futureList = new CompletableFutureList<>(futures);
return futureList.getMainFuture()
.thenApply(unused -> Pair.of(hook, memberCount));
}
@Override
public CommandConfiguration getConfiguration() {
Parameter durationParameter = Parameter
.builder()
.name(DURATION_PARAMETER)
.templated(true)
.type(String.class)
.optional(true)
.build();
List<Parameter> parameters = Arrays.asList(durationParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.hasExample(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ModerationSlashCommandNames.MODERATION)
.defaultPrivilege(SlashCommandPrivilegeLevels.INVITER)
.commandName("honeypotban")
.build();
return CommandConfiguration.builder()
.name(HONEYPOT_BAN_COMMAND)
.module(ModerationModuleDefinition.MODERATION)
.templated(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.slashCommandOnly(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.HONEYPOT;
}
}

View File

@@ -61,7 +61,7 @@ public class BanModerationActionModalListener implements ModalInteractionListene
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getDurationInputId()))
.filter(modalMapping -> modalMapping.getCustomId().equals(payload.getDurationInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);
@@ -76,7 +76,7 @@ public class BanModerationActionModalListener implements ModalInteractionListene
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getReasonInputId()))
.filter(modalMapping -> modalMapping.getCustomId().equals(payload.getReasonInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);

View File

@@ -4,97 +4,44 @@ import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.sync.jda.RoleAddedListener;
import dev.sheldan.abstracto.core.models.ConditionContextInstance;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.RoleAddedModel;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
import dev.sheldan.abstracto.core.service.ConditionService;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.service.SystemCondition;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.moderation.config.feature.HoneyPotFeatureConfig;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.listener.HoneyPotReasonModel;
import dev.sheldan.abstracto.moderation.service.BanService;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.ISnowflake;
import net.dv8tion.jda.api.entities.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class HoneyPotRoleAddedListener implements RoleAddedListener {
@Autowired
private ConfigService configService;
@Autowired
private BanService banService;
@Autowired
private TemplateService templateService;
@Autowired
private ConditionService conditionService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private RoleService roleService;
private static final String HONEYPOT_BAN_REASON_TEMPLATE = "honeypot_ban_reason";
private static final String LEVEL_CONDITION_USER_ID_PARAMETER = "userId";
private static final String LEVEL_CONDITION_LEVEL_PARAMETER = "level";
private static final String LEVEL_CONDITION_SERVER_PARAMETER = "serverId";
private static final String LEVEL_CONDITION_NAME = "HAS_LEVEL";
@Autowired
private HoneyPotServiceBean honeyPotServiceBean;
@Override
public DefaultListenerResult execute(RoleAddedModel model) {
Long honeyPotRoleId = configService.getLongValueOrConfigDefault(HoneyPotFeatureConfig.HONEYPOT_ROLE_ID, model.getServerId());
Long honeyPotRoleId = honeyPotServiceBean.getHoneyPotRoleId(model.getServerId());
if(honeyPotRoleId == 0) {
log.info("Server {} has honeypot feature enabled, but still default honeypot role config - Ignoring.", model.getServerId());
return DefaultListenerResult.IGNORED;
}
if(honeyPotRoleId.equals(model.getRoleId())) {
Integer levelToSkipBan = configService.getLongValueOrConfigDefault(HoneyPotFeatureConfig.HONEYPOT_IGNORED_LEVEL, model.getServerId()).intValue();
Long amountOfSecondsToIgnore = configService.getLongValueOrConfigDefault(HoneyPotFeatureConfig.HONEYPOT_IGNORED_JOIN_DURATION_SECONDS, model.getServerId());
boolean allowed = userHasLevel(model.getTargetMember(), levelToSkipBan) || userJoinedLongerThanSeconds(model.getTargetMember(), amountOfSecondsToIgnore);
if(allowed) {
log.info("User {} in server {} has at least level {} or joined more than {} seconds ago and will not get banned by honeypot. All existing roles besides {} will be removed.",
model.getTargetUser().getUserId(), model.getTargetUser().getServerId(), levelToSkipBan, amountOfSecondsToIgnore, honeyPotRoleId);
cleanupRolesBesidesHoneyPot(model, honeyPotRoleId);
} else {
boolean fellIntoHoneyPot = honeyPotServiceBean.fellIntoHoneyPot(model.getServerId(), model.getTargetMember());
if (fellIntoHoneyPot) {
log.info("Banning user {} in guild {} due to role {}.", model.getTargetUser().getUserId(), model.getTargetUser().getServerId(), model.getRoleId());
HoneyPotReasonModel reasonModel = HoneyPotReasonModel
.builder()
.memberDisplay(MemberDisplay.fromMember(model.getTargetMember()))
.roleDisplay(RoleDisplay.fromRole(model.getRole()))
.build();
String banReason = templateService.renderTemplate(HONEYPOT_BAN_REASON_TEMPLATE, reasonModel, model.getServerId());
banService.banUserWithNotification(model.getTargetUser(), banReason, ServerUser.fromMember(model.getTargetMember().getGuild().getSelfMember()),
model.getTargetMember().getGuild(), Duration.ofDays(7)).thenAccept(banResult -> {
log.info("Banned user {} in guild {} due to role {}.", model.getTargetUser().getUserId(), model.getTargetUser().getServerId(), model.getRoleId());
}).exceptionally(throwable -> {
log.error("Failed to ban user {} in guild {} due to role {}.", model.getTargetUser().getUserId(), model.getTargetUser().getServerId(), model.getRoleId(), throwable);
return null;
});
honeyPotServiceBean.banForHoneyPot(model.getTargetMember(), model.getRole());
} else {
log.info("User {} in server {} will not get banned by honeypot. All existing roles besides {} will be removed.",
model.getTargetUser().getUserId(), model.getTargetUser().getServerId(), honeyPotRoleId);
cleanupRolesBesidesHoneyPot(model, honeyPotRoleId);
}
return DefaultListenerResult.PROCESSED;
} else {
@@ -126,28 +73,6 @@ public class HoneyPotRoleAddedListener implements RoleAddedListener {
}
}
private boolean userHasLevel(Member member, Integer level) {
log.info("Checking if member {} is ignored to click on the honeypot in server {}.", member.getIdLong(),member.getGuild().getIdLong());
Map<String, Object> parameters = new HashMap<>();
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
parameters.put(LEVEL_CONDITION_USER_ID_PARAMETER, userInAServer.getUserInServerId());
parameters.put(LEVEL_CONDITION_LEVEL_PARAMETER, level);
parameters.put(LEVEL_CONDITION_SERVER_PARAMETER, member.getGuild().getIdLong());
ConditionContextInstance contextInstance = ConditionContextInstance
.builder()
.conditionName(LEVEL_CONDITION_NAME)
.parameters(parameters)
.build();
SystemCondition.Result result = conditionService.checkConditions(contextInstance);
return SystemCondition.Result.isSuccessful(result);
}
private boolean userJoinedLongerThanSeconds(Member member, Long seconds) {
log.info("Checking if member {} joined the server more than {} seconds ago.", member.getIdLong(), seconds);
// the incorrectness of timejoined should not matter, we chunk anyway
return member.getTimeJoined().toInstant().isBefore(Instant.now().minus(seconds, ChronoUnit.SECONDS));
}
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.HONEYPOT;

View File

@@ -0,0 +1,115 @@
package dev.sheldan.abstracto.moderation.listener;
import dev.sheldan.abstracto.core.models.ConditionContextInstance;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
import dev.sheldan.abstracto.core.service.ConditionService;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.MemberService;
import dev.sheldan.abstracto.core.service.SystemCondition;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.moderation.config.feature.HoneyPotFeatureConfig;
import dev.sheldan.abstracto.moderation.model.listener.HoneyPotReasonModel;
import dev.sheldan.abstracto.moderation.service.BanService;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class HoneyPotServiceBean {
@Autowired
private ConfigService configService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private ConditionService conditionService;
@Autowired
private TemplateService templateService;
@Autowired
private BanService banService;
@Autowired
private MemberService memberService;
private static final String LEVEL_CONDITION_USER_ID_PARAMETER = "userId";
private static final String LEVEL_CONDITION_LEVEL_PARAMETER = "level";
private static final String LEVEL_CONDITION_SERVER_PARAMETER = "serverId";
private static final String LEVEL_CONDITION_NAME = "HAS_LEVEL";
private static final String HONEYPOT_BAN_REASON_TEMPLATE = "honeypot_ban_reason";
public Long getHoneyPotRoleId(Long serverId) {
return configService.getLongValueOrConfigDefault(HoneyPotFeatureConfig.HONEYPOT_ROLE_ID, serverId);
}
public boolean fellIntoHoneyPot(Long serverId, Member member) {
Integer levelToSkipBan = configService.getLongValueOrConfigDefault(HoneyPotFeatureConfig.HONEYPOT_IGNORED_LEVEL, serverId).intValue();
Long amountOfSecondsToIgnore = configService.getLongValueOrConfigDefault(HoneyPotFeatureConfig.HONEYPOT_IGNORED_JOIN_DURATION_SECONDS, serverId);
boolean allowed = userHasLevel(member, levelToSkipBan) || userJoinedLongerThanSeconds(member, amountOfSecondsToIgnore);
return !allowed;
}
public List<Member> getCurrentMembersWithHoneypotRole(Guild guild) {
return memberService.getMembersWithRole(guild.getIdLong(), getHoneyPotRoleId(guild.getIdLong()));
}
public CompletableFuture<Void> banForHoneyPot(Member targetMember, Role role) {
HoneyPotReasonModel reasonModel = HoneyPotReasonModel
.builder()
.memberDisplay(MemberDisplay.fromMember(targetMember))
.roleDisplay(RoleDisplay.fromRole(role))
.build();
ServerUser bannedUser = ServerUser.fromMember(targetMember);
String banReason = templateService.renderTemplate(HONEYPOT_BAN_REASON_TEMPLATE, reasonModel, bannedUser.getServerId());
long roleId = role.getIdLong();
return banService.banUserWithNotification(bannedUser, banReason, ServerUser.fromMember(targetMember.getGuild().getSelfMember()),
targetMember.getGuild(), Duration.ofDays(7)).thenAccept(banResult -> {
log.info("Banned user {} in guild {} due to role {}.", bannedUser.getUserId(), bannedUser.getServerId(), roleId);
}).exceptionally(throwable -> {
log.error("Failed to ban user {} in guild {} due to role {}.", bannedUser.getUserId(), bannedUser.getServerId(), roleId, throwable);
return null;
});
}
private boolean userHasLevel(Member member, Integer level) {
log.info("Checking if member {} is ignored to click on the honeypot in server {}.", member.getIdLong(),member.getGuild().getIdLong());
Map<String, Object> parameters = new HashMap<>();
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
parameters.put(LEVEL_CONDITION_USER_ID_PARAMETER, userInAServer.getUserInServerId());
parameters.put(LEVEL_CONDITION_LEVEL_PARAMETER, level);
parameters.put(LEVEL_CONDITION_SERVER_PARAMETER, member.getGuild().getIdLong());
ConditionContextInstance contextInstance = ConditionContextInstance
.builder()
.conditionName(LEVEL_CONDITION_NAME)
.parameters(parameters)
.build();
SystemCondition.Result result = conditionService.checkConditions(contextInstance);
return SystemCondition.Result.isSuccessful(result);
}
private boolean userJoinedLongerThanSeconds(Member member, Long seconds) {
log.info("Checking if member {} joined the server more than {} seconds ago.", member.getIdLong(), seconds);
// the incorrectness of timejoined should not matter, we chunk anyway
return member.getTimeJoined().toInstant().isBefore(Instant.now().minus(seconds, ChronoUnit.SECONDS));
}
}

View File

@@ -51,7 +51,7 @@ public class KickModerationActionModalListener implements ModalInteractionListen
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getReasonInputId()))
.filter(modalMapping -> modalMapping.getCustomId().equals(payload.getReasonInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);

View File

@@ -59,7 +59,7 @@ public class MuteModerationActionModalListener implements ModalInteractionListen
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getReasonInputId()))
.filter(modalMapping -> modalMapping.getCustomId().equals(payload.getReasonInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);
@@ -68,7 +68,7 @@ public class MuteModerationActionModalListener implements ModalInteractionListen
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getDurationInputId()))
.filter(modalMapping -> modalMapping.getCustomId().equals(payload.getDurationInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);

View File

@@ -60,7 +60,7 @@ public class WarnModerationActionModalListener implements ModalInteractionListen
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getReasonInputId()))
.filter(modalMapping -> modalMapping.getCustomId().equals(payload.getReasonInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);

View File

@@ -43,7 +43,7 @@ public class ReportContextModalListener implements ModalInteractionListener {
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getTextInputId()))
.filter(modalMapping -> modalMapping.getCustomId().equals(payload.getTextInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);

View File

@@ -168,14 +168,14 @@ public class BanServiceBean implements BanService {
@Transactional
public CompletableFuture<Void> sendUnBanLogMessage(UserUnBannedLogModel model, Long serverId) {
MessageToSend messageToSend = templateService.renderEmbedTemplate(USER_UN_BANNED_NOTIFICATION_TEMPLATE, model, serverId);
return FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, ModerationPostTarget.BAN_LOG, serverId));
return FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(messageToSend, ModerationPostTarget.BAN_LOG, serverId));
}
@Override
@Transactional
public CompletableFuture<Void> sendBanLogMessage(UserBannedLogModel model, Long serverId) {
MessageToSend messageToSend = templateService.renderEmbedTemplate(USER_BANNED_NOTIFICATION_TEMPLATE, model, serverId);
return FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, ModerationPostTarget.BAN_LOG, serverId));
return FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(messageToSend, ModerationPostTarget.BAN_LOG, serverId));
}
}

View File

@@ -129,7 +129,7 @@ public class InfractionServiceBean implements InfractionService {
.build();
infractionLevelChangedListenerManager.sendInfractionLevelChangedEvent(newLevel, oldLevel, currentPoints, oldPoints, ServerUser.fromAUserInAServer(aUserInAServer));
MessageToSend messageToSend = templateService.renderEmbedTemplate(INFRACTION_NOTIFICATION_TEMPLATE_KEY, model, serverId);
return FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, InfractionPostTarget.INFRACTION_NOTIFICATION, serverId));
return FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(messageToSend, InfractionPostTarget.INFRACTION_NOTIFICATION, serverId));
} else {
return CompletableFuture.completedFuture(null);
}

View File

@@ -61,7 +61,9 @@ public class KickServiceBean implements KickService {
public CompletableFuture<Void> kickMember(Member kickedMember, Member kickingMember, String reason) {
Guild guild = kickedMember.getGuild();
log.info("Kicking user {} from guild {}", kickedMember.getUser().getIdLong(), guild.getIdLong());
CompletableFuture<Void> kickFuture = guild.kick(kickedMember, reason).submit();
CompletableFuture<Void> kickFuture = guild.kick(kickedMember)
.reason(reason)
.submit();
CompletableFuture<Message> logFuture = sendKickLog(kickedMember.getUser(), ServerUser.fromMember(kickedMember), kickingMember.getUser(), ServerUser.fromMember(kickingMember), reason, guild.getIdLong());
return CompletableFuture.allOf(kickFuture, logFuture)
.thenAccept(unused -> self.storeInfraction(kickedMember, kickingMember, reason, logFuture.join(), guild.getIdLong()));
@@ -125,7 +127,7 @@ public class KickServiceBean implements KickService {
public CompletableFuture<Message> sendKicklog(KickLogModel kickLogModel, Long serverId) {
MessageToSend warnLogMessage = templateService.renderEmbedTemplate(KICK_LOG_TEMPLATE, kickLogModel, serverId);
log.debug("Sending kick log message in guild {}.", serverId);
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(warnLogMessage, ModerationPostTarget.KICK_LOG, serverId);
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(warnLogMessage, ModerationPostTarget.KICK_LOG, serverId).get(0);
return FutureUtils.toSingleFutureGeneric(messageFutures).thenApply(unused -> messageFutures.get(0).join());
}

View File

@@ -320,7 +320,7 @@ public class MuteServiceBean implements MuteService {
@Override
public CompletableFuture<Void> sendMuteLogMessage(MuteLogModel model, Long serverId) {
MessageToSend message = templateService.renderEmbedTemplate(MuteServiceBean.MUTE_LOG_TEMPLATE, model, serverId);
return FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(message, MutingPostTarget.MUTE_LOG, serverId));
return FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(message, MutingPostTarget.MUTE_LOG, serverId));
}
@Override

View File

@@ -114,7 +114,7 @@ public class ReactionReportServiceBean implements ReactionReportService {
.reportedMessage(reportedMessage)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(REACTION_REPORT_TEMPLATE_KEY, model, serverId);
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, ReactionReportPostTarget.REACTION_REPORTS, serverId);
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, ReactionReportPostTarget.REACTION_REPORTS, serverId).get(0);
return FutureUtils.toSingleFutureGeneric(messageFutures)
.thenAccept(unused -> reportMessageCreatedListenerManager.sendReportMessageCreatedEvent(reportedMessage, messageFutures.get(0).join(), anonymous ? null : reporter))
.thenAccept(unused -> {

View File

@@ -120,7 +120,7 @@ public class WarnServiceBean implements WarnService {
.reason(reason)
.build();
MessageToSend message = renderMessageModel(warnContext, guild.getIdLong());
List<CompletableFuture<Message>> futures = postTargetService.sendEmbedInPostTarget(message, WarningPostTarget.WARN_LOG, guild.getIdLong());
List<CompletableFuture<Message>> futures = postTargetService.sendEmbedInPostTarget(message, WarningPostTarget.WARN_LOG, guild.getIdLong()).get(0);
return FutureUtils.toSingleFutureGeneric(futures).thenCompose(unused -> futures.get(0));
}
@@ -363,7 +363,7 @@ public class WarnServiceBean implements WarnService {
.warnings(warnDecayWarnings)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(WARN_DECAY_LOG_TEMPLATE_KEY, warnDecayLogModel, serverId);
return FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, WarnDecayPostTarget.DECAY_LOG, server.getId()));
return FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(messageToSend, WarnDecayPostTarget.DECAY_LOG, server.getId()));
}
@Override

View File

@@ -0,0 +1,6 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,16 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<property name="moderationModule" value="(SELECT id FROM module WHERE name = 'moderation')"/>
<property name="honeypotFeature" value="(SELECT id FROM feature WHERE key = 'honeypot')"/>
<changeSet author="Sheldan" id="honeypotBan-command">
<insert tableName="command">
<column name="name" value="honeypotBan"/>
<column name="module_id" valueComputed="${moderationModule}"/>
<column name="feature_id" valueComputed="${honeypotFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,6 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="command.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -13,4 +13,5 @@
<include file="1.4.3/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.10/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.23/collection.xml" relativeToChangelogFile="true"/>
<include file="1.6.21/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>moderation</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.moderation.model.template.command;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class HoneyPotBanResponseModel {
private Integer bannedMemberCount;
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>modmail</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -44,6 +44,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling-int</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>metrics-int</artifactId>

View File

@@ -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<CommandResult> 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<Parameter> 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<CommandCondition> getConditions() {
List<CommandCondition> conditions = super.getConditions();
conditions.add(requiresModMailCondition);
return conditions;
}
}

View File

@@ -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<CommandResult> 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<Parameter> 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<CommandCondition> getConditions() {
List<CommandCondition> conditions = super.getConditions();
conditions.add(requiresModMailCondition);
return conditions;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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.getLastUpdated() != null) {
return thread.getLastUpdated();
}
return thread.getCreated();
}
private CompletableFuture<Void> 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);
}
}
}

View File

@@ -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, duration);
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, Duration configuredDuration) {
if (thread.getRemindersSnoozedUntil() != null) {
return thread.getRemindersSnoozedUntil().minus(configuredDuration);
}
return getUpdatedOrCrated(thread);
}
private static Instant getUpdatedOrCrated(ModMailThread thread) {
if(thread.getUpdated() != null) {
return thread.getUpdated();
}
return thread.getCreated();
}
private CompletableFuture<Void> sendReminder(ModMailThread modMailThread) {
List<ModMailRole> modmailRolesToPing = modMailRoleManagementService.getRolesForServer(modMailThread.getServer());
List<RoleDisplay> 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));
}
}

View File

@@ -36,6 +36,7 @@ public interface ModMailThreadRepository extends JpaRepository<ModMailThread, Lo
boolean existsByUserAndStateNot(AUserInAServer userInAServer, ModMailThreadState state);
List<ModMailThread> findByUserAndState(AUserInAServer userInAServer, ModMailThreadState state);
List<ModMailThread> findByStateNot(ModMailThreadState state);
@Override
Optional<ModMailThread> findById(@NonNull Long aLong);

View File

@@ -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;
@@ -50,6 +53,7 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
@@ -166,6 +170,9 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private List<ModmailThreadActionListener> 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 +276,45 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(MODMAIL_THREAD_CREATED_TEMPLATE_KEY, model, interactionHook));
}
@Transactional
public void checkModmailActionsForNeededActions() {
List<ModMailThread> 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.getLastUpdated())
.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 =
self.executeThreadAction(modmailThreadActionListener, 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 for thread {}.", thread.getId(), exception);
}
}
});
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public ModmailThreadActionListener.ModmailThreadActionListenerResult executeThreadAction(
ModmailThreadActionListener modmailThreadActionListener, ModmailThreadActionListenerModel model) {
return modmailThreadActionListener.execute(model);
}
/**
* 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.
@@ -332,7 +378,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
public CompletableFuture<Void> sendModMailNotification(User user, GuildMessageChannel channel, boolean appeal) {
Long serverId = channel.getGuild().getIdLong();
MessageToSend messageToSend = getModmailNotificationMessageToSend(user, channel, serverId, true, appeal);
return FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_PING, serverId));
return FutureUtils.toSingleFutureGenericList(postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_PING, serverId));
}
private MessageToSend getModmailNotificationMessageToSend(User user, GuildMessageChannel channel, Long serverId, boolean pingRole, boolean appeal) {
@@ -808,6 +854,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.
@@ -1023,7 +1090,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
ModMailThread modMailThread = modMailThreadManagementService.getById(modmailThreadId);
return channelService.sendMessageEmbedToSendToAChannel(messageToSend, modMailThread.getChannel());
} else {
return postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, model.getServerId());
return postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, model.getServerId()).get(0);
}
}
@@ -1064,7 +1131,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
ModMailLoggedMessageModel message = loadedMessages.get(i);
log.debug("Sending message {} of modmail thread {} to modmail log post target.", modMailThreadId, message.getMessage().getId());
MessageToSend messageToSend = templateService.renderEmbedTemplate("modmail_close_logged_message", message, updateMessage.getGuild().getIdLong());
List<CompletableFuture<Message>> logFuture = postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, updateMessage.getGuild().getIdLong());
List<CompletableFuture<Message>> logFuture = postTargetService.sendEmbedInPostTarget(messageToSend, ModMailPostTargets.MOD_MAIL_LOG, updateMessage.getGuild().getIdLong()).get(0);
if(i != 0 && (i % 10) == 0) {
progressModel.setLoggedMessages(i);
messageService.editMessageWithNewTemplate(updateMessage, MODMAIL_CLOSE_PROGRESS_TEMPLATE_KEY, progressModel);
@@ -1127,5 +1194,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);
}
}

View File

@@ -89,6 +89,11 @@ public class ModMailThreadManagementServiceBean implements ModMailThreadManageme
return modMailThreadRepository.findByUser(aUserInAServer);
}
@Override
public List<ModMailThread> getAllOpenThreads() {
return modMailThreadRepository.findByStateNot(ModMailThreadState.CLOSED);
}
@Override
public ModMailThread getLatestModMailThread(AUserInAServer aUserInAServer) {
return modMailThreadRepository.findTopByUserOrderByClosedDesc(aUserInAServer);
@@ -111,6 +116,7 @@ public class ModMailThreadManagementServiceBean implements ModMailThreadManageme
.user(userInAServer)
.server(userInAServer.getServerReference())
.state(ModMailThreadState.INITIAL)
.lastUpdated(Instant.now())
.updated(Instant.now())
.appeal(appeal)
.build();
@@ -123,7 +129,12 @@ 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.setLastUpdated(Instant.now());
modMailThread.setUpdated(Instant.now());
modMailThreadRepository.save(modMailThread);
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,21 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<property name="modmailModule" value="(SELECT id FROM module WHERE name = 'modmail')"/>
<property name="modmailFeature" value="(SELECT id FROM feature WHERE key = 'modmail')"/>
<changeSet author="Sheldan" id="modmail_thread_action_commands">
<insert tableName="command">
<column name="name" value="snoozeThreadReminder"/>
<column name="module_id" valueComputed="${modmailModule}"/>
<column name="feature_id" valueComputed="${modmailFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="setThreadPause"/>
<column name="module_id" valueComputed="${modmailModule}"/>
<column name="feature_id" valueComputed="${modmailFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,7 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="modmail_action_job.xml" relativeToChangelogFile="true"/>
<include file="command.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,15 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<changeSet author="Sheldan" id="modmail_action_job-insert">
<insert tableName="scheduler_job">
<column name="name" value="modmailActionJob"/>
<column name="group_name" value="modmail"/>
<column name="clazz" value="dev.sheldan.abstracto.modmail.job.ModmailThreadActionJob"/>
<column name="active" value="true"/>
<column name="cron_expression" value="0 0 0 * * ?"/>
<column name="recovery" value="false"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,15 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<changeSet author="Sheldan" id="mod_mail_thread-add_columns_for_modmail_thread_actions">
<addColumn tableName="mod_mail_thread">
<column name="reminders_snoozed_until" type="TIMESTAMP WITHOUT TIME ZONE" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="true" />
</column>
<column name="previous_state" type="VARCHAR(255)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,6 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="modmail_thread.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,6 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,12 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<changeSet author="Sheldan" id="mod_mail_thread-add_last_updated_column">
<addColumn tableName="mod_mail_thread">
<column name="last_updated" type="TIMESTAMP WITHOUT TIME ZONE" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="true" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,6 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="modmail_thread.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -5,4 +5,6 @@
<include file="1.0-modmail/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.37/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.51/collection.xml" relativeToChangelogFile="true"/>
<include file="1.6.22/collection.xml" relativeToChangelogFile="true"/>
<include file="1.6.23/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -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
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

View File

@@ -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();
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>modmail</artifactId>
<version>1.6.17-SNAPSHOT</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -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<String> 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

View File

@@ -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;

View File

@@ -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<ModmailThreadActionListenerModel, ModmailThreadActionListener.ModmailThreadActionListenerResult>, Prioritized {
enum ModmailThreadActionListenerResult implements ListenerExecutionResult {
FINAL, IGNORED, PROCESSED
}
}

View File

@@ -56,9 +56,15 @@ public class ModMailThread implements Serializable {
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
@Column(name = "last_updated")
private Instant lastUpdated;
@Column(name = "closed")
private Instant closed;
@Column(name = "reminders_snoozed_until")
private Instant remindersSnoozedUntil;
@Column(name = "appeal", nullable = false)
private Boolean appeal;
@@ -92,4 +98,8 @@ public class ModMailThread implements Serializable {
@Column(name = "state")
private ModMailThreadState state;
@Enumerated(EnumType.STRING)
@Column(name = "previous_state")
private ModMailThreadState previousState;
}

Some files were not shown because too many files have changed in this diff Show More