Compare commits

...

83 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
Sheldan
c791c063e3 [AB-xxx] changing the structure of starboard post model 2025-11-22 15:28:00 +01:00
release-bot
d293d764db Commit from GitHub Actions (Publishes a new version of abstracto) 2025-11-12 22:53:03 +00:00
release-bot
a7cd674cdd [maven-release-plugin] prepare for next development iteration 2025-11-12 22:39:42 +00:00
release-bot
0abefe64e9 [maven-release-plugin] prepare release v1.6.16 2025-11-12 22:39:40 +00:00
Sheldan
d1267605c1 [AB-xxx] adding roles to the model of leave logger 2025-11-12 23:33:50 +01:00
release-bot
b4ffea341c Commit from GitHub Actions (Publishes a new version of abstracto) 2025-10-12 18:28:32 +00:00
release-bot
507c755809 [maven-release-plugin] prepare for next development iteration 2025-10-12 18:15:34 +00:00
release-bot
9e0e92a530 [maven-release-plugin] prepare release v1.6.15 2025-10-12 18:15:32 +00:00
Sheldan
d078b3fa87 [AB-xxx] adding feature to suggest slash commands for message commands, if the command is slash command only 2025-10-12 20:10:14 +02:00
release-bot
71b7dd2383 Commit from GitHub Actions (Publishes a new version of abstracto) 2025-09-10 23:10:18 +00:00
release-bot
ea0384490e [maven-release-plugin] prepare for next development iteration 2025-09-10 22:56:04 +00:00
release-bot
b5dbc0b1ed [maven-release-plugin] prepare release v1.6.14 2025-09-10 22:56:02 +00:00
Sheldan
e265eb6760 [AB-xxx] adding user agent for all outgoing okhttp requests 2025-09-11 00:52:49 +02:00
release-bot
2f18b7431d Commit from GitHub Actions (Publishes a new version of abstracto) 2025-09-10 22:05:18 +00:00
release-bot
2d8827fa81 [maven-release-plugin] prepare for next development iteration 2025-09-10 21:53:38 +00:00
release-bot
dddeb15127 [maven-release-plugin] prepare release v1.6.13 2025-09-10 21:53:37 +00:00
Sheldan
78fbe0723b [AB-xxx] actively restricting length of slash command parameters if a validator is configured for the parameter (more easy solution than a separate one) 2025-09-10 23:46:39 +02:00
release-bot
e1d4a41d60 Commit from GitHub Actions (Publishes a new version of abstracto) 2025-08-04 18:59:22 +00:00
release-bot
354a82f806 [maven-release-plugin] prepare for next development iteration 2025-08-04 18:43:45 +00:00
release-bot
ff3e3d85ba [maven-release-plugin] prepare release v1.6.12 2025-08-04 18:43:43 +00:00
Sheldan
46bf4fbc42 [AB-xxx] changing the method used to edit the giveaway message 2025-08-02 00:06:43 +02:00
Sheldan
97ac25dbb6 [AB-xxx] adding auto complete for feature names/feature mode names 2025-08-01 23:52:15 +02:00
Sheldan
ef4bdb2ab2 [AB-xxx] enabling commands which take users to only require one parameter instead of a string and a member option 2025-07-29 23:04:33 +02:00
release-bot
433fdb7068 Commit from GitHub Actions (Publishes a new version of abstracto) 2025-07-20 09:04:03 +00:00
release-bot
e59b6269e1 [maven-release-plugin] prepare for next development iteration 2025-07-20 08:49:59 +00:00
release-bot
8077501584 [maven-release-plugin] prepare release v1.6.11 2025-07-20 08:49:57 +00:00
Sheldan
cb9ab8f542 [AB-xxx] adding ability to define unique ids for components, a color of the container, disabled state for multiple components and spoiler for container
fixing reminder message not containing a link to the message anymore
2025-07-19 23:56:06 +02:00
release-bot
f513f8890b Commit from GitHub Actions (Publishes a new version of abstracto) 2025-07-13 20:08:48 +00:00
release-bot
6f02834b75 [maven-release-plugin] prepare for next development iteration 2025-07-13 19:56:17 +00:00
release-bot
75456b45c9 [maven-release-plugin] prepare release v1.6.10 2025-07-13 19:56:15 +00:00
Sheldan
92e581305e [AB-xxx] changing logic to replace embedded messages with the link instead of removing the reaction/button for deletion 2025-07-13 21:52:37 +02:00
release-bot
bd8b57e977 Commit from GitHub Actions (Publishes a new version of abstracto) 2025-07-13 18:05:19 +00:00
release-bot
e5fc411dc2 [maven-release-plugin] prepare for next development iteration 2025-07-13 17:52:51 +00:00
release-bot
9d9fdef42e [maven-release-plugin] prepare release v1.6.9 2025-07-13 17:52:49 +00:00
Sheldan
4a3d43b1b0 [AB-xxx] adding initial support for components v2
fixing issue with buttons which only provide an emoji
adding logging in case updating a starboard post goes wrong
2025-07-13 19:45:59 +02:00
release-bot
15d41c58ef Commit from GitHub Actions (Publishes a new version of abstracto) 2025-05-29 20:16:51 +00:00
release-bot
ad863af5d6 [maven-release-plugin] prepare for next development iteration 2025-05-29 20:05:41 +00:00
release-bot
f3dae2f6a3 [maven-release-plugin] prepare release v1.6.8 2025-05-29 20:05:40 +00:00
Sheldan
9f9c0612eb [AB-xxx] fixing youtube sometimes not returning videos even if specified 2025-05-29 22:00:45 +02:00
Sheldan
db73071a71 [AB-xxx] updating JDA version 2025-04-26 13:11:36 +02:00
Sheldan
d7125fbf25 [AB-xxx] adding input to response model for 8ball and choose command 2025-04-26 13:09:34 +02:00
release-bot
fd3bf41406 Commit from GitHub Actions (Publishes a new version of abstracto) 2025-04-01 11:47:09 +00:00
release-bot
d1abe194ec [maven-release-plugin] prepare for next development iteration 2025-04-01 11:37:04 +00:00
release-bot
79477923c6 [maven-release-plugin] prepare release v1.6.7 2025-04-01 11:37:03 +00:00
Sheldan
be9ffa3045 [AB-xxx] updating postgres driver version 2025-04-01 13:34:34 +02:00
release-bot
e29ceb9243 [maven-release-plugin] prepare for next development iteration 2025-03-31 21:49:19 +00:00
release-bot
70acf46cfd [maven-release-plugin] prepare release v1.6.6 2025-03-31 21:49:17 +00:00
Sheldan
022603ae3b [AB-xxx] updating JDA version in readme 2025-03-31 23:46:56 +02:00
release-bot
264082d63d [maven-release-plugin] prepare for next development iteration 2025-03-31 21:32:13 +00:00
277 changed files with 4189 additions and 1062 deletions

2
.env
View File

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

View File

@@ -11,7 +11,7 @@ This repository does not provide the full functionality in order to start a disc
## Technologies
* [JDA](https://github.com/DV8FromTheWorld/JDA/) The Discord API Wrapper in the version 5.0.0-beta.21
* [JDA](https://github.com/DV8FromTheWorld/JDA/) The Discord API Wrapper in the version 5.4.0
* [Spring boot](https://github.com/spring-projects/spring-boot) is used as a framework to create standalone application in Java with Java EE methods. (including dependency injection and more)
* [Hibernate](https://github.com/hibernate/hibernate-orm) is used as a reference implementation of JPA.
* [Freemarker](https://github.com/apache/freemarker) is used as a templating engine. This is used to provide internationalization for user facing text and enable dynamic embed configuration.
@@ -30,4 +30,4 @@ If you want to view the documentation to an earlier released version you need to
If you find any issue, feel free to create a GitHub issue.
## License
This project is licensed under the MIT license.
This project is licensed under the MIT license.

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>anti-raid</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.5</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.5</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.5</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.5</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -74,7 +74,7 @@ public class CreateAssignableRolePlace extends AbstractConditionableCommand {
@Override
public CommandConfiguration getConfiguration() {
List<ParameterValidator> rolePlaceNameValidator = Arrays.asList(MaxStringLengthValidator.max(AssignableRolePlace.ASSIGNABLE_PLACE_NAME_LIMIT));
List<ParameterValidator> rolePlaceNameValidator = Arrays.asList(MaxStringLengthValidator.max(AssignableRolePlace.ASSIGNABLE_PLACE_NAME_LIMIT.intValue()));
Parameter rolePostName = Parameter
.builder()
.name(ASSIGNABLE_ROLE_PLACE_NAME_PARAMETER)
@@ -95,7 +95,7 @@ public class CreateAssignableRolePlace extends AbstractConditionableCommand {
.templated(true)
.optional(true)
.build();
List<ParameterValidator> rolePlaceDescriptionValidator = Arrays.asList(MaxStringLengthValidator.max(AssignableRolePlace.ASSIGNABLE_PLACE_NAME_LIMIT));
List<ParameterValidator> rolePlaceDescriptionValidator = Arrays.asList(MaxStringLengthValidator.max(AssignableRolePlace.ASSIGNABLE_PLACE_NAME_LIMIT.intValue()));
Parameter text = Parameter
.builder()
.name(ASSIGNABLE_ROLE_PLACE_TEXT_PARAMETER)

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.interactions.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.5</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.5</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.5</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.5</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.5</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.5</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.5</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.5</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.5</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -52,6 +52,7 @@ public class Choose extends AbstractConditionableCommand {
String choice = entertainmentService.takeChoice(choices, commandContext.getAuthor());
ChooseResponseModel responseModel = ChooseResponseModel
.builder()
.choices(choices)
.chosenValue(choice)
.build();
return FutureUtils.toSingleFutureGeneric(channelService.sendEmbedTemplateInMessageChannel(CHOOSE_RESPONSE_TEMPLATE_KEY, responseModel, commandContext.getChannel()))
@@ -70,6 +71,7 @@ public class Choose extends AbstractConditionableCommand {
String choice = entertainmentService.takeChoice(choices, event.getMember());
ChooseResponseModel responseModel = ChooseResponseModel
.builder()
.choices(choices)
.chosenValue(choice)
.build();
return interactionService.replyEmbed(CHOOSE_RESPONSE_TEMPLATE_KEY, responseModel, event)

View File

@@ -56,6 +56,7 @@ public class EightBall extends AbstractConditionableCommand {
String chosenKey = entertainmentService.getEightBallValue(text);
EightBallResponseModel responseModel = EightBallResponseModel
.builder()
.input(text)
.chosenKey(chosenKey)
.build();
return templateService.renderEmbedTemplate(EIGHT_BALL_RESPONSE_TEMPLATE_KEY, responseModel, serverId);

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.5</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.entertainment.model.command;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@@ -9,4 +10,5 @@ import lombok.Setter;
@Builder
public class ChooseResponseModel {
private String chosenValue;
private List<String> choices;
}

View File

@@ -9,4 +9,5 @@ import lombok.Setter;
@Builder
public class EightBallResponseModel {
private String chosenKey;
private String input;
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.5</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.5</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -97,7 +97,7 @@ public class LeaderBoardCommand extends AbstractConditionableCommand {
} else {
leaderBoard = userExperienceService.findLeaderBoardData(server, page);
}
List<CompletableFuture> futures = new ArrayList<>();
List<CompletableFuture<?>> futures = new ArrayList<>();
CompletableFuture<List<LeaderBoardEntryModel>> completableFutures = converter.fromLeaderBoard(leaderBoard, actorUser.getGuild().getIdLong());
futures.add(completableFutures);
log.info("Rendering leaderboard for page {} in server {} for user {}.", page, actorUser.getId(), actorUser.getGuild().getId());
@@ -158,7 +158,6 @@ public class LeaderBoardCommand extends AbstractConditionableCommand {
Parameter focusMe = Parameter
.builder()
.name(FOCUS_PARAMETER)
.validators(leaderBoardPageValidators)
.optional(true)
.slashCommandOnly(true)
.templated(true)

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.5</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.5</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.5</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());
@@ -155,7 +155,7 @@ public class GiveawayServiceBean implements GiveawayService {
Long giveawayId = giveaway.getGiveawayId().getId();
log.info("Adding giveaway participating of user {} to giveaway {} in server {}.", member.getIdLong(), giveawayId, member.getGuild().getIdLong());
MessageToSend messageToSend = templateService.renderEmbedTemplate(GIVEAWAY_MESSAGE_TEMPLATE_KEY, giveawayMessageModel, member.getGuild().getIdLong());
return channelService.editEmbedMessageInAChannel(messageToSend.getEmbeds().get(0), messageChannel, giveaway.getMessageId())
return channelService.editMessageInAChannelFuture(messageToSend, messageChannel, giveaway.getMessageId())
.thenAccept(message -> {
self.persistAddedParticipant(member, giveawayId);
});

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>giveaway</artifactId>
<version>1.6.5</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.5</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.5</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.5</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.5</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.5</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.5</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.5</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.5</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.5</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

@@ -11,6 +11,7 @@ import dev.sheldan.abstracto.core.interaction.button.ButtonConfigModel;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.linkembed.config.LinkEmbedFeatureDefinition;
import dev.sheldan.abstracto.linkembed.config.LinkEmbedFeatureMode;
import dev.sheldan.abstracto.linkembed.model.template.MessageEmbedCleanupReplacementModel;
import dev.sheldan.abstracto.linkembed.model.template.MessageEmbedDeleteButtonPayload;
import dev.sheldan.abstracto.linkembed.model.template.MessageEmbeddedModel;
import dev.sheldan.abstracto.core.service.*;
@@ -18,7 +19,6 @@ import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.linkembed.model.MessageEmbedLink;
import dev.sheldan.abstracto.linkembed.model.database.EmbeddedMessage;
@@ -26,8 +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;
@@ -49,6 +52,7 @@ public class MessageEmbedServiceBean implements MessageEmbedService {
private final Pattern messageRegex = Pattern.compile("(?<whole>(?:https?://)?(?:\\w+\\.)?discord(?:app)?\\.com/channels/(?<server>\\d+)/(?<channel>\\d+)/(?<message>\\d+)(?:.*?))+", Pattern.CASE_INSENSITIVE);
public static final String MESSAGE_EMBED_TEMPLATE = "message_embed";
private static final String MESSAGE_EMBED_CLEANUP_REPLACEMENT_TEMPLATE = "message_embed_cleanup_replacement";
public static final String REMOVAL_EMOTE = "removeEmbed";
@Autowired
@@ -78,9 +82,6 @@ public class MessageEmbedServiceBean implements MessageEmbedService {
@Autowired
private ReactionService reactionService;
@Autowired
private MessageService messageService;
@Autowired
private ComponentService componentServiceBean;
@@ -160,15 +161,21 @@ public class MessageEmbedServiceBean implements MessageEmbedService {
return CompletableFuture.completedFuture(null);
}
log.info("Cleaning up {} embedded embeddedMessages", embeddedMessages.size());
List<ServerChannelMessage> reactionChannelMessages = embeddedMessages.stream()
.filter(embeddedMessage -> embeddedMessage.getDeletionComponentId() == null)
.map(this::convertEmbedMessageToServerChannelMessage)
.collect(Collectors.toList());
List<Pair<ServerChannelMessage, ServerChannelMessage>> embeddingMessages = embeddedMessages.stream()
.map(embeddedMessage -> Pair.of(ServerChannelMessage
.builder()
.serverId(embeddedMessage.getEmbeddingServer().getId())
.channelId(embeddedMessage.getEmbeddingChannel().getId())
.messageId(embeddedMessage.getEmbeddingMessageId())
.build(),
ServerChannelMessage
.builder().
serverId(embeddedMessage.getEmbeddedServer().getId())
.channelId(embeddedMessage.getEmbeddedChannel().getId())
.messageId(embeddedMessage.getEmbeddedMessageId())
.build()))
.toList();
List<ServerChannelMessage> buttonChannelMessages = embeddedMessages.stream()
.filter(embeddedMessage -> embeddedMessage.getDeletionComponentId() != null)
.map(this::convertEmbedMessageToServerChannelMessage)
.collect(Collectors.toList());
List<Long> embeddedMessagesHandled = embeddedMessages
.stream()
.map(EmbeddedMessage::getEmbeddingMessageId)
@@ -179,67 +186,35 @@ public class MessageEmbedServiceBean implements MessageEmbedService {
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
List<CompletableFuture<Message>> reactionMessageFutures = messageService.retrieveMessages(reactionChannelMessages);
List<CompletableFuture<Message>> buttonMessageFutures = messageService.retrieveMessages(buttonChannelMessages);
CompletableFutureList<Message> reactionFutureList = new CompletableFutureList<>(reactionMessageFutures);
CompletableFutureList<Message> buttonFutureList = new CompletableFutureList<>(buttonMessageFutures);
return reactionFutureList.getMainFuture()
.handle((unused, throwable) -> {
if(throwable != null) {
log.warn("Embedded messages reaction message loading failed.", throwable);
}
return self.removeReactions(reactionFutureList.getObjects());
})
.thenCompose(Function.identity())
.thenCompose(unused -> buttonFutureList.getMainFuture())
.handle((unused, throwable) -> {
if(throwable != null) {
log.warn("Embedded messages button message loading failed.", throwable);
}
return self.removeButtons(buttonFutureList.getObjects());
})
// deleting the messages from db regardless of exceptions, at most the reaction remains
.thenCompose(Function.identity())
.whenComplete((unused, throwable) -> {
if(throwable != null) {
log.warn("Embedded message button clearing failed.", throwable);
}
self.deleteEmbeddedMessages(embeddedMessagesHandled, componentPayloadsToDelete);
})
.exceptionally(throwable -> {
log.error("Failed to clean up embedded messages.", throwable);
return null;
});
}
public CompletableFuture<Void> removeButtons(List<Message> messages) {
List<CompletableFuture<Void>> removalFutures = new ArrayList<>();
messages.forEach(message -> removalFutures.add(componentServiceBean.clearButtons(message)));
return FutureUtils.toSingleFutureGeneric(removalFutures);
}
private ServerChannelMessage convertEmbedMessageToServerChannelMessage(EmbeddedMessage embeddedMessage) {
return ServerChannelMessage
List<CompletableFuture<Message>> editList = embeddingMessages.stream().map(messagePair -> {
ServerChannelMessage embeddingMessage = messagePair.getLeft();
ServerChannelMessage embeddedMessage = messagePair.getRight();
MessageEmbedCleanupReplacementModel model = MessageEmbedCleanupReplacementModel
.builder()
.serverId(embeddedMessage.getEmbeddingServer().getId())
.channelId(embeddedMessage.getEmbeddingChannel().getId())
.messageId(embeddedMessage.getEmbeddingMessageId())
.message(embeddedMessage)
.build();
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);
}
self.deleteEmbeddedMessages(embeddedMessagesHandled, componentPayloadsToDelete);
});
}
@Transactional
public CompletableFuture<Void> removeReactions(List<Message> allMessages) {
List<CompletableFuture<Void>> removalFutures = new ArrayList<>();
Map<Long, List<Message>> groupedPerServer = allMessages
.stream()
.collect(Collectors.groupingBy(message -> message.getGuild().getIdLong()));
groupedPerServer.forEach((serverId, serverMessages) -> {
// we assume the emote remained the same
CompletableFutureList<Void> removalFuture = reactionService.removeReactionFromMessagesWithFutureWithFutureList(serverMessages, REMOVAL_EMOTE);
removalFutures.add(removalFuture.getMainFuture());
});
return FutureUtils.toSingleFutureGeneric(removalFutures);
}
@Transactional
public void deleteEmbeddedMessages(List<Long> embeddedMessagesToDelete, List<String> componentPayloadsToDelete) {
@@ -357,7 +332,9 @@ public class MessageEmbedServiceBean implements MessageEmbedService {
private Boolean shouldMentionReferencedAuthor(Message message) {
if(message.getReferencedMessage() != null) {
return message.getMentions().getMentions(Message.MentionType.USER).contains(message.getReferencedMessage().getAuthor());
return message.getMentions().getMentions(Message.MentionType.USER)
.stream()
.anyMatch(user -> message.getReferencedMessage().getAuthor().getIdLong() == user.getIdLong());
}
return false;
}

View File

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

View File

@@ -0,0 +1,11 @@
package dev.sheldan.abstracto.linkembed.model.template;
import dev.sheldan.abstracto.core.models.ServerChannelMessage;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class MessageEmbedCleanupReplacementModel {
private ServerChannelMessage message;
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.5</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.5</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

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncLeaveListener;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.listener.MemberLeaveModel;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
import dev.sheldan.abstracto.core.models.template.display.UserDisplay;
import dev.sheldan.abstracto.core.service.PostTargetService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
@@ -13,6 +14,7 @@ import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.logging.config.LoggingFeatureDefinition;
import dev.sheldan.abstracto.logging.config.LoggingPostTarget;
import dev.sheldan.abstracto.logging.model.template.MemberLeaveLogModel;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -41,14 +43,21 @@ public class LeaveLogger implements AsyncLeaveListener {
.userId(listenerModel.getUser().getIdLong())
.serverId(listenerModel.getServerId())
.build();
List<RoleDisplay> roles = listenerModel
.getMember()
.getRoles()
.stream()
.map(RoleDisplay::fromRole)
.toList();
MemberLeaveLogModel model = MemberLeaveLogModel
.builder()
.leavingUser(leavingUser)
.user(UserDisplay.fromUser(listenerModel.getUser()))
.roles(roles)
.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.5</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -1,7 +1,9 @@
package dev.sheldan.abstracto.logging.model.template;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
import dev.sheldan.abstracto.core.models.template.display.UserDisplay;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@@ -12,4 +14,5 @@ import lombok.Setter;
public class MemberLeaveLogModel {
private ServerUser leavingUser;
private UserDisplay user;
private List<RoleDisplay> roles;
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.6.5</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.5</version>
<version>1.6.24</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -23,7 +23,6 @@ import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -76,16 +75,15 @@ public class Ban extends AbstractConditionableCommand {
duration = null;
}
if(slashCommandParameterService.hasCommandOptionWithFullType(USER_PARAMETER, event, OptionType.USER)) {
Member member = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, Member.class);
Member member = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, Member.class);
if(member != null) {
return event.deferReply().submit()
.thenCompose((hook) -> self.banMember(event, member, reason, duration, hook))
.thenApply(commandResult -> CommandResult.fromSuccess());
} else {
String userIdStr = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, String.class);
Long userId = Long.parseLong(userIdStr);
User user = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, User.class);
return event.deferReply().submit()
.thenCompose((hook) -> self.banViaUserId(event, userId, reason, duration, hook))
.thenCompose((hook) -> self.banViaUserId(event, user.getIdLong(), reason, duration, hook))
.thenApply(commandResult -> CommandResult.fromSuccess());
}
}

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

@@ -33,7 +33,6 @@ import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -86,16 +85,15 @@ public class Infractions extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> showInfractions(InteractionHook hook, SlashCommandInteractionEvent event) {
List<Infraction> infractions;
Guild guild = hook.getInteraction().getGuild();
if(slashCommandParameterService.hasCommandOptionWithFullType(USER_PARAMETER, event, OptionType.USER)) {
Member member = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, Member.class);
Member member = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, Member.class);
User user = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, User.class);
if(member != null) {
if(!member.getGuild().equals(guild)) {
throw new EntityGuildMismatchException();
}
infractions = infractionManagementService.getInfractionsForUser(userInServerManagementService.loadOrCreateUser(member));
} else if(slashCommandParameterService.hasCommandOptionWithFullType(USER_PARAMETER, event, OptionType.STRING)){
String userIdStr = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, String.class);
Long userId = Long.parseLong(userIdStr);
AUserInAServer userInServer = userInServerManagementService.loadOrCreateUser(guild.getIdLong(), userId);
} else if(user != null){
AUserInAServer userInServer = userInServerManagementService.loadOrCreateUser(guild.getIdLong(), user.getIdLong());
infractions = infractionManagementService.getInfractionsForUser(userInServer);
} else {

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.5</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.5</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.5</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>

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