sxcybot

OSRS oriented Discord Bot
git clone git://git.wimdupont.com/sxcybot.git
Log | Files | Refs | README | LICENSE

commit c579c4f923193c16397840719ad0b45170b3c2c0
parent 4755eaa04cac6b3936626238d8cae92d65667532
Author: Wim Dupont <wim@wimdupont.com>
Date:   Wed, 24 Jul 2024 21:51:08 +0200

added chat mod

Diffstat:
Mpom.xml | 2+-
Msrc/main/java/com/wimdupont/sxcybot/SxcyBotApplication.java | 18+++++++-----------
Msrc/main/java/com/wimdupont/sxcybot/listeners/AdminCommandListener.java | 91+++++++++++++++++++++++++++++++------------------------------------------------
Asrc/main/java/com/wimdupont/sxcybot/listeners/ChatListener.java | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main/java/com/wimdupont/sxcybot/listeners/CommandListener.java | 73++++++++++++++++++++-----------------------------------------------------
Msrc/main/java/com/wimdupont/sxcybot/listeners/admin/RoleAssignListener.java | 3+--
Msrc/main/java/com/wimdupont/sxcybot/listeners/admin/banlist/AddBanlistUserListener.java | 2+-
Msrc/main/java/com/wimdupont/sxcybot/listeners/admin/banlist/DeleteBanlistUserListener.java | 2+-
Msrc/main/java/com/wimdupont/sxcybot/listeners/admin/pvmrole/AddPvmListener.java | 2+-
Msrc/main/java/com/wimdupont/sxcybot/listeners/admin/pvmrole/DeletePvmListener.java | 2+-
Msrc/main/java/com/wimdupont/sxcybot/listeners/admin/role/AddRoleListener.java | 12+++++++++---
Msrc/main/java/com/wimdupont/sxcybot/listeners/admin/role/DeleteRoleListener.java | 2+-
Msrc/main/java/com/wimdupont/sxcybot/listeners/admin/rule/DeleteRuleListener.java | 2+-
Asrc/main/java/com/wimdupont/sxcybot/repository/guild/ChatDataRepository.java | 13+++++++++++++
Asrc/main/java/com/wimdupont/sxcybot/repository/guild/dao/ChatData.java | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main/java/com/wimdupont/sxcybot/services/CleanupScheduler.java | 21++++++++++++++++++---
Msrc/main/java/com/wimdupont/sxcybot/services/MailService.java | 14+++++++-------
Asrc/main/java/com/wimdupont/sxcybot/services/guild/ChatModService.java | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/resources/db/migration/V1_19__chatmod.sql | 9+++++++++
Msrc/main/resources/releasenotes.csv | 2+-
20 files changed, 464 insertions(+), 142 deletions(-)

diff --git a/pom.xml b/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>3.3.0</version> + <version>3.3.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.wimdupont</groupId> diff --git a/src/main/java/com/wimdupont/sxcybot/SxcyBotApplication.java b/src/main/java/com/wimdupont/sxcybot/SxcyBotApplication.java @@ -1,8 +1,7 @@ package com.wimdupont.sxcybot; import com.wimdupont.sxcybot.enums.Command; -import com.wimdupont.sxcybot.listeners.AdminCommandListener; -import com.wimdupont.sxcybot.listeners.CommandListener; +import com.wimdupont.sxcybot.listeners.ChatListener; import com.wimdupont.sxcybot.listeners.EventWaiterUtil; import com.wimdupont.sxcybot.listeners.GuildMemberEventListener; import com.wimdupont.sxcybot.listeners.PollReactionListener; @@ -32,8 +31,7 @@ import java.util.Arrays; public class SxcyBotApplication implements CommandLineRunner { private static final Logger LOGGER = LoggerFactory.getLogger(SxcyBotApplication.class); - private final CommandListener commandListener; - private final AdminCommandListener adminCommandListener; + private final ChatListener chatListener; private final EventWaiterUtil eventWaiterUtil; private final PollReactionListener pollReactionListener; private final GuildMemberEventListener guildMemberEventListener; @@ -42,8 +40,7 @@ public class SxcyBotApplication implements CommandLineRunner { private final PvMRoleResolver pvMRoleResolver; private final String token; - public SxcyBotApplication(CommandListener commandListener, - AdminCommandListener adminCommandListener, + public SxcyBotApplication(ChatListener chatListener, EventWaiterUtil eventWaiterUtil, PollReactionListener pollReactionListener, GuildMemberEventListener guildMemberEventListener, @@ -51,8 +48,7 @@ public class SxcyBotApplication implements CommandLineRunner { ChannelDetailService channelDetailService, PvMRoleResolver pvMRoleResolver, @Value("${discord.bot.token}") String token) { - this.commandListener = commandListener; - this.adminCommandListener = adminCommandListener; + this.chatListener = chatListener; this.eventWaiterUtil = eventWaiterUtil; this.pollReactionListener = pollReactionListener; this.guildMemberEventListener = guildMemberEventListener; @@ -76,13 +72,13 @@ public class SxcyBotApplication implements CommandLineRunner { GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_BANS ) - .addEventListeners(commandListener, adminCommandListener, eventWaiterUtil, guildMemberEventListener, pollReactionListener) + .addEventListeners(chatListener, eventWaiterUtil, guildMemberEventListener, pollReactionListener) .setActivity(Activity.listening(Commands.COMMAND_PREFIX + Command.HELP.name().toLowerCase())) .build(); try { - LOGGER.debug("Awaiting jda " + jda); + LOGGER.debug("Awaiting jda {}", jda); jda.awaitReady(); - LOGGER.debug("Jda " + jda + " ready"); + LOGGER.debug("Jda {} ready", jda); channelDetailService.setJda(jda); processArgs(jda, args); } catch (InterruptedException e) { diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/AdminCommandListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/AdminCommandListener.java @@ -1,6 +1,5 @@ package com.wimdupont.sxcybot.listeners; -import com.wimdupont.sxcybot.enums.Command; import com.wimdupont.sxcybot.enums.Command.Admin; import com.wimdupont.sxcybot.exceptions.InsufficientPrivilegesException; import com.wimdupont.sxcybot.listeners.admin.BanlistListener; @@ -12,7 +11,6 @@ import com.wimdupont.sxcybot.listeners.admin.EditRuleListener; import com.wimdupont.sxcybot.listeners.admin.RoleAssignListener; import com.wimdupont.sxcybot.repository.guild.dao.GuildRole; import com.wimdupont.sxcybot.services.guild.GuildRoleService; -import com.wimdupont.sxcybot.util.Constants.Commands; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; @@ -21,7 +19,6 @@ import org.springframework.stereotype.Component; import javax.annotation.Nonnull; import java.util.List; -import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; @@ -61,64 +58,48 @@ public class AdminCommandListener extends ListenerAdapter { this.guildRoleService = guildRoleService; } - @Override - public void onMessageReceived(@Nonnull MessageReceivedEvent event) { - if (event.getChannelType().isGuild()) { - String msg = event.getMessage().getContentRaw().toLowerCase(); - if (msg.startsWith(Commands.COMMAND_PREFIX)) { - Optional<Admin> adminCommand = messageToAdminCommand(msg); - if (adminCommand.isPresent() && event.getMember() != null) { - Stream<Role> roleStream = event.getMember().getRoles().stream(); - try { - switch (adminCommand.get()) { - case EDITRULE -> { - isPrivileged(roleStream, event, ADMIN_ROLE); - editRuleListener.process(event); - } - case EDITBANLIST -> { - isPrivileged(roleStream, event, ADMIN_ROLE); - editBanlistListener.process(event); - } - case ROLE -> { - isPrivileged(roleStream, event, SUB_ADMIN); - roleAssignListener.process(event); - } - case BANLIST -> { - isPrivileged(roleStream, event, STAFF_ROLE); - banlistListener.process(event); - } - case EDITROLE -> { - isPrivileged(roleStream, event, GOD); - editRoleListener.process(event); - } - case EDITPVM -> { - isPrivileged(roleStream, event, STAFF_ROLE); - editPvmListener.process(event); - } - case EDITBOSS -> { - isPrivileged(roleStream, event, GENERAL); - bossUpdateMultiplierListener.process(event); - } - default -> { - } - } - } catch (InsufficientPrivilegesException e) { - event.getChannel().sendMessage(e.getMessage()).queue(); + public void executeCommand(@Nonnull MessageReceivedEvent event, Admin adminCommand) { + if (event.getMember() != null) { + Stream<Role> roleStream = event.getMember().getRoles().stream(); + try { + switch (adminCommand) { + case EDITRULE -> { + isPrivileged(roleStream, event, ADMIN_ROLE); + editRuleListener.process(event); + } + case EDITBANLIST -> { + isPrivileged(roleStream, event, ADMIN_ROLE); + editBanlistListener.process(event); + } + case ROLE -> { + isPrivileged(roleStream, event, SUB_ADMIN); + roleAssignListener.process(event); + } + case BANLIST -> { + isPrivileged(roleStream, event, STAFF_ROLE); + banlistListener.process(event); + } + case EDITROLE -> { + isPrivileged(roleStream, event, GOD); + editRoleListener.process(event); + } + case EDITPVM -> { + isPrivileged(roleStream, event, STAFF_ROLE); + editPvmListener.process(event); + } + case EDITBOSS -> { + isPrivileged(roleStream, event, GENERAL); + bossUpdateMultiplierListener.process(event); + } + default -> { } } + } catch (InsufficientPrivilegesException e) { + event.getChannel().sendMessage(e.getMessage()).queue(); } } } - private Optional<Admin> messageToAdminCommand(String msg) { - for (Admin adminCommand : Command.Admin.values()) { - if (msg.split(" ")[0].equals(Commands.COMMAND_PREFIX + adminCommand.name().toLowerCase())) { - return Optional.of(adminCommand); - } - } - return Optional.empty(); - } - private void isPrivileged(Stream<Role> roleStream, MessageReceivedEvent event, int elevation) throws InsufficientPrivilegesException { List<GuildRole> guildRoles = guildRoleService.findAllByElevationLessThanEqual(elevation); if (roleStream.noneMatch(hasPrivilege(event, guildRoles))) diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/ChatListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/ChatListener.java @@ -0,0 +1,68 @@ +package com.wimdupont.sxcybot.listeners; + +import com.wimdupont.sxcybot.enums.Command; +import com.wimdupont.sxcybot.enums.Command.Admin; +import com.wimdupont.sxcybot.listeners.member.BB8Listener; +import com.wimdupont.sxcybot.services.guild.ChatModService; +import com.wimdupont.sxcybot.util.Constants.Commands; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; +import java.util.EnumSet; +import java.util.Optional; + +@Component +public class ChatListener extends ListenerAdapter { + + private final CommandListener commandListener; + private final AdminCommandListener adminCommandListener; + private final BB8Listener bb8Listener; + private final ChatModService chatModService; + + public ChatListener(CommandListener commandListener, + AdminCommandListener adminCommandListener, + BB8Listener bb8Listener, + ChatModService chatModService) { + this.commandListener = commandListener; + this.adminCommandListener = adminCommandListener; + this.bb8Listener = bb8Listener; + this.chatModService = chatModService; + } + + @Override + public void onMessageReceived(@Nonnull MessageReceivedEvent event) { + if (event.getChannelType().isGuild()) { + var msg = event.getMessage().getContentRaw().toLowerCase(); + + if (chatModService.isAllowed(event) + && msg.startsWith(Commands.COMMAND_PREFIX)) { + executeCommand(event, msg); + } + } + } + + private void executeCommand(MessageReceivedEvent event, String msg) { + var command = messageToCommand(Command.class, msg); + if (command.isPresent()) { + commandListener.executeCommand(event, command.get()); + } else { + var adminCommand = messageToCommand(Admin.class, msg); + if (adminCommand.isPresent()) { + adminCommandListener.executeCommand(event, adminCommand.get()); + } else if (Commands.BB8.equals(msg)) { + bb8Listener.process(event); + } + } + } + + private <T extends Enum<T>> Optional<T> messageToCommand(Class<T> clazz, String msg) { + for (T command : EnumSet.allOf(clazz)) { + if (msg.split(" ")[0].equals(Commands.COMMAND_PREFIX + command.name().toLowerCase())) { + return Optional.of(command); + } + } + return Optional.empty(); + } +} diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/CommandListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/CommandListener.java @@ -1,10 +1,8 @@ package com.wimdupont.sxcybot.listeners; import com.wimdupont.sxcybot.enums.Command; -import com.wimdupont.sxcybot.listeners.member.BB8Listener; import com.wimdupont.sxcybot.listeners.member.CombatStatsListener; import com.wimdupont.sxcybot.listeners.member.CustomPollListener; -import com.wimdupont.sxcybot.listeners.member.EventListener; import com.wimdupont.sxcybot.listeners.member.ForumListener; import com.wimdupont.sxcybot.listeners.member.HelpListener; import com.wimdupont.sxcybot.listeners.member.HiscoreBossListener; @@ -18,22 +16,17 @@ import com.wimdupont.sxcybot.listeners.member.PvmRolePollListener; import com.wimdupont.sxcybot.listeners.member.RuleListener; import com.wimdupont.sxcybot.listeners.member.RulesListener; import com.wimdupont.sxcybot.listeners.member.StatsListener; -import com.wimdupont.sxcybot.util.Constants.Commands; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; -import java.util.Optional; @Component -public class CommandListener extends ListenerAdapter { +public class CommandListener { - private final BB8Listener bb8Listener; private final RuleListener ruleListener; private final RulesListener rulesListener; private final PingListener pingListener; - private final EventListener eventListener; private final ForumListener forumListener; private final PollListener pollListener; private final CustomPollListener customPollListener; @@ -47,11 +40,9 @@ public class CommandListener extends ListenerAdapter { private final PvmRoleCheckListener pvmRoleCheckListener; private final PvmRolePollListener pvmRolePollListener; - public CommandListener(BB8Listener bb8Listener, - RuleListener ruleListener, + public CommandListener(RuleListener ruleListener, RulesListener rulesListener, PingListener pingListener, - EventListener eventListener, ForumListener forumListener, PollListener pollListener, CustomPollListener customPollListener, @@ -64,11 +55,9 @@ public class CommandListener extends ListenerAdapter { HiscoreBossListener hiscoreBossListener, PvmRoleCheckListener pvmRoleCheckListener, PvmRolePollListener pvmRolePollListener) { - this.bb8Listener = bb8Listener; this.ruleListener = ruleListener; this.rulesListener = rulesListener; this.pingListener = pingListener; - this.eventListener = eventListener; this.forumListener = forumListener; this.pollListener = pollListener; this.customPollListener = customPollListener; @@ -83,50 +72,28 @@ public class CommandListener extends ListenerAdapter { this.pvmRolePollListener = pvmRolePollListener; } - @Override - public void onMessageReceived(@Nonnull MessageReceivedEvent event) { - if (event.getChannelType().isGuild()) { - String msg = event.getMessage().getContentRaw().toLowerCase(); - if (msg.startsWith(Commands.COMMAND_PREFIX)) { - Optional<Command> command = messageToCommand(msg); - if (command.isPresent()) { - switch (command.get()) { - case RULE -> ruleListener.process(event); - case RULES -> rulesListener.process(event); - case PING -> pingListener.process(event); - - //TODO + public void executeCommand(@Nonnull MessageReceivedEvent event, Command command) { + switch (command) { + case RULE -> ruleListener.process(event); + case RULES -> rulesListener.process(event); + case PING -> pingListener.process(event); + //TODO // case EVENT: // eventListener.proces(event); // break; - case CBSTATS -> combatStatsListener.process(event); - case STATS -> statsListener.process(event); - case PRICE -> priceListener.process(event); - case FORUM -> forumListener.process(event); - case POLL -> pollListener.process(event); - case CPOLL -> customPollListener.process(event); - case KC -> killCountListener.process(event); - case PVMLIST -> pvmListListener.process(event); - case BOSSLIST -> hiscoreBossListener.process(event); - case PVMPOLL -> pvmRolePollListener.process(event); - case PVMCHECK -> pvmRoleCheckListener.process(event); - case HELP -> helpListener.process(event); - default -> { - } - } - } else if (Commands.BB8.equals(msg)) { - bb8Listener.process(event); - } - } + case CBSTATS -> combatStatsListener.process(event); + case STATS -> statsListener.process(event); + case PRICE -> priceListener.process(event); + case FORUM -> forumListener.process(event); + case POLL -> pollListener.process(event); + case CPOLL -> customPollListener.process(event); + case KC -> killCountListener.process(event); + case PVMLIST -> pvmListListener.process(event); + case BOSSLIST -> hiscoreBossListener.process(event); + case PVMPOLL -> pvmRolePollListener.process(event); + case PVMCHECK -> pvmRoleCheckListener.process(event); + case HELP -> helpListener.process(event); } } - private Optional<Command> messageToCommand(String msg) { - for (Command command : Command.values()) { - if (msg.split(" ")[0].equals(Commands.COMMAND_PREFIX + command.name().toLowerCase())) { - return Optional.of(command); - } - } - return Optional.empty(); - } } diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/admin/RoleAssignListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/admin/RoleAssignListener.java @@ -54,7 +54,6 @@ public class RoleAssignListener implements Listener { privateChannel.sendMessageEmbeds(embedBuilder.build()).queue(); eventWaiterUtil.waitForPrivateChannelEvent(roleReceiver -> { String roleMessage = roleReceiver.getMessage().getContentRaw(); -// try { String[] roleNumbers = roleMessage.split(" "); embedBuilder.clearFields(); embedBuilder.setTitle("Role assigner:"); @@ -72,7 +71,7 @@ public class RoleAssignListener implements Listener { event.getGuild().addRoleToMember(member, role.get()).queue(); action = "assigned"; } - embedBuilder.addField(String.format("\"%s\" has been succesfully %s.", discordRole.get().getName(), action), "", false); + embedBuilder.addField(String.format("\"%s\" has been successfully %s.", discordRole.get().getName(), action), "", false); event.getChannel().sendMessage(String.format("Role \"%s\" has been %s to %s by %s", role.get().getName(), action, member.getUser(), event.getMember().getUser().getName())).queue(); } else { diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/admin/banlist/AddBanlistUserListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/admin/banlist/AddBanlistUserListener.java @@ -37,7 +37,7 @@ public class AddBanlistUserListener implements PrivateListener { .lastModifiedBy(JdaUtil.getName(event).orElse(null)) .build(); User bannedUser = userService.save(userToBan); - privateChannel.sendMessage(String.format("User %s has been succesfully banned.", bannedUser.getName())).queue(); + privateChannel.sendMessage(String.format("User %s has been successfully banned.", bannedUser.getName())).queue(); event.getChannel().sendMessage(String.format("User %s has been banned by %s. (%s)", bannedUser.getName(), JdaUtil.getUser(event), bannedUser.getDescription() )).queue(); diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/admin/banlist/DeleteBanlistUserListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/admin/banlist/DeleteBanlistUserListener.java @@ -31,7 +31,7 @@ public class DeleteBanlistUserListener implements PrivateListener { try { User userToDelete = userService.findByName(nameReceiver.getMessage().getContentRaw()); userService.delete(userToDelete); - privateChannel.sendMessage(String.format("%s succesfully deleted.", userToDelete)).queue(); + privateChannel.sendMessage(String.format("%s successfully deleted.", userToDelete)).queue(); event.getChannel().sendMessage(String.format("User %s has been deleted from the banlist by %s", userToDelete, JdaUtil.getUser(event))).queue(); } catch (EntityNotFoundException e) { privateChannel.sendMessage(e.getMessage()).queue(); diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/admin/pvmrole/AddPvmListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/admin/pvmrole/AddPvmListener.java @@ -57,7 +57,7 @@ public class AddPvmListener implements PrivateListener { ); event.getChannel().sendMessage(String.format("%s (RSN: %s) has been added to the PvM competition by %s!", member, pvmRoleUser.getRsn(), creator)).queue(); - privateChannel.sendMessage(String.format("%s (RSN: %s) has been succesfully added!", + privateChannel.sendMessage(String.format("%s (RSN: %s) has been successfully added!", member, pvmRoleUser.getRsn())).queue(); try { pvmRoleSnapshotComparatorService.takeSnapshot(event.getChannel(), pvmRoleUser, true, true); diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/admin/pvmrole/DeletePvmListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/admin/pvmrole/DeletePvmListener.java @@ -61,7 +61,7 @@ public class DeletePvmListener implements PrivateListener { if ("y".equalsIgnoreCase(verifyMessage) || "yes".equalsIgnoreCase(verifyMessage)) { pvmKcSnapshotService.deleteAll(pvmKcSnapshotService.findAllByPvmRoleUser(pvmRoleUser)); pvmRoleUserService.delete(pvmRoleUser); - privateChannel.sendMessage(String.format("%s has been succesfully deleted.", pvmRoleUser)).queue(); + privateChannel.sendMessage(String.format("%s has been successfully deleted.", pvmRoleUser)).queue(); event.getChannel().sendMessage(String.format("PvM competitor %s has been deleted by %s", name, JdaUtil.getUser(event))).queue(); } else { privateChannel.sendMessage(String.format("You can check all competitors with the \"%s\" command.", Commands.COMMAND_PREFIX + Command.PVMLIST.name().toLowerCase())).queue(); diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/admin/role/AddRoleListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/admin/role/AddRoleListener.java @@ -33,16 +33,22 @@ public class AddRoleListener implements PrivateListener { privateChannel.sendMessage("Name of the role you want to add?").queue(); eventWaiterUtil.waitForPrivateChannelEvent(nameReceiver -> { String roleName = nameReceiver.getMessage().getContentRaw(); - Optional<Role> role = event.getGuild().getRoles().stream().filter(f -> roleName.equalsIgnoreCase(f.getName())).findFirst(); + Optional<Role> role = event.getGuild().getRoles().stream() + .filter(f -> roleName.equalsIgnoreCase(f.getName())) + .findFirst(); if (role.isPresent()) { - int orderValue = guildRoleService.findAll().stream().filter(f -> f.getOrderValue() != null).max(Comparator.comparing(GuildRole::getOrderValue)).map(GuildRole::getOrderValue).orElse(0) + 1; + int orderValue = guildRoleService.findAll().stream() + .filter(f -> f.getOrderValue() != null) + .max(Comparator.comparing(GuildRole::getOrderValue)) + .map(GuildRole::getOrderValue) + .orElse(0) + 1; GuildRole roleToAdd = GuildRole.Builder.newBuilder() .name(roleName) .orderValue(orderValue) .elevation(Constants.ADDED_ROLE_ELEVATION) .build(); GuildRole guildRole = guildRoleService.save(roleToAdd); - privateChannel.sendMessage(String.format("Role %s has been succesfully added.", guildRole.getName())).queue(); + privateChannel.sendMessage(String.format("Role %s has been successfully added.", guildRole.getName())).queue(); event.getChannel().sendMessage(String.format("Role %s has been added by %s.", guildRole.getName(), JdaUtil.getUser(event) )).queue(); diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/admin/role/DeleteRoleListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/admin/role/DeleteRoleListener.java @@ -39,7 +39,7 @@ public class DeleteRoleListener implements PrivateListener { role.setOrderValue(role.getOrderValue() - 1); guildRoleService.save(role); }); - privateChannel.sendMessage(String.format("%s succesfully deleted.", guildRole)).queue(); + privateChannel.sendMessage(String.format("%s successfully deleted.", guildRole)).queue(); event.getChannel().sendMessage(String.format("Role %s has been deleted by %s", guildRole, JdaUtil.getUser(event))).queue(); } else { privateChannel.sendMessage(String.format("Only roles that can be added with the %srole command can be deleted.", Commands.COMMAND_PREFIX)).queue(); diff --git a/src/main/java/com/wimdupont/sxcybot/listeners/admin/rule/DeleteRuleListener.java b/src/main/java/com/wimdupont/sxcybot/listeners/admin/rule/DeleteRuleListener.java @@ -38,7 +38,7 @@ public class DeleteRuleListener implements PrivateListener { ruleToUpdate.setNumber(ruleToUpdate.getNumber() - 1); ruleService.save(ruleToUpdate); }); - privateChannel.sendMessage(String.format("%s has been succesfully deleted.", rule)).queue(); + privateChannel.sendMessage(String.format("%s has been successfully deleted.", rule)).queue(); event.getChannel().sendMessage(String.format("Rule %s has been deleted by %s", rule, JdaUtil.getUser(event))).queue(); } catch (NumberFormatException e) { privateChannel.sendMessage("Please try again later with a valid rule number.").queue(); diff --git a/src/main/java/com/wimdupont/sxcybot/repository/guild/ChatDataRepository.java b/src/main/java/com/wimdupont/sxcybot/repository/guild/ChatDataRepository.java @@ -0,0 +1,13 @@ +package com.wimdupont.sxcybot.repository.guild; + +import com.wimdupont.sxcybot.repository.guild.dao.ChatData; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ChatDataRepository extends JpaRepository<ChatData, String> { + + List<ChatData> findAllByDiscordIdAndWithUrlTrueOrderByCreatedDateTimeDesc(String discordId); +} diff --git a/src/main/java/com/wimdupont/sxcybot/repository/guild/dao/ChatData.java b/src/main/java/com/wimdupont/sxcybot/repository/guild/dao/ChatData.java @@ -0,0 +1,154 @@ +package com.wimdupont.sxcybot.repository.guild.dao; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.Id; +import org.hibernate.annotations.UuidGenerator; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@EntityListeners(AuditingEntityListener.class) +public class ChatData { + + @Id + @UuidGenerator + private String id; + private String discordId; + private String channelId; + private String messageId; + private boolean withUrl; + @CreatedDate + private LocalDateTime createdDateTime; + @LastModifiedDate + private LocalDateTime lastModifiedDateTime; + + @SuppressWarnings("unused") + protected ChatData() { + } + + private ChatData(Builder builder) { + setId(builder.id); + setDiscordId(builder.discordId); + setChannelId(builder.channelId); + setMessageId(builder.messageId); + setWithUrl(builder.withUrl); + setCreatedDateTime(builder.createdDateTime); + setLastModifiedDateTime(builder.lastModifiedDateTime); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDiscordId() { + return discordId; + } + + public void setDiscordId(String discordId) { + this.discordId = discordId; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getMessageId() { + return messageId; + } + + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + public boolean isWithUrl() { + return withUrl; + } + + public void setWithUrl(boolean withUrl) { + this.withUrl = withUrl; + } + + public LocalDateTime getCreatedDateTime() { + return createdDateTime; + } + + public void setCreatedDateTime(LocalDateTime createdDateTime) { + this.createdDateTime = createdDateTime; + } + + public LocalDateTime getLastModifiedDateTime() { + return lastModifiedDateTime; + } + + public void setLastModifiedDateTime(LocalDateTime lastModifiedDateTime) { + this.lastModifiedDateTime = lastModifiedDateTime; + } + + public static final class Builder { + private String id; + private String discordId; + private String channelId; + private String messageId; + private boolean withUrl; + private LocalDateTime createdDateTime; + private LocalDateTime lastModifiedDateTime; + + private Builder() { + } + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder id(String val) { + id = val; + return this; + } + + public Builder discordId(String val) { + discordId = val; + return this; + } + + public Builder channelId(String val) { + channelId = val; + return this; + } + + public Builder messageId(String val) { + messageId = val; + return this; + } + + public Builder withUrl(boolean val) { + withUrl = val; + return this; + } + + public Builder createdDateTime(LocalDateTime val) { + createdDateTime = val; + return this; + } + + public Builder lastModifiedDateTime(LocalDateTime val) { + lastModifiedDateTime = val; + return this; + } + + public ChatData build() { + return new ChatData(this); + } + } +} diff --git a/src/main/java/com/wimdupont/sxcybot/services/CleanupScheduler.java b/src/main/java/com/wimdupont/sxcybot/services/CleanupScheduler.java @@ -1,5 +1,6 @@ package com.wimdupont.sxcybot.services; +import com.wimdupont.sxcybot.repository.guild.ChatDataRepository; import com.wimdupont.sxcybot.repository.guild.pvmrole.dao.PvmKcSnapshot; import com.wimdupont.sxcybot.repository.guild.pvmrole.dao.PvmRoleUser; import com.wimdupont.sxcybot.services.guild.PollService; @@ -20,16 +21,20 @@ public class CleanupScheduler { private static final Logger LOGGER = LoggerFactory.getLogger(CleanupScheduler.class); private static final int POLL_DAYS_VALID = 60; + private static final int CHAT_DATA_VALID = 14; private final PvmKcSnapshotService pvmKcSnapshotService; private final PvmRoleUserService pvmRoleUserService; private final PollService pollService; + private final ChatDataRepository chatDataRepository; public CleanupScheduler(PvmKcSnapshotService pvmKcSnapshotService, PvmRoleUserService pvmRoleUserService, - PollService pollService) { + PollService pollService, + ChatDataRepository chatDataRepository) { this.pvmKcSnapshotService = pvmKcSnapshotService; this.pvmRoleUserService = pvmRoleUserService; this.pollService = pollService; + this.chatDataRepository = chatDataRepository; } @Scheduled(cron = "${db.cleanup.schedule}") @@ -37,6 +42,7 @@ public class CleanupScheduler { public void cleanup() { cleanupPvmKcSnapshots(); cleanupPolls(); + cleanupChatData(); } private void cleanupPvmKcSnapshots() { @@ -48,15 +54,24 @@ public class CleanupScheduler { .sorted(Comparator.comparing(PvmKcSnapshot::getCreatedDate).reversed()) .skip(2) .toList(); - LOGGER.info("deleted {} from RSN: {}, discordId: {}", pvmKcSnapshotList.size(), pvmRoleUser.getRsn(), pvmRoleUser.getDiscordId()); + LOGGER.info("deleted {} from RSN: {}, discordId: {}", + pvmKcSnapshotList.size(), pvmRoleUser.getRsn(), pvmRoleUser.getDiscordId()); pvmKcSnapshotService.deleteAll(pvmKcSnapshotList); } } } private void cleanupPolls() { - pollService.findAll().stream().filter(f -> ChronoUnit.DAYS.between(f.getCreatedDate(), LocalDateTime.now()) > POLL_DAYS_VALID) + pollService.findAll().stream() + .filter(f -> ChronoUnit.DAYS.between(f.getCreatedDate(), LocalDateTime.now()) > POLL_DAYS_VALID) .peek(f -> LOGGER.info("removed poll {} from {}", f.getMessageId(), f.getCreatedDate())) .forEach(pollService::delete); } + + private void cleanupChatData() { + chatDataRepository.findAll().stream() + .filter(f -> ChronoUnit.DAYS.between(f.getCreatedDateTime(), LocalDateTime.now()) > CHAT_DATA_VALID) + .peek(f -> LOGGER.info("removed chat data {} from {}", f.getMessageId(), f.getCreatedDateTime())) + .forEach(chatDataRepository::delete); + } } diff --git a/src/main/java/com/wimdupont/sxcybot/services/MailService.java b/src/main/java/com/wimdupont/sxcybot/services/MailService.java @@ -41,9 +41,9 @@ public class MailService { properties.put("mail.smtp.auth", withAuth()); properties.put("mail.smtp.starttls.enable", mailConfig.isSsl()); properties.put("mail.smtp.host", mailConfig.getHost()); - if (mailConfig.getPort() != null) { - properties.put("mail.smtp.port", mailConfig.getPort()); - } + if (mailConfig.getPort() != null) { + properties.put("mail.smtp.port", mailConfig.getPort()); + } var session = Session.getInstance(properties, getAuthenticator()); try { @@ -63,9 +63,9 @@ public class MailService { } private Authenticator getAuthenticator() { - if (!withAuth()) { - return null; - } + if (!withAuth()) { + return null; + } return new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication( @@ -76,7 +76,7 @@ public class MailService { } private boolean withAuth() { - return mailConfig.getUsername() != null && mailConfig.getPassword() != null; + return mailConfig.getUsername() != null && mailConfig.getPassword() != null; } } diff --git a/src/main/java/com/wimdupont/sxcybot/services/guild/ChatModService.java b/src/main/java/com/wimdupont/sxcybot/services/guild/ChatModService.java @@ -0,0 +1,114 @@ +package com.wimdupont.sxcybot.services.guild; + +import com.wimdupont.sxcybot.repository.guild.ChatDataRepository; +import com.wimdupont.sxcybot.repository.guild.dao.ChatData; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.exceptions.HierarchyException; +import net.dv8tion.jda.api.exceptions.RateLimitedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Transactional +public class ChatModService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ChatModService.class); + private final ChatDataRepository chatDataRepository; + + public ChatModService(ChatDataRepository chatDataRepository) { + this.chatDataRepository = chatDataRepository; + } + + public boolean isAllowed(MessageReceivedEvent event) { + if (event.getMember() == null || event.getMember().getUser().isBot() || !containsUrl(event)) + return true; + + var discordId = event.getMember().getUser().getId(); + var previousUrlMessages = chatDataRepository.findAllByDiscordIdAndWithUrlTrueOrderByCreatedDateTimeDesc(discordId); + var currentData = persist(discordId, event.getTextChannel().getId(), event.getMessageId()); + + return isValidForHistory(previousUrlMessages, event, currentData); + } + + private boolean isValidForHistory(List<ChatData> previousUrlMessages, + MessageReceivedEvent event, + ChatData currentData) { + if (previousUrlMessages.isEmpty()) + return true; + try { + LocalDateTime latest = previousUrlMessages.stream().findFirst() + .map(ChatData::getCreatedDateTime) + .orElseThrow(); + if (latest.plusMinutes(1L).isBefore(LocalDateTime.now())) { + chatDataRepository.deleteAll(previousUrlMessages); + } else if (previousUrlMessages.size() >= 2 && previousUrlMessages.size() < 4) { + dmWarning(event); + } else if (previousUrlMessages.size() >= 4 && event.getMember() != null) { + try { + event.getMember().kick("Spamming URL's.").queue(); + } catch (HierarchyException | ErrorResponseException e) { + LOGGER.error(e.getMessage(), e); + } + for (ChatData chatData : previousUrlMessages) { + removeMessage(event.getGuild(), chatData); + } + removeMessage(event.getGuild(), currentData); + return false; + } + } catch (RateLimitedException | ErrorResponseException e) { + LOGGER.error(e.getMessage(), e); + } + return true; + } + + private ChatData persist(String discordId, String channelId, String messageId) { + return chatDataRepository.save(ChatData.Builder.newBuilder() + .discordId(discordId) + .channelId(channelId) + .messageId(messageId) + .withUrl(true) + .build()); + } + + private void dmWarning(MessageReceivedEvent event) throws RateLimitedException { + if (event.getMember() != null) { + var privateChannel = event.getMember().getUser().openPrivateChannel().complete(true); + privateChannel.sendMessage(String.format("Warning: stop spamming URL's! (server: %s)", + event.getGuild().getName())).queue(); + } + } + + private void removeMessage(Guild guild, ChatData chatData) throws RateLimitedException { + var channel = guild.getTextChannelById(chatData.getChannelId()); + if (channel != null) { + channel.retrieveMessageById(chatData.getMessageId()).complete(true).delete().queue(); + chatDataRepository.delete(chatData); + } + } + + private boolean containsUrl(MessageReceivedEvent event) { + var msg = event.getMessage().getContentRaw().toLowerCase(); + if (containsUrl(msg)) + return true; + + if (!event.getMessage().getEmbeds().isEmpty()) { + return event.getMessage().getEmbeds().stream() + .anyMatch(f -> StringUtils.hasLength(f.getUrl()) + || containsUrl(f.getTitle()) + || containsUrl(f.getDescription())); + } + return false; + } + + private boolean containsUrl(String msg) { + return msg != null && msg.matches(".*https?://.*|.*www\\..*"); + } +} diff --git a/src/main/resources/db/migration/V1_19__chatmod.sql b/src/main/resources/db/migration/V1_19__chatmod.sql @@ -0,0 +1,9 @@ +CREATE TABLE chat_data ( + id VARCHAR(36) primary key NOT NULL, + discord_id VARCHAR(100) NOT NULL, + channel_id VARCHAR(100) NOT NULL, + message_id VARCHAR(100) NOT NULL, + with_url TINYINT(1) DEFAULT 0, + created_date_time DATETIME, + last_modified_date_time DATETIME +); diff --git a/src/main/resources/releasenotes.csv b/src/main/resources/releasenotes.csv @@ -1 +1 @@ -Delete PvM Role by RSN added;Because of the previous changes, deletions were not possible if discordId was not findable by name. This solves this issue. +Chat mod functionality added;URL spam will now be detected by the bot. If this occurs, then the bot will send a DM warning. The user will be kicked if he/she continues.