Compare commits

...

28 Commits

Author SHA1 Message Date
release-bot
903361cb58 [maven-release-plugin] prepare release v1.5.31 2024-03-26 21:39:59 +00:00
Sheldan
c8cf412a4a [AB-xxx] changing style of leaderboard table
preparing for release
2024-03-26 22:37:47 +01:00
release-bot
affc249012 [maven-release-plugin] prepare for next development iteration 2024-03-26 21:17:03 +00:00
release-bot
653671ea79 [maven-release-plugin] prepare release v1.5.30 2024-03-26 21:17:00 +00:00
Sheldan
7185908682 [AB-xxx] prepare for release 2024-03-26 22:13:42 +01:00
Sheldan
675da8d5d8 [AB-xxx] adding rank to leaderboard page
changing design of leaderboard page
fixing role not having an id
2024-03-26 22:13:18 +01:00
release-bot
e91becee0d [maven-release-plugin] prepare for next development iteration 2024-03-26 01:10:22 +00:00
release-bot
18732efe75 [maven-release-plugin] prepare release v1.5.29 2024-03-26 01:10:20 +00:00
Sheldan
63897fd914 [AB-xxx] prepare for release 2024-03-26 02:08:06 +01:00
Sheldan
9b865af43b [AB-xxx] fixing not serving static files 2024-03-26 02:07:25 +01:00
release-bot
b78275734c [maven-release-plugin] prepare for next development iteration 2024-03-26 00:36:30 +00:00
release-bot
00a6b0d1f8 [maven-release-plugin] prepare release v1.5.28 2024-03-26 00:36:28 +00:00
Sheldan
13fe6f5e51 [AB-xxx] prepare for release 2024-03-26 01:31:53 +01:00
Sheldan
bc0c3a18d7 [AB-xxx] initial experience leaderboard version 2024-03-26 01:28:56 +01:00
release-bot
8f9b7eba16 Commit from GitHub Actions (Publishes a new version of abstracto) 2024-03-17 12:10:52 +00:00
release-bot
48eacb2e1c [maven-release-plugin] prepare for next development iteration 2024-03-17 12:02:17 +00:00
release-bot
2168814814 [maven-release-plugin] prepare release v1.5.27 2024-03-17 12:02:16 +00:00
Sheldan
972a2829d7 [AB-xxx] updating JDA version 2024-03-17 12:59:16 +01:00
Sheldan
dbf5d99622 [AB-111] adding ability to perform moderation actions on various logging/report messages 2024-03-17 12:37:30 +01:00
Sheldan
f45721ba42 [AB-xxx] fixing schema name for scheduling not being configurable 2024-03-11 22:32:09 +01:00
Sheldan
9034968868 [AB-xxx] only executing level actions if the user changes level 2024-03-01 23:14:13 +01:00
release-bot
bcb9bacea5 [maven-release-plugin] prepare for next development iteration 2024-02-28 20:31:20 +00:00
release-bot
4b922da294 [maven-release-plugin] prepare release v1.5.26 2024-02-28 20:31:18 +00:00
Sheldan
6d90314492 [AB-xxx] prepare for release 2024-02-28 21:27:35 +01:00
Sheldan
93b02d37ed [AB-47] adding feature to define custom actions once members reach a certain level 2024-02-28 21:26:42 +01:00
Sheldan
c11ddd84ab [AB-xxx] adding character replacement to amongus text for ß 2024-02-24 10:41:03 +01:00
Sheldan
518355a68a [AB-xxx] adding creation of release to release job 2024-02-19 01:45:39 +01:00
release-bot
74ebeb4844 [maven-release-plugin] prepare for next development iteration 2024-02-18 23:22:57 +00:00
251 changed files with 20813 additions and 797 deletions

2
.env
View File

@@ -1,2 +1,2 @@
REGISTRY_PREFIX=harbor.sheldan.dev/abstracto/
VERSION=1.5.25
VERSION=1.5.31

View File

@@ -25,8 +25,18 @@ jobs:
with:
distribution: 'corretto'
java-version: 17
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: '21.x'
- name: Build with Maven
run: mvn -B install --file abstracto-application/pom.xml
- name: Install node dependencies and build
working-directory: ./ui/experience-tracking
run: npm ci
- name: Build ui application
working-directory: ./ui/experience-tracking
run: npm run build
- uses: actions/setup-ruby@v1
- name: Send Webhook Notification
if: always()

View File

@@ -1,5 +1,8 @@
name: Publishes a new version of abstracto
on: workflow_dispatch
permissions:
packages: write
contents: write
jobs:
publish:
runs-on: ubuntu-latest
@@ -12,10 +15,20 @@ jobs:
with:
distribution: 'corretto'
java-version: 17
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: '21.x'
- name: Load current version
id: version
working-directory: ./abstracto-application
run: echo "version=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec | cut -d- -f1)" >> $GITHUB_ENV
- name: Create a Release
uses: elgohr/Github-Release-Action@v5
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
title: Release of version ${{ env.version }}
- name: Release maven packages
uses: qcastel/github-actions-maven-release@v1.12.41
env:
@@ -28,6 +41,14 @@ jobs:
release-branch-name: master
maven-args: "-Dmaven.javadoc.skip=true -s settings.xml -DskipTests"
access-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install node dependencies and build
working-directory: ./ui/experience-tracking
run: npm ci
- name: Build ui application
working-directory: ./ui/experience-tracking
run: npm run build
- name: Copy built UI
run: cp -R ui/experience-tracking/build/* python/components/experience-tracking/resources/templates/experience/leaderboards/
- name: Login to Harbor
uses: docker/login-action@v2
with:

View File

@@ -12,7 +12,7 @@ An example implementation of this bot can be seen [here](https://github.com/Shel
## Technologies
* [JDA](https://github.com/DV8FromTheWorld/JDA/) The Discord API Wrapper in the version 5.0.0-beta.13
* [JDA](https://github.com/DV8FromTheWorld/JDA/) The Discord API Wrapper in the version 5.0.0-beta.21
* [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.

View File

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

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>anti-raid</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -38,6 +38,21 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>core-int</artifactId>

View File

@@ -0,0 +1,68 @@
package dev.sheldan.abstracto.experience.api;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.frontend.RoleDisplay;
import dev.sheldan.abstracto.core.service.GuildService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.experience.model.api.ExperienceConfig;
import dev.sheldan.abstracto.experience.model.api.ExperienceRoleDisplay;
import dev.sheldan.abstracto.experience.model.template.LevelRole;
import dev.sheldan.abstracto.experience.service.ExperienceRoleService;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Comparator;
import java.util.List;
@RestController
@RequestMapping(value = "/experience/v1/")
public class ExperienceConfigController {
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private ExperienceRoleService experienceRoleService;
@Autowired
private GuildService guildService;
@GetMapping(value = "/leaderboards/{serverId}/config", produces = "application/json")
public ExperienceConfig getLeaderboard(@PathVariable("serverId") Long serverId) {
AServer server = serverManagementService.loadServer(serverId);
List<LevelRole> levelRoles = experienceRoleService.loadLevelRoleConfigForServer(server);
levelRoles = levelRoles.stream().sorted(Comparator.comparingInt(LevelRole::getLevel).reversed()).toList();
Guild guild = guildService.getGuildById(serverId);
List<ExperienceRoleDisplay> roles = levelRoles
.stream()
.map(levelRole -> convertRole(levelRole, guild))
.toList();
return ExperienceConfig
.builder()
.roles(roles)
.build();
}
private ExperienceRoleDisplay convertRole(LevelRole levelRole, Guild guild) {
Role guildRole = guild.getRoleById(levelRole.getRoleId());
RoleDisplay roleDisplay;
if(guildRole != null) {
roleDisplay = RoleDisplay.fromRole(guildRole);
} else {
roleDisplay = RoleDisplay
.builder()
.id(levelRole.getRoleId())
.build();
}
return ExperienceRoleDisplay
.builder()
.level(levelRole.getLevel())
.role(roleDisplay)
.build();
}
}

View File

@@ -0,0 +1,81 @@
package dev.sheldan.abstracto.experience.api;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.frontend.RoleDisplay;
import dev.sheldan.abstracto.core.models.frontend.UserDisplay;
import dev.sheldan.abstracto.core.service.GuildService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.experience.model.api.UserExperienceDisplay;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
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.entities.UserSnowflake;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.SortDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/experience/v1")
public class LeaderboardController {
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private UserExperienceManagementService userExperienceManagementService;
@Autowired
private GuildService guildService;
@GetMapping(value = "/leaderboards/{serverId}", produces = "application/json")
public Page<UserExperienceDisplay> getLeaderboard(@PathVariable("serverId") Long serverId,
@PageableDefault(value = 25, page = 0)
@SortDefault(sort = "experience", direction = Sort.Direction.DESC)
Pageable pageable) {
AServer server = serverManagementService.loadServer(serverId);
Guild guild = guildService.getGuildById(serverId);
Page<AUserExperience> allElements = userExperienceManagementService.loadAllUsersPaginated(server, pageable);
return allElements
.map(userExperience -> convertFromUser(guild, userExperience, pageable, allElements));
}
private UserExperienceDisplay convertFromUser(Guild guild, AUserExperience aUserExperience, Pageable pageable, Page<AUserExperience> page) {
Long userId = aUserExperience.getUser().getUserReference().getId();
Member member = guild.getMember(UserSnowflake.fromId(userId));
AExperienceRole experienceRole = aUserExperience.getCurrentExperienceRole();
UserDisplay userDisplay = null;
RoleDisplay roleDisplay = null;
if(experienceRole != null) {
Role role = guild.getRoleById(experienceRole.getRole().getId());
if(role != null) {
roleDisplay = RoleDisplay.fromRole(role);
} else {
roleDisplay = RoleDisplay.fromARole(experienceRole.getRole());
}
}
if(member != null) {
userDisplay = UserDisplay.fromMember(member);
}
return UserExperienceDisplay
.builder()
.id(userId)
.messages(aUserExperience.getMessageCount())
.level(aUserExperience.getLevelOrDefault())
.rank((int) pageable.getOffset() + page.getContent().indexOf(aUserExperience) + 1)
.experience(aUserExperience.getExperience())
.role(roleDisplay)
.member(userDisplay)
.build();
}
}

View File

@@ -0,0 +1,193 @@
package dev.sheldan.abstracto.experience.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.config.FeatureMode;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandAutoCompleteService;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureMode;
import dev.sheldan.abstracto.experience.config.ExperienceSlashCommandNames;
import dev.sheldan.abstracto.experience.exception.LevelActionNotFoundException;
import dev.sheldan.abstracto.experience.listener.LevelActionListener;
import dev.sheldan.abstracto.experience.model.LevelActionPayload;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.LevelActionService;
import dev.sheldan.abstracto.experience.service.management.LevelActionManagementService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Component
@Slf4j
public class AddLevelAction extends AbstractConditionableCommand {
private static final String COMMAND_NAME = "addLevelAction";
private static final String ACTION_PARAMETER_NAME = "action";
private static final String LEVEL_PARAMETER_NAME = "level";
private static final String PARAMETER_PARAMETER_NAME = "parameter";
private static final String MEMBER_PARAMETER_NAME = "member";
private static final String RESPONSE_TEMPLATE = "addLevelAction_response";
@Autowired
private LevelActionManagementService levelActionManagementService;
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private SlashCommandAutoCompleteService slashCommandAutoCompleteService;
@Autowired
private LevelActionService levelActionService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private UserExperienceManagementService userExperienceManagementService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
String actionName = slashCommandParameterService.getCommandOption(ACTION_PARAMETER_NAME, event, String.class);
LevelActionListener listener = levelActionService.getLevelActionListenerForName(actionName)
.orElseThrow(LevelActionNotFoundException::new);
AUserInAServer aUserInAServer;
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER_NAME, event)) {
Member member = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER_NAME, event, Member.class);
aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
} else {
aUserInAServer = null;
}
Integer level = slashCommandParameterService.getCommandOption(LEVEL_PARAMETER_NAME, event, Integer.class);
String parameter = slashCommandParameterService.getCommandOption(PARAMETER_PARAMETER_NAME, event, String.class);
LevelActionPayload payload = listener.createPayload(event.getGuild(), parameter);
AServer server = serverManagementService.loadServer(event.getGuild());
log.info("Adding level action {} for level {} in server {}.", actionName, level, event.getGuild().getId());
AUserExperience userExperience = null;
if(aUserInAServer != null) {
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(aUserInAServer.getUserInServerId());
userExperience = aUserExperienceOptional.orElseGet(() -> {
AUserExperience user = userExperienceManagementService.createUserInServer(aUserInAServer);
return userExperienceManagementService.saveUser(user);
});
}
levelActionManagementService.createLevelAction(level, server, actionName, userExperience, payload);
return interactionService.replyEmbed(RESPONSE_TEMPLATE, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public List<String> performAutoComplete(CommandAutoCompleteInteractionEvent event) {
if(slashCommandAutoCompleteService.matchesParameter(event.getFocusedOption(), ACTION_PARAMETER_NAME)) {
String input = event.getFocusedOption().getValue().toLowerCase();
List<String> availableLevelActions = levelActionService.getAvailableLevelActions()
.stream()
.map(String::toLowerCase)
.toList();
if(!input.isEmpty()) {
return availableLevelActions.stream().filter(s -> s.startsWith(input)).toList();
} else {
return availableLevelActions;
}
} else {
return new ArrayList<>();
}
}
@Override
public CommandConfiguration getConfiguration() {
Parameter actionParameter = Parameter
.builder()
.name(ACTION_PARAMETER_NAME)
.templated(true)
.type(String.class)
.supportsAutoComplete(true)
.build();
Parameter levelParameter = Parameter
.builder()
.name(LEVEL_PARAMETER_NAME)
.templated(true)
.type(Integer.class)
.build();
Parameter parameterParameter = Parameter
.builder()
.name(PARAMETER_PARAMETER_NAME)
.templated(true)
.type(String.class)
.build();
Parameter memberParameter = Parameter
.builder()
.name(MEMBER_PARAMETER_NAME)
.templated(true)
.optional(true)
.type(Member.class)
.build();
List<Parameter> parameters = Arrays.asList(levelParameter, actionParameter, parameterParameter, memberParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ExperienceSlashCommandNames.EXPERIENCE_CONFIG)
.groupName("levelAction")
.commandName("add")
.build();
return CommandConfiguration.builder()
.name(COMMAND_NAME)
.module(ExperienceModuleDefinition.EXPERIENCE)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.supportsEmbedException(true)
.slashCommandOnly(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ExperienceFeatureDefinition.EXPERIENCE;
}
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(ExperienceFeatureMode.LEVEL_ACTION);
}
}

View File

@@ -0,0 +1,171 @@
package dev.sheldan.abstracto.experience.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.config.FeatureMode;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandAutoCompleteService;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureMode;
import dev.sheldan.abstracto.experience.config.ExperienceSlashCommandNames;
import dev.sheldan.abstracto.experience.exception.LevelActionNotFoundException;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.management.LevelActionManagementService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Component
@Slf4j
public class RemoveLevelAction extends AbstractConditionableCommand {
private static final String COMMAND_NAME = "removeLevelAction";
private static final String ACTION_PARAMETER_NAME = "action";
private static final String LEVEL_PARAMETER_NAME = "level";
private static final String MEMBER_PARAMETER_NAME = "member";
private static final String RESPONSE_TEMPLATE = "removeLevelAction_response";
@Autowired
private LevelActionManagementService levelActionManagementService;
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private SlashCommandAutoCompleteService slashCommandAutoCompleteService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private UserExperienceManagementService userExperienceManagementService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
String actionName = slashCommandParameterService.getCommandOption(ACTION_PARAMETER_NAME, event, String.class);
AUserExperience userExperience = null;
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER_NAME, event)) {
Member member = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER_NAME, event, Member.class);
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
userExperience = userExperienceManagementService.findUserInServer(aUserInAServer);
}
Integer level = slashCommandParameterService.getCommandOption(LEVEL_PARAMETER_NAME, event, Integer.class);
AServer server = serverManagementService.loadServer(event.getGuild());
log.info("Removing level action {} for level {} in server {}.", actionName, level, event.getGuild().getId());
if(levelActionManagementService.getLevelAction(actionName, level, server, userExperience).isEmpty()) {
throw new LevelActionNotFoundException();
}
levelActionManagementService.deleteLevelAction(level, server, actionName, userExperience);
return interactionService.replyEmbed(RESPONSE_TEMPLATE, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public List<String> performAutoComplete(CommandAutoCompleteInteractionEvent event) {
if(slashCommandAutoCompleteService.matchesParameter(event.getFocusedOption(), ACTION_PARAMETER_NAME)) {
String input = event.getFocusedOption().getValue().toLowerCase();
AServer server = serverManagementService.loadServer(event.getGuild());
Set<String> availableLevelActions = levelActionManagementService.getLevelActionsOfServer(server)
.stream()
.map(levelAction -> levelAction.getAction().toLowerCase())
.collect(Collectors.toSet());
if(!input.isEmpty()) {
return availableLevelActions.stream().filter(s -> s.startsWith(input)).toList();
} else {
return new ArrayList<>(availableLevelActions);
}
} else {
return new ArrayList<>();
}
}
@Override
public CommandConfiguration getConfiguration() {
Parameter actionParameter = Parameter
.builder()
.name(ACTION_PARAMETER_NAME)
.templated(true)
.type(String.class)
.supportsAutoComplete(true)
.build();
Parameter levelParameter = Parameter
.builder()
.name(LEVEL_PARAMETER_NAME)
.templated(true)
.type(Integer.class)
.build();
Parameter memberParameter = Parameter
.builder()
.name(MEMBER_PARAMETER_NAME)
.templated(true)
.optional(true)
.type(Member.class)
.build();
List<Parameter> parameters = Arrays.asList(levelParameter, actionParameter, memberParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ExperienceSlashCommandNames.EXPERIENCE_CONFIG)
.groupName("levelAction")
.commandName("remove")
.build();
return CommandConfiguration.builder()
.name(COMMAND_NAME)
.module(ExperienceModuleDefinition.EXPERIENCE)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.supportsEmbedException(true)
.slashCommandOnly(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ExperienceFeatureDefinition.EXPERIENCE;
}
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(ExperienceFeatureMode.LEVEL_ACTION);
}
}

View File

@@ -0,0 +1,79 @@
package dev.sheldan.abstracto.experience.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.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.FeatureMode;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureMode;
import dev.sheldan.abstracto.experience.config.ExperienceSlashCommandNames;
import dev.sheldan.abstracto.experience.model.template.LevelActionsDisplay;
import dev.sheldan.abstracto.experience.service.LevelActionService;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class ShowLevelActions extends AbstractConditionableCommand {
private static final String COMMAND_NAME = "showLevelActions";
private static final String TEMPLATE_KEY = "showLevelActions_response";
@Autowired
private InteractionService interactionService;
@Autowired
private LevelActionService levelActionService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
LevelActionsDisplay levelActionsToDisplay = levelActionService.getLevelActionsToDisplay(event.getGuild());
return interactionService.replyEmbed(TEMPLATE_KEY, levelActionsToDisplay, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ExperienceSlashCommandNames.EXPERIENCE_CONFIG)
.groupName("levelAction")
.commandName("show")
.build();
return CommandConfiguration.builder()
.name(COMMAND_NAME)
.module(ExperienceModuleDefinition.EXPERIENCE)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.supportsEmbedException(true)
.slashCommandOnly(true)
.causesReaction(true)
.help(helpInfo)
.build();
}
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(ExperienceFeatureMode.LEVEL_ACTION);
}
@Override
public FeatureDefinition getFeature() {
return ExperienceFeatureDefinition.EXPERIENCE;
}
}

View File

@@ -0,0 +1,55 @@
package dev.sheldan.abstracto.experience.listener;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import dev.sheldan.abstracto.experience.model.*;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class AddMemberToChannelLevelAction implements LevelActionListener {
public static final String ADD_MEMBER_TO_CHANNEL_ABOVE_LEVEL = "add_member_to_channel_above_level";
@Autowired
private Gson gson;
@Override
public void apply(AUserExperience userExperience, LevelAction levelAction, MemberActionModification container) {
AddMemberToChannelLevelActionPayload payload = (AddMemberToChannelLevelActionPayload) levelAction.getLoadedPayload();
log.info("Adding member {} to channel {} in server {}.", userExperience.getUser().getUserReference().getId(), payload.getChannelId(), userExperience.getServer().getId());
container.getChannelsToAdd().add(payload.getChannelId());
container.getChannelsToRemove().remove(payload.getChannelId());
}
@Override
public void prepareAction(LevelAction levelAction) {
levelAction.setLoadedPayload(gson.fromJson(levelAction.getPayload(), AddMemberToChannelLevelActionPayload.class));
}
@Override
public AddMemberToChannelLevelActionPayload createPayload(Guild guild, String input) {
GuildChannel channel = ParseUtils.parseGuildChannelFromText(input, guild);
return AddMemberToChannelLevelActionPayload
.builder()
.channelId(channel.getIdLong())
.build();
}
@Override
public String getName() {
return ADD_MEMBER_TO_CHANNEL_ABOVE_LEVEL;
}
@Override
public boolean shouldExecute(AUserExperience aUserExperience, LevelAction levelAction) {
return aUserExperience.getLevelOrDefault() >= levelAction.getLevel().getLevel();
}
}

View File

@@ -0,0 +1,55 @@
package dev.sheldan.abstracto.experience.listener;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import dev.sheldan.abstracto.experience.model.*;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class AddRoleLevelAction implements LevelActionListener {
public static final String ADD_ROLE_ABOVE_LEVEL = "add_role_above_level";
@Autowired
private Gson gson;
@Override
public void apply(AUserExperience userExperience, LevelAction levelAction, MemberActionModification container) {
AddRoleLevelActionPayload payload = (AddRoleLevelActionPayload) levelAction.getLoadedPayload();
log.info("Adding role {} to user {} in server {}.", payload.getRoleId(), userExperience.getUser().getUserReference().getId(), userExperience.getServer().getId());
container.getRolesToAdd().add(payload.getRoleId());
container.getRolesToRemove().remove(payload.getRoleId());
}
@Override
public void prepareAction(LevelAction levelAction) {
levelAction.setLoadedPayload(gson.fromJson(levelAction.getPayload(), AddRoleLevelActionPayload.class));
}
@Override
public LevelActionPayload createPayload(Guild guild, String input) {
Role role = ParseUtils.parseRoleFromText(input, guild);
return AddRoleLevelActionPayload
.builder()
.roleId(role.getIdLong())
.build();
}
@Override
public String getName() {
return ADD_ROLE_ABOVE_LEVEL;
}
@Override
public boolean shouldExecute(AUserExperience aUserExperience, LevelAction levelAction) {
return aUserExperience.getLevelOrDefault() >= levelAction.getLevel().getLevel();
}
}

View File

@@ -6,10 +6,13 @@ import dev.sheldan.abstracto.core.listener.async.jda.AsyncJoinListener;
import dev.sheldan.abstracto.core.listener.sync.jda.JoinListener;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.MemberJoinModel;
import dev.sheldan.abstracto.core.service.FeatureModeService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureMode;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.LevelActionService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
@@ -35,6 +38,12 @@ public class JoiningUserRoleListener implements AsyncJoinListener {
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private FeatureModeService featureModeService;
@Autowired
private LevelActionService levelActionService;
@Override
public DefaultListenerResult execute(MemberJoinModel model) {
if(model.getMember().isPending()) {
@@ -43,12 +52,24 @@ public class JoiningUserRoleListener implements AsyncJoinListener {
}
Optional<AUserInAServer> userInAServerOptional = userInServerManagementService.loadUserOptional(model.getServerId(), model.getJoiningUser().getUserId());
userInAServerOptional.ifPresent(aUserInAServer -> {
Optional<AUserExperience> userExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(aUserInAServer.getUserInServerId());
Long userInServerId = aUserInAServer.getUserInServerId();
Optional<AUserExperience> userExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInServerId);
if(userExperienceOptional.isPresent()) {
log.info("User {} joined {} with previous experience. Setting up experience role again (if necessary).", model.getJoiningUser().getUserId(), model.getServerId());
userExperienceService.syncForSingleUser(userExperienceOptional.get(), model.getMember(), true).thenAccept(result ->
AUserExperience aUserExperience = userExperienceOptional.get();
userExperienceService.syncForSingleUser(aUserExperience, model.getMember(), true).thenAccept(result ->
log.info("Finished re-assigning experience for re-joining user {} in server {}.", model.getJoiningUser().getUserId(), model.getServerId())
);
if(featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, aUserInAServer.getServerReference() , ExperienceFeatureMode.LEVEL_ACTION)) {
levelActionService.applyLevelActionsToUser(aUserExperience)
.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;
});
}
} else {
log.info("Joined user {} in server {} does not have any previous experience. Not setting up anything.", model.getJoiningUser().getUserId(), model.getServerId());
}

View File

@@ -5,10 +5,13 @@ import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncUpdatePendingListener;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.MemberUpdatePendingModel;
import dev.sheldan.abstracto.core.service.FeatureModeService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureMode;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.LevelActionService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
@@ -34,16 +37,34 @@ public class MemberPendingRoleListener implements AsyncUpdatePendingListener {
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private FeatureModeService featureModeService;
@Autowired
private LevelActionService levelActionService;
@Override
public DefaultListenerResult execute(MemberUpdatePendingModel model) {
Optional<AUserInAServer> userInAServerOptional = userInServerManagementService.loadUserOptional(model.getServerId(), model.getUser().getUserId());
userInAServerOptional.ifPresent(aUserInAServer -> {
Optional<AUserExperience> userExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(aUserInAServer.getUserInServerId());
Long userInServerId = aUserInAServer.getUserInServerId();
Optional<AUserExperience> userExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInServerId);
if(userExperienceOptional.isPresent()) {
log.info("User {} updated pending status {} with previous experience. Setting up experience role again (if necessary).", model.getUser().getUserId(), model.getServerId());
userExperienceService.syncForSingleUser(userExperienceOptional.get(), model.getMember(), true).thenAccept(result ->
AUserExperience aUserExperience = userExperienceOptional.get();
userExperienceService.syncForSingleUser(aUserExperience, model.getMember(), true).thenAccept(result ->
log.info("Finished re-assigning experience for update pending user {} in server {}.", model.getUser().getUserId(), model.getServerId())
);
if(featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, aUserInAServer.getServerReference() , ExperienceFeatureMode.LEVEL_ACTION)) {
levelActionService.applyLevelActionsToUser(aUserExperience)
.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;
});
}
} else {
log.info("Member updating pending {} in server {} does not have any previous experience. Not setting up anything.", model.getUser().getUserId(), model.getServerId());
}

View File

@@ -0,0 +1,55 @@
package dev.sheldan.abstracto.experience.listener;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import dev.sheldan.abstracto.experience.model.RemoveMemberFromChannelLevelActionPayload;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class RemoveMemberFromChannelLevelAction implements LevelActionListener {
public static final String REMOVE_MEMBER_FROM_CHANNEL_ABOVE_LEVEL = "remove_member_from_channel_above_level";
@Autowired
private Gson gson;
@Override
public void apply(AUserExperience userExperience, LevelAction levelAction, MemberActionModification container) {
RemoveMemberFromChannelLevelActionPayload payload = (RemoveMemberFromChannelLevelActionPayload) levelAction.getLoadedPayload();
log.info("Removing member {} from channel {} in server {}.", userExperience.getUser().getUserReference().getId(), payload.getChannelId(), userExperience.getServer().getId());
container.getChannelsToRemove().add(payload.getChannelId());
container.getChannelsToAdd().remove(payload.getChannelId());
}
@Override
public void prepareAction(LevelAction levelAction) {
levelAction.setLoadedPayload(gson.fromJson(levelAction.getPayload(), RemoveMemberFromChannelLevelActionPayload.class));
}
@Override
public RemoveMemberFromChannelLevelActionPayload createPayload(Guild guild, String input) {
GuildChannel channel = ParseUtils.parseGuildChannelFromText(input, guild);
return RemoveMemberFromChannelLevelActionPayload
.builder()
.channelId(channel.getIdLong())
.build();
}
@Override
public String getName() {
return REMOVE_MEMBER_FROM_CHANNEL_ABOVE_LEVEL;
}
@Override
public boolean shouldExecute(AUserExperience aUserExperience, LevelAction levelAction) {
return aUserExperience.getLevelOrDefault() >= levelAction.getLevel().getLevel();
}
}

View File

@@ -0,0 +1,56 @@
package dev.sheldan.abstracto.experience.listener;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import dev.sheldan.abstracto.experience.model.*;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class RemoveRoleLevelAction implements LevelActionListener {
public static final String REMOVE_ROLE_ABOVE_LEVEL = "remove_role_above_level";
@Autowired
private Gson gson;
@Override
public void apply(AUserExperience userExperience, LevelAction levelAction, MemberActionModification container) {
RemoveRoleLevelActionPayload payload = (RemoveRoleLevelActionPayload) levelAction.getLoadedPayload();
log.info("Removing role {} from user {} in server {}.", payload.getRoleId(), userExperience.getUser().getUserReference().getId(), userExperience.getServer().getId());
container.getRolesToRemove().add(payload.getRoleId());
container.getRolesToAdd().remove(payload.getRoleId());
}
@Override
public void prepareAction(LevelAction levelAction) {
levelAction.setLoadedPayload(gson.fromJson(levelAction.getPayload(), RemoveRoleLevelActionPayload.class));
}
@Override
public LevelActionPayload createPayload(Guild guild, String input) {
Role role = ParseUtils.parseRoleFromText(input, guild);
return RemoveRoleLevelActionPayload
.builder()
.roleId(role.getIdLong())
.build();
}
@Override
public String getName() {
return REMOVE_ROLE_ABOVE_LEVEL;
}
@Override
public boolean shouldExecute(AUserExperience aUserExperience, LevelAction levelAction) {
return aUserExperience.getLevelOrDefault() >= levelAction.getLevel().getLevel();
}
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.experience.model;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class AddMemberToChannelLevelActionPayload implements LevelActionPayload {
private Long channelId;
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.experience.model;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class AddRoleLevelActionPayload implements LevelActionPayload {
private Long roleId;
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.experience.model;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class RemoveMemberFromChannelLevelActionPayload implements LevelActionPayload {
private Long channelId;
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.experience.model;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class RemoveRoleLevelActionPayload implements LevelActionPayload {
private Long roleId;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.experience.model.api;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Getter
@Builder
public class ExperienceConfig {
private List<ExperienceRoleDisplay> roles;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.experience.model.api;
import dev.sheldan.abstracto.core.models.frontend.RoleDisplay;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class ExperienceRoleDisplay {
private RoleDisplay role;
private Integer level;
}

View File

@@ -0,0 +1,19 @@
package dev.sheldan.abstracto.experience.model.api;
import dev.sheldan.abstracto.core.models.frontend.RoleDisplay;
import dev.sheldan.abstracto.core.models.frontend.UserDisplay;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class UserExperienceDisplay {
private UserDisplay member;
private Long id;
private Integer rank;
private Integer level;
private Long experience;
private Long messages;
private RoleDisplay role;
}

View File

@@ -0,0 +1,22 @@
package dev.sheldan.abstracto.experience.repository;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface LevelActionRepository extends JpaRepository<LevelAction, Long> {
List<LevelAction> findByServerAndAffectedUserIsNullOrServerAndAffectedUser(AServer server, AServer server2, AUserExperience user);
Optional<LevelAction> findByServerAndActionAndLevelOrAffectedUserAndLevelAndAction(AServer server, String action, AExperienceLevel level, AUserExperience user, AExperienceLevel level2, String action2);
List<LevelAction> findByServer(AServer server);
void deleteByLevelAndActionAndServer(AExperienceLevel level, String action, AServer server);
void deleteByLevelAndActionAndAffectedUser(AExperienceLevel level, String action, AUserExperience affectedUser);
}

View File

@@ -3,6 +3,7 @@ package dev.sheldan.abstracto.experience.repository;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
@@ -24,6 +25,7 @@ public interface UserExperienceRepository extends JpaRepository<AUserExperience
* @return A complete list of {@link AUserExperience} of the given {@link AServer server}
*/
List<AUserExperience> findByUser_ServerReference(AServer server);
Page<AUserExperience> findAllByServer(AServer server, Pageable pageable);
/**
* Retrieves the {@link AUserExperience userExperience} ordered by experience, and applies the {@link Pageable pageable} to only filter out certain pages.

View File

@@ -104,6 +104,9 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
@Autowired
private FeatureModeService featureModeService;
@Autowired
private LevelActionService levelActionService;
@Autowired
private AUserExperienceServiceBean self;
@@ -306,25 +309,26 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
log.debug("User {} has a experience disable role in server {} - not giving any experience.", member.getIdLong(), serverId);
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()));
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
Long userInServerId = userInAServer.getUserInServerId();
log.debug("Handling {}. The user might gain {}.", userInServerId, gainedExperience);
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);
@@ -332,7 +336,8 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
RoleCalculationResult result = RoleCalculationResult
.builder()
.build();
if(!Objects.equals(newLevel.getLevel(), aUserExperience.getCurrentLevel().getLevel())) {
boolean userChangesLevel = !Objects.equals(newLevel.getLevel(), aUserExperience.getCurrentLevel().getLevel());
if(userChangesLevel) {
Integer oldLevel = aUserExperience.getCurrentLevel() != null ? aUserExperience.getCurrentLevel().getLevel() : 0;
log.info("User {} in server {} changed level. New {}, Old {}.", member.getIdLong(),
member.getGuild().getIdLong(), newLevel.getLevel(),
@@ -367,7 +372,17 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
aUserExperience.setCurrentExperienceRole(calculatedNewRole);
}
aUserExperience.setMessageCount(aUserExperience.getMessageCount() + 1L);
if(!aUserExperienceOptional.isPresent()) {
if(userChangesLevel && featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, server, ExperienceFeatureMode.LEVEL_ACTION)) {
levelActionService.applyLevelActionsToUser(aUserExperience)
.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())) {
@@ -375,7 +390,7 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
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(), member.getIdLong(), member.getGuild().getIdLong(), 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 {
@@ -383,7 +398,7 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
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);
log.warn("Failed to remove role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
@@ -391,7 +406,7 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
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);
log.warn("Failed to add role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}

View File

@@ -166,7 +166,10 @@ public class ExperienceRoleServiceBean implements ExperienceRoleService {
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
List<LevelRole> levelRoles = new ArrayList<>();
roles.forEach(aExperienceRole -> {
Role role = roleService.getRoleFromGuild(aExperienceRole.getRole());
Role role = null;
if(!aExperienceRole.getRole().getDeleted()) {
role = roleService.getRoleFromGuild(aExperienceRole.getRole());
}
LevelRole levelRole = LevelRole
.builder()
.role(role)

View File

@@ -0,0 +1,170 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.GuildService;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.experience.listener.LevelActionListener;
import dev.sheldan.abstracto.experience.listener.MemberActionModification;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import dev.sheldan.abstracto.experience.model.template.LevelActionDisplay;
import dev.sheldan.abstracto.experience.model.template.LevelActionsDisplay;
import dev.sheldan.abstracto.experience.service.management.LevelActionManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
@Slf4j
public class LevelActionServiceBean implements LevelActionService {
@Autowired
private LevelActionManagementService levelActionManagementService;
@Autowired(required = false)
private List<LevelActionListener> levelActions = new ArrayList<>();
@Autowired
private RoleService roleService;
@Autowired
private ChannelService channelService;
@Autowired
private GuildService guildService;
@Autowired
private ServerManagementService serverManagementService;
@Override
public CompletableFuture<Void> applyLevelActionsToUser(AUserExperience user) {
if(levelActions == null || levelActions.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
List<LevelAction> levelActionsOfUserInServer = levelActionManagementService.getLevelActionsOfUserInServer(user);
if(levelActionsOfUserInServer.isEmpty()) {
log.info("No actions available - no actions executed.");
return CompletableFuture.completedFuture(null);
}
Map<Integer, List<LevelAction>> actionConfigMap = new HashMap<>();
levelActionsOfUserInServer.forEach(levelAction -> {
if(levelAction.getLevel().getLevel() > user.getLevelOrDefault()) {
return;
}
if(actionConfigMap.containsKey(levelAction.getLevel().getLevel())) {
actionConfigMap.get(levelAction.getLevel().getLevel()).add(levelAction);
} else {
List<LevelAction> levelLevelActions = new ArrayList<>();
levelLevelActions.add(levelAction);
actionConfigMap.put(levelAction.getLevel().getLevel(), levelLevelActions);
}
});
Map<String, LevelActionListener> actionStringListenerMap = levelActions
.stream()
.collect(Collectors.toMap(a -> a.getName().toLowerCase(), Function.identity()));
List<Integer> levels = actionConfigMap
.keySet()
.stream()
.sorted()
.toList();
log.debug("Performing actions for {} levels.", levels.size());
MemberActionModification modification = MemberActionModification
.builder()
.build();
levels.forEach(level -> {
List<LevelAction> actionsOnLevel = actionConfigMap.get(level);
actionsOnLevel.forEach(levelAction -> {
LevelActionListener listener = actionStringListenerMap.get(levelAction.getAction().toLowerCase());
listener.prepareAction(levelAction);
listener.apply(user, levelAction, modification);
});
});
return evaluateModifications(user, modification);
}
private CompletableFuture<Void> evaluateModifications(AUserExperience user, MemberActionModification modification) {
List<CompletableFuture<Void>> futures = new ArrayList<>();
Long userId = user.getUser().getUserReference().getId();
log.info("Updating user {}, rolesToAdd: {}, rolesToRemove: {}",
userId, modification.getRolesToAdd().size(), modification.getRolesToRemove().size());
if(!modification.getRolesToAdd().isEmpty() || !modification.getRolesToRemove().isEmpty()) {
CompletableFuture<Void> roleFuture = roleService.updateRolesIds(user.getUser(), new ArrayList<>(modification.getRolesToAdd()), new ArrayList<>(modification.getRolesToRemove()));
futures.add(roleFuture);
}
log.info("Updating user {}, channelsToAdd: {}, channelsToRemove: {}.", userId, modification.getChannelsToAdd().size(), modification.getChannelsToRemove().size());
Guild guild = guildService.getGuildById(user.getServer().getId());
EnumSet<Permission> permissions = EnumSet.of(Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND);
modification.getChannelsToAdd().forEach(channelId -> {
futures.add(channelService.addMemberViewToChannel(guild, channelId, userId, permissions));
});
modification.getChannelsToRemove().forEach(channelId -> {
futures.add(channelService.removeChannelOverrideForMember(guild, channelId, userId));
});
if(!futures.isEmpty()) {
return new CompletableFutureList<>(futures).getMainFuture();
} else {
log.info("Actions resulted in no actions performed.");
return CompletableFuture.completedFuture(null);
}
}
@Override
public List<String> getAvailableLevelActions() {
return levelActions
.stream()
.map(LevelActionListener::getName)
.map(String::toLowerCase)
.toList();
}
@Override
public Optional<LevelActionListener> getLevelActionListenerForName(String name) {
return levelActions
.stream()
.filter(levelActionListener -> levelActionListener.getName().equalsIgnoreCase(name))
.findFirst();
}
@Override
public Optional<LevelAction> getLevelAction(AUserExperience userExperience, String action, Integer level) {
return Optional.empty();
}
@Override
public LevelActionsDisplay getLevelActionsToDisplay(Guild guild) {
AServer server = serverManagementService.loadServer(guild);
List<LevelActionDisplay> actions = levelActionManagementService.getLevelActionsOfServer(server)
.stream().map(levelAction -> LevelActionDisplay
.builder()
.actionKey(levelAction.getAction().toLowerCase())
.level(levelAction.getLevel().getLevel())
.parameters(levelAction.getPayload())
.member(levelAction.getAffectedUser() != null ? MemberDisplay.fromAUserInAServer(levelAction.getAffectedUser().getUser()) : null)
.build())
.toList();
return LevelActionsDisplay
.builder()
.actions(actions)
.build();
}
}

View File

@@ -0,0 +1,75 @@
package dev.sheldan.abstracto.experience.service.management;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.experience.model.LevelActionPayload;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import dev.sheldan.abstracto.experience.repository.LevelActionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
@Component
public class LevelActionManagementServiceBean implements LevelActionManagementService {
@Autowired
private LevelActionRepository levelActionRepository;
@Autowired
private ExperienceLevelManagementService experienceLevelManagementService;
@Autowired
private Gson gson;
@Override
public LevelAction createLevelAction(Integer level, AServer server, String action, AUserExperience user, String payload) {
AExperienceLevel experienceLevel = experienceLevelManagementService.getLevel(level);
LevelAction levelAction = LevelAction
.builder()
.action(action)
.affectedUser(user)
.payload(payload)
.server(server)
.level(experienceLevel)
.build();
return levelActionRepository.save(levelAction);
}
@Override
public void deleteLevelAction(Integer level, AServer server, String action, AUserExperience user) {
AExperienceLevel experienceLevel = experienceLevelManagementService.getLevel(level);
if(user == null) {
levelActionRepository.deleteByLevelAndActionAndServer(experienceLevel, action, server);
} else {
levelActionRepository.deleteByLevelAndActionAndAffectedUser(experienceLevel, action, user);
}
}
@Override
public LevelAction createLevelAction(Integer level, AServer server, String action, AUserExperience user, LevelActionPayload actionPayload) {
String payload = gson.toJson(actionPayload);
return createLevelAction(level, server, action, user, payload);
}
@Override
public List<LevelAction> getLevelActionsOfUserInServer(AUserExperience aUserInAServer) {
return levelActionRepository.findByServerAndAffectedUserIsNullOrServerAndAffectedUser(aUserInAServer.getServer(),
aUserInAServer.getServer(), aUserInAServer);
}
@Override
public List<LevelAction> getLevelActionsOfServer(AServer server) {
return levelActionRepository.findByServer(server);
}
@Override
public Optional<LevelAction> getLevelAction(String action, Integer level, AServer server, AUserExperience aUserExperience) {
AExperienceLevel experienceLevel = experienceLevelManagementService.getLevel(level);
return levelActionRepository.findByServerAndActionAndLevelOrAffectedUserAndLevelAndAction(server, action.toLowerCase(),
experienceLevel, aUserExperience, experienceLevel, action.toLowerCase());
}
}

View File

@@ -11,7 +11,9 @@ import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.repository.UserExperienceRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
import java.util.List;
@@ -70,6 +72,11 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
return repository.findByUser_ServerReference(server);
}
@Override
public Page<AUserExperience> loadAllUsersPaginated(AServer server, Pageable pageable) {
return repository.findAllByServer(server, pageable);
}
@Override
public List<AUserExperience> findLeaderBoardUsersPaginated(AServer aServer, Integer page, Integer size) {
return repository.findTop10ByUser_ServerReferenceOrderByExperienceDesc(aServer, PageRequest.of(page, size));

View File

@@ -13,4 +13,8 @@ abstracto.systemConfigs.expCooldownSeconds.longValue=60
abstracto.featureModes.levelUpNotification.featureName=experience
abstracto.featureModes.levelUpNotification.mode=levelUpNotification
abstracto.featureModes.levelUpNotification.enabled=false
abstracto.featureModes.levelUpNotification.enabled=false
abstracto.featureModes.levelAction.featureName=experience
abstracto.featureModes.levelAction.mode=levelAction
abstracto.featureModes.levelAction.enabled=false

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,25 @@
<?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="experienceModule" value="(SELECT id FROM module WHERE name = 'experience')"/>
<property name="experienceFeature" value="(SELECT id FROM feature WHERE key = 'experience')"/>
<changeSet author="Sheldan" id="experience-levelAction-commands">
<insert tableName="command">
<column name="name" value="addLevelAction"/>
<column name="module_id" valueComputed="${experienceModule}"/>
<column name="feature_id" valueComputed="${experienceFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="removeLevelAction"/>
<column name="module_id" valueComputed="${experienceModule}"/>
<column name="feature_id" valueComputed="${experienceFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="showLevelActions"/>
<column name="module_id" valueComputed="${experienceModule}"/>
<column name="feature_id" valueComputed="${experienceFeature}"/>
</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

@@ -0,0 +1,49 @@
<?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="level_action-table">
<createTable tableName="level_action">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_level_action"/>
</column>
<column name="action" type="VARCHAR(128)">
<constraints nullable="false"/>
</column>
<column name="payload" type="TEXT">
<constraints nullable="false"/>
</column>
<column name="affected_user_id" type="BIGINT">
<constraints nullable="true"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="level_id" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseColumnNames="level_id" baseTableName="level_action" constraintName="fk_level_action_level" deferrable="false"
initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="level"
referencedTableName="experience_level" validate="true"/>
<addForeignKeyConstraint baseColumnNames="affected_user_id" baseTableName="level_action" constraintName="fk_level_action_user" deferrable="false"
initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id"
referencedTableName="user_experience" validate="true"/>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="level_action" constraintName="fk_level_action_server" deferrable="false"
initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="server"
validate="true"/>
<sql>
DROP TRIGGER IF EXISTS level_action_update_trigger ON level_action;
CREATE TRIGGER level_action_update_trigger BEFORE UPDATE ON level_action FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS level_action_insert_trigger ON level_action;
CREATE TRIGGER level_action_insert_trigger BEFORE INSERT ON level_action FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,8 @@
<?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="level_action.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -6,4 +6,5 @@
<include file="1.2.15/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.8/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.17/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.26/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

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

View File

@@ -8,6 +8,7 @@ import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import static dev.sheldan.abstracto.experience.config.ExperienceFeatureMode.LEVEL_ACTION;
import static dev.sheldan.abstracto.experience.config.ExperienceFeatureMode.LEVEL_UP_NOTIFICATION;
/**
@@ -46,6 +47,6 @@ public class ExperienceFeatureConfig implements FeatureConfig {
@Override
public List<FeatureMode> getAvailableModes() {
return Arrays.asList(LEVEL_UP_NOTIFICATION);
return Arrays.asList(LEVEL_UP_NOTIFICATION, LEVEL_ACTION);
}
}

View File

@@ -5,7 +5,8 @@ import lombok.Getter;
@Getter
public enum ExperienceFeatureMode implements FeatureMode {
LEVEL_UP_NOTIFICATION("levelUpNotification");
LEVEL_UP_NOTIFICATION("levelUpNotification"),
LEVEL_ACTION("levelAction");
private final String key;

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.experience.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.templating.Templatable;
public class LevelActionNotFoundException extends AbstractoRunTimeException implements Templatable {
public LevelActionNotFoundException() {
super("Level action not found.");
}
@Override
public String getTemplateName() {
return "level_action_not_found_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.experience.listener;
import dev.sheldan.abstracto.experience.model.LevelActionPayload;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import net.dv8tion.jda.api.entities.Guild;
import org.springframework.stereotype.Component;
@Component
public interface LevelActionListener {
String getName();
void apply(AUserExperience userExperience, LevelAction levelAction, MemberActionModification container);
boolean shouldExecute(AUserExperience aUserExperience, LevelAction levelAction);
void prepareAction(LevelAction levelAction);
LevelActionPayload createPayload(Guild guild, String input);
}

View File

@@ -0,0 +1,23 @@
package dev.sheldan.abstracto.experience.listener;
import lombok.Builder;
import lombok.Getter;
import java.util.HashSet;
import java.util.Set;
@Getter
@Builder
public class MemberActionModification {
@Builder.Default
private Set<Long> rolesToRemove = new HashSet<>();
@Builder.Default
private Set<Long> rolesToAdd = new HashSet<>();
@Builder.Default
private Set<Long> channelsToRemove = new HashSet<>();
@Builder.Default
private Set<Long> channelsToAdd = new HashSet<>();
}

View File

@@ -0,0 +1,4 @@
package dev.sheldan.abstracto.experience.model;
public interface LevelActionPayload {
}

View File

@@ -0,0 +1,53 @@
package dev.sheldan.abstracto.experience.model.database;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.experience.model.LevelActionPayload;
import jakarta.persistence.*;
import lombok.*;
import java.time.Instant;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "level_action")
@Getter
@Setter
@EqualsAndHashCode
public class LevelAction {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "level_id", nullable = false)
private AExperienceLevel level;
@Column(name = "action", nullable = false)
private String action;
@Column(name = "payload", nullable = false)
private String payload;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "affected_user_id")
private AUserExperience affectedUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "server_id", nullable = false)
private AServer server;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
@Builder.Default
@Transient
private LevelActionPayload loadedPayload = null;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.experience.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class LevelActionDisplay {
private String actionKey;
private Integer level;
private MemberDisplay member;
private String parameters;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.experience.model.template;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Getter
@Builder
public class LevelActionsDisplay {
private List<LevelActionDisplay> actions;
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.experience.listener.LevelActionListener;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import dev.sheldan.abstracto.experience.model.template.LevelActionsDisplay;
import net.dv8tion.jda.api.entities.Guild;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public interface LevelActionService {
CompletableFuture<Void> applyLevelActionsToUser(AUserExperience user);
List<String> getAvailableLevelActions();
Optional<LevelActionListener> getLevelActionListenerForName(String name);
Optional<LevelAction> getLevelAction(AUserExperience userExperience, String action, Integer level);
LevelActionsDisplay getLevelActionsToDisplay(Guild guild);
}

View File

@@ -0,0 +1,20 @@
package dev.sheldan.abstracto.experience.service.management;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.experience.model.LevelActionPayload;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LevelAction;
import java.util.List;
import java.util.Optional;
public interface LevelActionManagementService {
LevelAction createLevelAction(Integer level, AServer server, String action, AUserExperience user, String parameters);
void deleteLevelAction(Integer level, AServer server, String action, AUserExperience user);
LevelAction createLevelAction(Integer level, AServer server, String action, AUserExperience user, LevelActionPayload actionPayload);
List<LevelAction> getLevelActionsOfUserInServer(AUserExperience aUserInAServer);
List<LevelAction> getLevelActionsOfServer(AServer server);
Optional<LevelAction> getLevelAction(String action, Integer level, AServer server, AUserExperience aUserExperience);
}

View File

@@ -6,6 +6,8 @@ import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
@@ -50,6 +52,7 @@ public interface UserExperienceManagementService {
* @return A list of {@link AUserExperience} objects associated with the given {@link AServer}
*/
List<AUserExperience> loadAllUsers(AServer server);
Page<AUserExperience> loadAllUsersPaginated(AServer server, Pageable pageable);
/**
* Retrieves a list of {@link AUserExperience} ordered by {@link AUserExperience} experience and only returns the positions between {@code start} and @{code end}.

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</version>
</parent>
<artifactId>giveaway-impl</artifactId>

View File

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

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>image-generation</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</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.5.25</version>
<version>1.5.31</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -21,6 +21,8 @@ import dev.sheldan.abstracto.invitefilter.model.template.listener.DeletedInvite;
import dev.sheldan.abstracto.invitefilter.model.template.listener.DeletedInvitesNotificationModel;
import dev.sheldan.abstracto.invitefilter.service.management.AllowedInviteLinkManagement;
import dev.sheldan.abstracto.invitefilter.service.management.FilteredInviteLinkManagement;
import dev.sheldan.abstracto.moderation.model.ModerationActionButton;
import dev.sheldan.abstracto.moderation.service.ModerationActionService;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@@ -78,6 +80,9 @@ public class InviteLinkFilterServiceBean implements InviteLinkFilterService {
@Autowired
private RoleImmunityService roleImmunityService;
@Autowired(required = false)
private ModerationActionService moderationActionService;
private static final Pattern INVITE_CODE_PATTERN = Pattern.compile("(?<code>[a-z0-9-]+)", Pattern.CASE_INSENSITIVE);
public static final String INVITE_FILTER_METRIC = "invite.filter";
@@ -230,10 +235,18 @@ public class InviteLinkFilterServiceBean implements InviteLinkFilterService {
log.info("Post target {} not defined for server {} - not sending invite link deletion notification.", InviteFilterPostTarget.INVITE_DELETE_LOG.getKey(), serverId);
return CompletableFuture.completedFuture(null);
}
boolean moderationActionsEnabled = featureModeService.featureModeActive(InviteFilterFeatureDefinition.INVITE_FILTER, serverId, InviteFilterMode.FILTER_MODERATION_ACTIONS);
List<ModerationActionButton> moderationActionComponents = new ArrayList<>();
if(moderationActionsEnabled && moderationActionService != null) {
ServerUser reportedServerUser = ServerUser.fromMember(message.getMember());
List<ModerationActionButton> moderationActions = moderationActionService.getModerationActionButtons(reportedServerUser);
moderationActionComponents.addAll(moderationActions);
}
DeletedInvitesNotificationModel model = DeletedInvitesNotificationModel
.builder()
.author(message.getMember())
.guild(message.getGuild())
.moderationActionComponents(moderationActionComponents)
.message(message)
.channel(message.getChannel())
.invites(groupInvites(codes))

View File

@@ -11,3 +11,6 @@ abstracto.featureModes.filterNotifications.featureName=inviteFilter
abstracto.featureModes.filterNotifications.mode=filterNotifications
abstracto.featureModes.filterNotifications.enabled=true
abstracto.featureModes.filterModerationActions.featureName=inviteFilter
abstracto.featureModes.filterModerationActions.mode=filterModerationActions
abstracto.featureModes.filterModerationActions.enabled=false

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>invite-filter</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.25</version>
<version>1.5.31</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -15,6 +15,12 @@
<artifactId>core-int</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>moderation-int</artifactId>
<version>1.5.31</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -23,6 +23,6 @@ public class InviteFilterFeatureConfig implements FeatureConfig {
@Override
public List<FeatureMode> getAvailableModes() {
return Arrays.asList(InviteFilterMode.FILTER_NOTIFICATIONS, InviteFilterMode.TRACK_USES);
return Arrays.asList(InviteFilterMode.values());
}
}

View File

@@ -5,7 +5,7 @@ import lombok.Getter;
@Getter
public enum InviteFilterMode implements FeatureMode {
TRACK_USES("trackUses"), FILTER_NOTIFICATIONS("filterNotifications");
TRACK_USES("trackUses"), FILTER_NOTIFICATIONS("filterNotifications"), FILTER_MODERATION_ACTIONS("filterModerationActions");
private final String key;

View File

@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.invitefilter.model.template.listener;
import dev.sheldan.abstracto.moderation.model.ModerationActionButton;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@@ -17,4 +18,5 @@ public class DeletedInvitesNotificationModel {
private Member author;
private Message message;
private List<DeletedInvite> invites;
private List<ModerationActionButton> moderationActionComponents;
}

View File

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

View File

@@ -8,7 +8,7 @@ import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.service.UserService;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.ParseUtils;
@@ -70,7 +70,7 @@ public class Ban extends AbstractConditionableCommand {
}
if(slashCommandParameterService.hasCommandOptionWithFullType(USER_PARAMETER, event, OptionType.USER)) {
Member member = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, Member.class);
return banService.banUserWithNotification(member.getUser(), reason, event.getMember(), duration)
return banService.banUserWithNotification(ServerUser.fromMember(member), reason, ServerUser.fromMember(event.getMember()), event.getGuild(), duration)
.thenCompose(banResult -> {
if(banResult == NOTIFICATION_FAILED) {
String errorNotification = templateService.renderSimpleTemplate(BAN_NOTIFICATION_NOT_POSSIBLE, event.getGuild().getIdLong());
@@ -84,7 +84,7 @@ public class Ban extends AbstractConditionableCommand {
String userIdStr = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, User.class, String.class);
Long userId = Long.parseLong(userIdStr);
return userService.retrieveUserForId(userId)
.thenCompose(user -> banService.banUserWithNotification(user, reason, event.getMember(), duration))
.thenCompose(user -> banService.banUserWithNotification(ServerUser.fromId(event.getGuild().getIdLong(), userId), reason, ServerUser.fromMember(event.getMember()), event.getGuild(), duration))
.thenCompose(banResult -> {
if(banResult == NOTIFICATION_FAILED) {
String errorNotification = templateService.renderSimpleTemplate(BAN_NOTIFICATION_NOT_POSSIBLE, event.getGuild().getIdLong());

View File

@@ -2,20 +2,21 @@ package dev.sheldan.abstracto.moderation.command;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.condition.CommandCondition;
import dev.sheldan.abstracto.core.command.config.*;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.EffectConfig;
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.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.EntityGuildMismatchException;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.moderation.config.ModerationModuleDefinition;
import dev.sheldan.abstracto.moderation.config.ModerationSlashCommandNames;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.template.command.KickLogModel;
import dev.sheldan.abstracto.moderation.service.KickServiceBean;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
@@ -47,30 +48,6 @@ public class Kick extends AbstractConditionableCommand {
@Autowired
private InteractionService interactionService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
Member member = (Member) parameters.get(0);
if(!member.getGuild().equals(commandContext.getGuild())) {
throw new EntityGuildMismatchException();
}
String defaultReason = templateService.renderSimpleTemplate(KICK_DEFAULT_REASON_TEMPLATE, commandContext.getGuild().getIdLong());
String reason = parameters.size() == 2 ? (String) parameters.get(1) : defaultReason;
KickLogModel kickLogModel = KickLogModel
.builder()
.kickedUser(member)
.reason(reason)
.guild(commandContext.getGuild())
.channel(commandContext.getChannel())
.member(commandContext.getAuthor())
.build();
kickLogModel.setKickedUser(member);
kickLogModel.setReason(reason);
return kickService.kickMember(member, reason, kickLogModel)
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Member member = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, Member.class);
@@ -84,17 +61,7 @@ public class Kick extends AbstractConditionableCommand {
reason = templateService.renderSimpleTemplate(KICK_DEFAULT_REASON_TEMPLATE, event.getGuild().getIdLong());
}
KickLogModel kickLogModel = KickLogModel
.builder()
.kickedUser(member)
.reason(reason)
.guild(event.getGuild())
.channel(event.getGuildChannel())
.member(event.getMember())
.build();
kickLogModel.setKickedUser(member);
kickLogModel.setReason(reason);
return kickService.kickMember(member, reason, kickLogModel)
return kickService.kickMember(member, event.getMember(), reason)
.thenCompose(unused -> interactionService.replyEmbed(KICK_RESPONSE, event))
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@@ -143,6 +110,7 @@ public class Kick extends AbstractConditionableCommand {
.slashCommandConfig(slashCommandConfig)
.supportsEmbedException(true)
.async(true)
.slashCommandOnly(true)
.effects(effectConfig)
.causesReaction(true)
.parameters(parameters)

View File

@@ -10,12 +10,17 @@ import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParame
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.EntityGuildMismatchException;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.models.ServerChannelMessage;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.service.ChannelService;
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.core.utils.SnowflakeUtils;
import dev.sheldan.abstracto.moderation.config.ModerationModuleDefinition;
import dev.sheldan.abstracto.moderation.config.ModerationSlashCommandNames;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.template.command.MuteContext;
import dev.sheldan.abstracto.moderation.service.MuteService;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
@@ -24,17 +29,18 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static dev.sheldan.abstracto.moderation.model.MuteResult.NOTIFICATION_FAILED;
import static dev.sheldan.abstracto.moderation.service.MuteService.MUTE_EFFECT_KEY;
@Component
public class Mute extends AbstractConditionableCommand {
private static final String MUTE_DEFAULT_REASON_TEMPLATE = "mute_default_reason";
public static final String MUTE_NOTIFICATION_NOT_POSSIBLE_TEMPLATE_KEY = "mute_notification_not_possible";
private static final String DURATION_PARAMETER = "duration";
private static final String MUTE_COMMAND = "mute";
private static final String USER_PARAMETER = "user";
@@ -53,6 +59,9 @@ public class Mute extends AbstractConditionableCommand {
@Autowired
private InteractionService interactionService;
@Autowired
private ChannelService channelService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
@@ -64,15 +73,19 @@ public class Mute extends AbstractConditionableCommand {
Duration duration = (Duration) parameters.get(1);
String defaultReason = templateService.renderSimpleTemplate(MUTE_DEFAULT_REASON_TEMPLATE, guild.getIdLong());
String reason = parameters.size() == 3 ? (String) parameters.get(2) : defaultReason;
MuteContext muteLogModel = MuteContext
.builder()
.muteTargetDate(Instant.now().plus(duration))
.mutedUser(member)
.channelId(commandContext.getChannel().getIdLong())
.reason(reason)
.mutingUser(commandContext.getAuthor())
.build();
return muteService.muteMemberWithLog(muteLogModel)
ServerUser userToMute = ServerUser.fromMember(member);
ServerUser mutingUser = ServerUser.fromMember(commandContext.getAuthor());
Long serverId = commandContext.getGuild().getIdLong();
ServerChannelMessage serverChannelMessage = ServerChannelMessage.fromMessage(commandContext.getMessage());
return muteService.muteMemberWithLog(userToMute, mutingUser, reason, duration, commandContext.getGuild(), serverChannelMessage)
.thenCompose(muteResult -> {
if(muteResult == NOTIFICATION_FAILED) {
MessageToSend errorNotification = templateService.renderEmbedTemplate(MUTE_NOTIFICATION_NOT_POSSIBLE_TEMPLATE_KEY, new Object(), serverId);
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(errorNotification, commandContext.getChannel()));
} else {
return CompletableFuture.completedFuture(null);
}
})
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@@ -88,16 +101,23 @@ public class Mute extends AbstractConditionableCommand {
} else {
reason = templateService.renderSimpleTemplate(MUTE_DEFAULT_REASON_TEMPLATE, guild.getIdLong());
}
MuteContext muteLogModel = MuteContext
Long serverId = event.getGuild().getIdLong();
ServerChannelMessage commandMessage = ServerChannelMessage
.builder()
.muteTargetDate(Instant.now().plus(duration))
.mutedUser(targetMember)
.reason(reason)
.serverId(serverId)
.channelId(event.getChannel().getIdLong())
.mutingUser(event.getMember())
.messageId(SnowflakeUtils.createSnowFlake())
.build();
return muteService.muteMemberWithLog(muteLogModel)
.thenCompose(unused -> interactionService.replyEmbed(MUTE_RESPONSE, event))
ServerUser userToMute = ServerUser.fromMember(targetMember);
ServerUser mutingUser = ServerUser.fromMember(event.getMember());
return muteService.muteMemberWithLog(userToMute, mutingUser, reason, duration, event.getGuild(), commandMessage)
.thenCompose(muteResult -> {
if(muteResult == NOTIFICATION_FAILED) {
return interactionService.replyEmbed(MUTE_NOTIFICATION_NOT_POSSIBLE_TEMPLATE_KEY, new Object(), event);
} else {
return interactionService.replyEmbed(MUTE_RESPONSE, event);
}
})
.thenApply(aVoid -> CommandResult.fromSuccess());
}

View File

@@ -11,6 +11,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.service.UserService;
import dev.sheldan.abstracto.moderation.config.ModerationModuleDefinition;
import dev.sheldan.abstracto.moderation.config.ModerationSlashCommandNames;
@@ -50,7 +51,7 @@ public class UnBan extends AbstractConditionableCommand {
String userIdStr = (String) parameters.get(0);
Long userId = Long.parseLong(userIdStr);
return userService.retrieveUserForId(userId)
.thenCompose(user -> banService.unBanUserWithNotification(user, commandContext.getAuthor()))
.thenCompose(user -> banService.unBanUserWithNotification(userId, ServerUser.fromMember(commandContext.getAuthor()), commandContext.getGuild()))
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@@ -59,7 +60,7 @@ public class UnBan extends AbstractConditionableCommand {
String userIdStr = slashCommandParameterService.getCommandOption(USER_PARAMETER, event, String.class);
Long userId = Long.parseLong(userIdStr);
return userService.retrieveUserForId(userId)
.thenCompose(user -> banService.unBanUserWithNotification(user, event.getMember()))
.thenCompose(user -> banService.unBanUserWithNotification(userId, ServerUser.fromMember(event.getMember()), event.getGuild()))
.thenCompose(unused -> interactionService.replyEmbed(UN_BAN_RESPONSE, event))
.thenApply(interactionHook -> CommandResult.fromSuccess());
}

View File

@@ -11,7 +11,7 @@ import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParame
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.EntityGuildMismatchException;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.moderation.config.ModerationModuleDefinition;
import dev.sheldan.abstracto.moderation.config.ModerationSlashCommandNames;
@@ -52,8 +52,9 @@ public class UnMute extends AbstractConditionableCommand {
if(!member.getGuild().equals(commandContext.getGuild())) {
throw new EntityGuildMismatchException();
}
AUserInAServer userToUnMute = userInServerManagementService.loadOrCreateUser(member);
return muteService.unMuteUser(userToUnMute, commandContext.getAuthor()).thenApply(aVoid ->
ServerUser userToUnmute = ServerUser.fromMember(member);
ServerUser unMutingMember = ServerUser.fromMember(commandContext.getAuthor());
return muteService.unMuteUser(userToUnmute, unMutingMember, commandContext.getGuild()).thenApply(aVoid ->
CommandResult.fromSuccess()
);
}
@@ -64,8 +65,9 @@ public class UnMute extends AbstractConditionableCommand {
if(!targetMember.getGuild().equals(event.getGuild())) {
throw new EntityGuildMismatchException();
}
AUserInAServer userToUnMute = userInServerManagementService.loadOrCreateUser(targetMember);
return muteService.unMuteUser(userToUnMute, event.getMember())
ServerUser userToUnmute = ServerUser.fromMember(targetMember);
ServerUser unMutingMember = ServerUser.fromMember(event.getMember());
return muteService.unMuteUser(userToUnmute, unMutingMember, event.getGuild())
.thenCompose(unused -> interactionService.replyEmbed(UN_MUTE_RESPONSE, event))
.thenApply(interactionHook -> CommandResult.fromSuccess());
}

View File

@@ -10,10 +10,12 @@ import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParame
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.EntityGuildMismatchException;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.models.ServerChannelMessage;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.utils.SnowflakeUtils;
import dev.sheldan.abstracto.moderation.config.ModerationModuleDefinition;
import dev.sheldan.abstracto.moderation.config.ModerationSlashCommandNames;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.template.command.WarnContext;
import dev.sheldan.abstracto.moderation.service.WarnService;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import lombok.extern.slf4j.Slf4j;
@@ -59,16 +61,8 @@ public class Warn extends AbstractConditionableCommand {
}
String defaultReason = templateService.renderSimpleTemplate(WARN_DEFAULT_REASON_TEMPLATE, commandContext.getGuild().getIdLong());
String reason = parameters.size() == 2 ? (String) parameters.get(1) : defaultReason;
WarnContext warnLogModel = WarnContext
.builder()
.reason(reason)
.warnedMember(member)
.channel(commandContext.getChannel())
.member(commandContext.getAuthor())
.guild(commandContext.getGuild())
.message(commandContext.getMessage())
.build();
return warnService.warnUserWithLog(warnLogModel)
ServerChannelMessage commandMessage = ServerChannelMessage.fromMessage(commandContext.getMessage());
return warnService.warnUserWithLog(commandContext.getGuild(), ServerUser.fromMember(member), ServerUser.fromMember(commandContext.getAuthor()), reason, commandMessage)
.thenApply(warning -> CommandResult.fromSuccess());
}
@@ -84,15 +78,13 @@ public class Warn extends AbstractConditionableCommand {
} else {
reason = templateService.renderSimpleTemplate(WARN_DEFAULT_REASON_TEMPLATE, event.getGuild().getIdLong());
}
WarnContext warnLogModel = WarnContext
ServerChannelMessage commandMessage = ServerChannelMessage
.builder()
.reason(reason)
.warnedMember(member)
.member(event.getMember())
.channel(event.getGuildChannel())
.guild(event.getGuild())
.serverId(event.getGuild().getIdLong())
.channelId(event.getChannel().getIdLong())
.messageId(SnowflakeUtils.createSnowFlake())
.build();
return warnService.warnUserWithLog(warnLogModel)
return warnService.warnUserWithLog(event.getGuild(), ServerUser.fromMember(member), ServerUser.fromMember(event.getMember()), reason, commandMessage)
.thenCompose(unused -> interactionService.replyEmbed(WARN_RESPONSE, event))
.thenApply(warning -> CommandResult.fromSuccess());
}

View File

@@ -0,0 +1,114 @@
package dev.sheldan.abstracto.moderation.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.ComponentService;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListener;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerModel;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.interaction.modal.ModalConfigPayload;
import dev.sheldan.abstracto.core.interaction.modal.ModalService;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.interaction.ModerationActionBanPayload;
import dev.sheldan.abstracto.moderation.model.template.listener.ModerationActionBanModalModel;
import dev.sheldan.abstracto.moderation.model.template.listener.ModerationActionPayloadModel;
import dev.sheldan.abstracto.moderation.service.ModerationActionServiceBean;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class BanModerationActionListener implements ButtonClickedListener {
@Autowired
private ModalService modalService;
@Autowired
private ComponentService componentService;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@Autowired
private BanModerationActionListener self;
private static final String BAN_REASON_MODERATION_ACTION_MODAL = "moderationAction_ban";
public static final String BAN_MODAL_ORIGIN = "BAN_MODERATION_ACTION_ORIGIN";
@Override
public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
ModerationActionPayloadModel payload = (ModerationActionPayloadModel) model.getDeserializedPayload();
if(ModerationActionServiceBean.BAN_ACTION.equals(payload.getAction())) {
log.info("Handling ban button interaction by user {} in server {} for user {}.",
payload.getUser().getUserId(), payload.getUser().getServerId(), model.getEvent().getMember().getIdLong());
String modalId = componentService.generateComponentId();
String reasonInputId = componentService.generateComponentId();
String durationInputId = componentService.generateComponentId();
ModerationActionBanModalModel modalModel = ModerationActionBanModalModel
.builder()
.modalId(modalId)
.durationComponentId(durationInputId)
.reasonComponentId(reasonInputId)
.build();
modalService.replyModal(model.getEvent(), BAN_REASON_MODERATION_ACTION_MODAL, modalModel).thenAccept(unused -> {
log.info("Returned ban reason moderation action modal for user {} towards user {} in server {}.",
payload.getUser().getUserId(), model.getEvent().getMember().getIdLong(), model.getServerId());
self.persistBanModerationActionPayload(payload.getUser(), reasonInputId, modalId);
}).exceptionally(throwable -> {
log.error("Failed to show modal for ban moderation action.", throwable);
return null;
});
return ButtonClickedListenerResult.ACKNOWLEDGED;
} else {
return ButtonClickedListenerResult.IGNORED;
}
}
@Transactional
public void persistBanModerationActionPayload(ServerUser userToBan, String reasonInput, String modalId) {
ModerationActionBanPayload payload = ModerationActionBanPayload
.builder()
.bannedUserId(userToBan.getUserId())
.serverId(userToBan.getServerId())
.reasonInputId(reasonInput)
.modalId(modalId)
.build();
ModalConfigPayload payloadConfig = ModalConfigPayload
.builder()
.modalPayload(payload)
.origin(BAN_MODAL_ORIGIN)
.payloadType(payload.getClass())
.modalId(modalId)
.build();
componentPayloadManagementService.createModalPayload(payloadConfig, userToBan.getServerId());
}
@Override
public Boolean handlesEvent(ButtonClickedListenerModel model) {
if(ModerationActionServiceBean.MODERATION_ACTION_ORIGIN.equals(model.getOrigin())){
ModerationActionPayloadModel payload = (ModerationActionPayloadModel) model.getDeserializedPayload();
return ModerationActionServiceBean.BAN_ACTION.equals(payload.getAction());
}
return false;
}
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.MODERATION;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
@Override
public Boolean autoAcknowledgeEvent() {
return false;
}
}

View File

@@ -0,0 +1,113 @@
package dev.sheldan.abstracto.moderation.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.InteractionExceptionService;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListener;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListenerModel;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListenerResult;
import dev.sheldan.abstracto.core.models.ServerUser;
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.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.interaction.ModerationActionBanPayload;
import dev.sheldan.abstracto.moderation.service.BanService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.interactions.modals.ModalMapping;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
@Slf4j
public class BanModerationActionModalListener implements ModalInteractionListener {
private static final String KICK_MODERATION_ACTION_MODAL_RESPONSE_TEMPLATE = "moderationAction_ban_response";
private static final String DEFAULT_BAN_REASON_TEMPLATE_KEY = "ban_default_reason";
@Autowired
private BanService banService;
@Autowired
private InteractionService interactionService;
@Autowired
private InteractionExceptionService interactionExceptionService;
@Autowired
private TemplateService templateService;
@Override
public Boolean handlesEvent(ModalInteractionListenerModel model) {
return BanModerationActionListener.BAN_MODAL_ORIGIN.equals(model.getOrigin());
}
@Override
public ModalInteractionListenerResult execute(ModalInteractionListenerModel model) {
ModerationActionBanPayload payload = (ModerationActionBanPayload) model.getDeserializedPayload();
ServerUser userBeingBanned = ServerUser
.builder()
.userId(payload.getBannedUserId())
.serverId(payload.getServerId())
.build();
ServerUser kickingUser = ServerUser.fromMember(model.getEvent().getMember());
String duration = model
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getDurationInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);
Duration messageDeletionDuration;
if(duration != null) {
messageDeletionDuration = ParseUtils.parseDuration(duration.trim());
} else {
messageDeletionDuration = null;
}
String reason;
String tempReason = model
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getDurationInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);
if(StringUtils.isBlank(tempReason)) {
reason = templateService.renderSimpleTemplate(DEFAULT_BAN_REASON_TEMPLATE_KEY);
} else {
reason = tempReason;
}
log.debug("Handling ban moderation action modal interaction by user {} in server {}.", kickingUser.getUserId(), kickingUser.getServerId());
model.getEvent().deferReply(true).queue(interactionHook -> {
banService.banUserWithNotification(userBeingBanned, reason, kickingUser, model.getEvent().getGuild(), messageDeletionDuration)
.thenCompose((future) -> FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(KICK_MODERATION_ACTION_MODAL_RESPONSE_TEMPLATE, new Object(), model.getEvent().getInteraction().getHook())))
.thenAccept(unused -> {
log.info("Kicked user {} from server {}. Performed by user {}.", userBeingBanned.getUserId(), kickingUser.getServerId(), kickingUser.getUserId());
}).exceptionally(throwable -> {
interactionExceptionService.reportExceptionToInteraction(throwable, model, this);
log.error("Failed to kick user {} from server {}. Performed by user {}.", userBeingBanned.getUserId(), kickingUser.getServerId(), kickingUser.getUserId(), throwable);
return null;
});
});
return ModalInteractionListenerResult.ACKNOWLEDGED;
}
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.MODERATION;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
}

View File

@@ -5,6 +5,7 @@ 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;
@@ -23,6 +24,7 @@ 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;
@@ -77,7 +79,8 @@ public class HoneyPotRoleAddedListener implements RoleAddedListener {
.roleDisplay(RoleDisplay.fromRole(model.getRole()))
.build();
String banReason = templateService.renderTemplate(HONEYPOT_BAN_REASON_TEMPLATE, reasonModel);
banService.banUserWithNotification(model.getTargetMember().getUser(), banReason, model.getTargetMember().getGuild().getSelfMember(), null).thenAccept(banResult -> {
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);

View File

@@ -0,0 +1,111 @@
package dev.sheldan.abstracto.moderation.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.ComponentService;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListener;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerModel;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.interaction.modal.ModalConfigPayload;
import dev.sheldan.abstracto.core.interaction.modal.ModalService;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.interaction.ModerationActionKickPayload;
import dev.sheldan.abstracto.moderation.model.template.listener.ModerationActionKickModalModel;
import dev.sheldan.abstracto.moderation.model.template.listener.ModerationActionPayloadModel;
import dev.sheldan.abstracto.moderation.service.ModerationActionServiceBean;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class KickModerationActionListener implements ButtonClickedListener {
@Autowired
private ModalService modalService;
@Autowired
private ComponentService componentService;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@Autowired
private KickModerationActionListener self;
private static final String KICK_REASON_MODERATION_ACTION_MODAL = "moderationAction_kick";
public static final String KICK_MODAL_ORIGIN = "KICK_MODERATION_ACTION_ORIGIN";
@Override
public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
ModerationActionPayloadModel payload = (ModerationActionPayloadModel) model.getDeserializedPayload();
if(ModerationActionServiceBean.KICK_ACTION.equals(payload.getAction())) {
log.info("Handling kick button interaction by user {} in server {} for user {}.",
payload.getUser().getUserId(), payload.getUser().getServerId(), model.getEvent().getMember().getIdLong());
String modalId = componentService.generateComponentId();
String reasonInputId = componentService.generateComponentId();
ModerationActionKickModalModel modalModel = ModerationActionKickModalModel
.builder()
.modalId(modalId)
.reasonComponentId(reasonInputId)
.build();
modalService.replyModal(model.getEvent(), KICK_REASON_MODERATION_ACTION_MODAL, modalModel).thenAccept(unused -> {
log.info("Returned kick reason moderation action modal for user {} towards user {} in server {}.",
payload.getUser().getUserId(), model.getEvent().getMember().getIdLong(), model.getServerId());
self.persistKickModerationActionPayload(payload.getUser(), reasonInputId, modalId);
}).exceptionally(throwable -> {
log.error("Failed to show modal for kick moderation action.", throwable);
return null;
});
return ButtonClickedListenerResult.ACKNOWLEDGED;
} else {
return ButtonClickedListenerResult.IGNORED;
}
}
@Transactional
public void persistKickModerationActionPayload(ServerUser userToKick, String reasonInput, String modalId) {
ModerationActionKickPayload payload = ModerationActionKickPayload
.builder()
.kickedUserId(userToKick.getUserId())
.serverId(userToKick.getServerId())
.reasonInputId(reasonInput)
.modalId(modalId)
.build();
ModalConfigPayload payloadConfig = ModalConfigPayload
.builder()
.modalPayload(payload)
.origin(KICK_MODAL_ORIGIN)
.payloadType(payload.getClass())
.modalId(modalId)
.build();
componentPayloadManagementService.createModalPayload(payloadConfig, userToKick.getServerId());
}
@Override
public Boolean handlesEvent(ButtonClickedListenerModel model) {
if(ModerationActionServiceBean.MODERATION_ACTION_ORIGIN.equals(model.getOrigin())){
ModerationActionPayloadModel payload = (ModerationActionPayloadModel) model.getDeserializedPayload();
return ModerationActionServiceBean.KICK_ACTION.equals(payload.getAction());
}
return false;
}
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.MODERATION;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
@Override
public Boolean autoAcknowledgeEvent() {
return false;
}
}

View File

@@ -0,0 +1,83 @@
package dev.sheldan.abstracto.moderation.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.InteractionExceptionService;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListener;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListenerModel;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListenerResult;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.interaction.ModerationActionKickPayload;
import dev.sheldan.abstracto.moderation.service.KickService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.interactions.modals.ModalMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class KickModerationActionModalListener implements ModalInteractionListener {
private static final String KICK_MODERATION_ACTION_MODAL_RESPONSE_TEMPLATE = "moderationAction_kick_response";
@Autowired
private KickService kickService;
@Autowired
private InteractionService interactionService;
@Autowired
private InteractionExceptionService interactionExceptionService;
@Override
public Boolean handlesEvent(ModalInteractionListenerModel model) {
return KickModerationActionListener.KICK_MODAL_ORIGIN.equals(model.getOrigin());
}
@Override
public ModalInteractionListenerResult execute(ModalInteractionListenerModel model) {
ModerationActionKickPayload payload = (ModerationActionKickPayload) model.getDeserializedPayload();
ServerUser userBeingKicked = ServerUser
.builder()
.userId(payload.getKickedUserId())
.serverId(payload.getServerId())
.build();
ServerUser kickingUser = ServerUser.fromMember(model.getEvent().getMember());
String reason = model
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getReasonInputId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);
log.debug("Handling kick moderation action modal interaction by user {} in server {}.", kickingUser.getUserId(), kickingUser.getServerId());
model.getEvent().deferReply(true).queue(interactionHook -> {
kickService.kickMember(model.getEvent().getGuild(), userBeingKicked, reason, kickingUser)
.thenCompose((future) -> FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(KICK_MODERATION_ACTION_MODAL_RESPONSE_TEMPLATE, new Object(), model.getEvent().getInteraction().getHook())))
.thenAccept(unused -> {
log.info("Kicked user {} from server {}. Performed by user {}.", userBeingKicked.getUserId(), kickingUser.getServerId(), kickingUser.getUserId());
}).exceptionally(throwable -> {
interactionExceptionService.reportExceptionToInteraction(throwable, model, this);
log.error("Failed to kick user {} from server {}. Performed by user {}.", userBeingKicked.getUserId(), kickingUser.getServerId(), kickingUser.getUserId(), throwable);
return null;
});
});
return ModalInteractionListenerResult.ACKNOWLEDGED;
}
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.MODERATION;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
}

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncMemberTimeoutUpdatedListener;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.listener.MemberTimeoutUpdatedModel;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.service.MemberService;
import dev.sheldan.abstracto.core.service.PostTargetService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
@@ -116,8 +117,8 @@ public class MemberTimeoutListener implements AsyncMemberTimeoutUpdatedListener
.builder()
.muteTargetDate(model.getNewTimeout() != null ? model.getNewTimeout().toInstant() : null)
.oldMuteTargetDate(model.getOldTimeout() != null ? model.getOldTimeout().toInstant() : null)
.mutingUser(future.isCompletedExceptionally() ? null : future.join())
.mutedUser(model.getMember())
.mutingUser(future.isCompletedExceptionally() ? null : MemberDisplay.fromMember(future.join()))
.mutedUser(MemberDisplay.fromMember(model.getMember()))
.reason(reason)
.build();
MessageToSend message = templateService.renderEmbedTemplate(MuteServiceBean.MUTE_LOG_TEMPLATE, muteLogModel, guild.getIdLong());

View File

@@ -0,0 +1,114 @@
package dev.sheldan.abstracto.moderation.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.ComponentService;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListener;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerModel;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.interaction.modal.ModalConfigPayload;
import dev.sheldan.abstracto.core.interaction.modal.ModalService;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.moderation.config.feature.ModerationFeatureDefinition;
import dev.sheldan.abstracto.moderation.model.interaction.ModerationActionMutePayload;
import dev.sheldan.abstracto.moderation.model.template.listener.ModerationActionMuteModalModel;
import dev.sheldan.abstracto.moderation.model.template.listener.ModerationActionPayloadModel;
import dev.sheldan.abstracto.moderation.service.ModerationActionServiceBean;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class MuteModerationActionListener implements ButtonClickedListener {
@Autowired
private ComponentService componentService;
@Autowired
private ModalService modalService;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@Autowired
private MuteModerationActionListener self;
private static final String MUTE_REASON_MODERATION_ACTION_MODAL = "moderationAction_mute";
public static final String MUTE_MODAL_ORIGIN = "MUTE_MODERATION_ACTION_ORIGIN";
@Override
public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
ModerationActionPayloadModel payload = (ModerationActionPayloadModel) model.getDeserializedPayload();
if(ModerationActionServiceBean.MUTE_ACTION.equals(payload.getAction())) {
log.info("Handling mute button interaction by user {} in server {} for user {}.",
payload.getUser().getUserId(), payload.getUser().getServerId(), model.getEvent().getMember().getIdLong());
String modalId = componentService.generateComponentId();
String reasonInputId = componentService.generateComponentId();
String durationInputId = componentService.generateComponentId();
ModerationActionMuteModalModel modalModel = ModerationActionMuteModalModel
.builder()
.modalId(modalId)
.durationComponentId(durationInputId)
.reasonComponentId(reasonInputId)
.build();
modalService.replyModal(model.getEvent(), MUTE_REASON_MODERATION_ACTION_MODAL, modalModel).thenAccept(unused -> {
log.info("Returned mute reason moderation action modal for user {} towards user {} in server {}.",
payload.getUser().getUserId(), model.getEvent().getMember().getIdLong(), model.getServerId());
self.persistMuteModerationActionPayload(payload.getUser(), reasonInputId, modalId, durationInputId);
}).exceptionally(throwable -> {
log.error("Failed to show modal for mute moderation action.", throwable);
return null;
});
return ButtonClickedListenerResult.ACKNOWLEDGED;
} else {
return ButtonClickedListenerResult.IGNORED;
}
}
@Transactional
public void persistMuteModerationActionPayload(ServerUser userToMute, String reasonInput, String modalId, String durationInputId) {
ModerationActionMutePayload payload = ModerationActionMutePayload
.builder()
.mutedUserId(userToMute.getUserId())
.serverId(userToMute.getServerId())
.reasonInputId(reasonInput)
.durationInputId(durationInputId)
.modalId(modalId)
.build();
ModalConfigPayload payloadConfig = ModalConfigPayload
.builder()
.modalPayload(payload)
.origin(MUTE_MODAL_ORIGIN)
.payloadType(payload.getClass())
.modalId(modalId)
.build();
componentPayloadManagementService.createModalPayload(payloadConfig, userToMute.getServerId());
}
@Override
public Boolean handlesEvent(ButtonClickedListenerModel model) {
if(ModerationActionServiceBean.MODERATION_ACTION_ORIGIN.equals(model.getOrigin())){
ModerationActionPayloadModel payload = (ModerationActionPayloadModel) model.getDeserializedPayload();
return ModerationActionServiceBean.MUTE_ACTION.equals(payload.getAction());
}
return false;
}
@Override
public FeatureDefinition getFeature() {
return ModerationFeatureDefinition.MUTING;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
@Override
public Boolean autoAcknowledgeEvent() {
return false;
}
}

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