change how events are handled by using a single class to register listeners onto

This commit is contained in:
anthorob committed 2026-08-24 17:10:16 -04:00
1 parent e0612f90a8
commit e58854a85c
7 files changed
+92 -476

No files matched your search

@@ -1,338 +0,0 @@
package me.bobert.netneutrality.context;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.command.options.CommandOption;
import me.bobert.netneutrality.core.command.options.MessageCommandOption;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
import net.dv8tion.jda.api.interactions.modals.Modal;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public class MessageCommandContext extends CommandContext {
private final MessageReceivedEvent event;
private final Message message;
public MessageCommandContext(MessageReceivedEvent event) {
super(event.getJDA(), event.isFromGuild() ? event.getGuild() : null, event.getChannel(), event.getMember(), event.getAuthor());
this.event = event;
this.message = event.getMessage();
}
@Override
public MessageReceivedEvent getEvent() {
return event;
}
@Override
public String getId() {
return String.format("%s:%s:%s:%s",
getGuild() == null ? "PrivateMessage" : getGuild().getId(),
getChannel().getId(),
getDiscordUser().getId(),
message.getId());
}
@Override
public ChannelType getType() {
return event.getChannelType();
}
public Message getMessage() {
return message;
}
@Override
public boolean isAcknowledged() {
return true;
}
@Override
public boolean isFromGuild() {
return event.isFromGuild();
}
@Override
public InteractionHook getInteractionHook() {
return null;
}
@Override
public void defer(boolean ephemeral, Consumer<AsyncContext> consumer) {
reply("Bot is thinking...", consumer);
}
@Override
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
if (ephemeral) {
sendMessageInPrivate(messageCreateData, components);
return;
}
if (getChannel() != null && getChannel().canTalk()) {
message.reply(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
}
}
@Override
public void reply(Modal modal) {
}
@Override
public void ack(boolean ephemeral) {
}
@Override
public void deleteMessage() {
if (message != null && getType() != ChannelType.PRIVATE) {
message.delete().queueAfter(1, TimeUnit.SECONDS);
}
}
@Override
public List<CommandOption> parseOptions(String[] args, List<OptionData> commandOptions) {
List<CommandOption> options = new ArrayList<>();
Mentions mentions = message.getMentions();
int roleMentionIndex = 0;
int userMentionIndex = 0;
int memberMentionIndex = 0;
int channelMentionIndex = 0;
int index = 0;
for (OptionData data : commandOptions) {
OptionType type = data.getType();
Set<ChannelType> requiredChannelTypes = data.getChannelTypes();
List<Command.Choice> choices = data.getChoices();
Object value = null;
try {
switch (type) {
case STRING -> {
String str = args[index];
if ((str.contains("<@") || str.contains("<#")) && str.contains(">")) {
invalidateContext(data, "expected a string at position " + (index + 1) + " but a role/user/channel was provided");
break;
}
if (!choices.isEmpty()) {
List<Command.Choice> matched = choices.stream().filter(choice -> choice.getAsString().equalsIgnoreCase(str)).toList();
if (matched.isEmpty()) {
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
}
}
Integer min = data.getMinLength();
Integer max = data.getMaxLength();
if (min != null && str.length() < min) {
invalidateContext(data, "minimum length for argument is " + min);
}
if (max != null && str.length() > max) {
invalidateContext(data, "maximum length for argument is " + max);
}
value = str;
}
case INTEGER -> {
long num = Long.parseLong(args[index]);
if (!choices.isEmpty()) {
List<Command.Choice> matched = choices.stream().filter(choice -> choice.getAsLong() == num).toList();
if (matched.isEmpty()) {
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
}
}
value = num;
Number max = data.getMaxValue();
Number min = data.getMinValue();
if (min != null && num < min.longValue()) {
value = min.longValue();
} else if (max != null && num > max.longValue()) {
value = max.longValue();
}
value = num;
}
case BOOLEAN -> {
String str = args[index];
if (str.equalsIgnoreCase("true") || str.equalsIgnoreCase("false")) {
value = Boolean.parseBoolean(str);
break;
}
invalidateContext(data, "expected true or false");
}
case NUMBER -> {
double num = Long.parseLong(args[index]);
if (!choices.isEmpty()) {
List<Command.Choice> matched = choices.stream().filter(choice -> choice.getAsDouble() == num).toList();
if (matched.isEmpty()) {
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
break;
}
}
Number max = data.getMaxValue();
Number min = data.getMinValue();
value = num;
if (min != null && num < min.doubleValue()) {
value = min.doubleValue();
} else if (max != null && num > max.doubleValue()) {
value = max.doubleValue();
}
}
case ROLE -> {
if (mentions.getRoles().size() > roleMentionIndex) {
Role role = mentions.getRoles().get(roleMentionIndex++);
if (role.getGuild().getId().equals(getGuild().getId())) {
value = role;
}
break;
}
invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid role.");
}
case USER -> {
if (mentions.getMembers().size() > memberMentionIndex) {
Member member = mentions.getMembers().get(memberMentionIndex++);
if (member.getGuild().getId().equals(getGuild().getId())) {
value = member;
break;
}
}
if (mentions.getUsers().size() > userMentionIndex) {
value = mentions.getUsers().get(userMentionIndex++);
break;
}
invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid user.");
}
case MENTIONABLE -> {
if (mentions.getRoles().size() > roleMentionIndex) {
Role role = mentions.getRoles().get(roleMentionIndex++);
if (role.getGuild().getId().equals(getGuild().getId())) {
value = role;
}
break;
}
if (mentions.getMembers().size() > memberMentionIndex) {
Member member = mentions.getMembers().get(memberMentionIndex++);
if (member.getGuild().getId().equals(getGuild().getId())) {
value = member;
}
break;
}
if (mentions.getUsers().size() > userMentionIndex) {
value = mentions.getUsers().get(userMentionIndex++);
break;
}
invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid user or role.");
}
case CHANNEL -> {
if (mentions.getChannels().size() > channelMentionIndex) {
GuildChannel channel = mentions.getChannels().get(channelMentionIndex++);
if (!requiredChannelTypes.contains(channel.getType())) {
invalidateContext(data, "not a valid channel type at index " + (index + 1) +
" , valid types are: " + validTypes(requiredChannelTypes));
break;
}
if (channel.getGuild().getId().equals(getGuild().getId())) {
value = channel;
break;
}
}
invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid channel.");
}
}
} catch (IndexOutOfBoundsException e) {
if (data.isRequired()) {
// This should never happen because required options are validated before calling parseOptions()
throw new IllegalArgumentException("Required option " + data.getName() + " was not found at index " + index);
}
} catch (NumberFormatException ignored) {
invalidateContext(data, "the argument provided is not a number.");
}
if (value != null) {
options.add(new MessageCommandOption(data, value));
}
index++;
}
return options;
}
private String validChoices(List<Command.Choice> choices) {
StringJoiner joiner = new StringJoiner(", ");
for (Command.Choice choice : choices) {
joiner.add(choice.getAsString());
}
return joiner.toString();
}
private String validTypes(Set<ChannelType> types) {
StringJoiner joiner = new StringJoiner(", ");
for (ChannelType type : types) {
joiner.add(type.name());
}
return joiner.toString();
}
@Override
public String parseCommandName() {
String rawMessage = message.getContentRaw().trim();
String prefix = Settings.getInstance().getPrefix();
if (rawMessage.startsWith(prefix)) {
return getFullCommand().split(" ", 2)[0];
}
return null;
}
@Override
public String getFullCommand() {
String rawMessage = message.getContentRaw().trim();
String prefix = Settings.getInstance().getPrefix();
return rawMessage.replaceFirst("^[" + prefix + "\\s]+", "").trim();
}
}
@@ -1,72 +0,0 @@
package me.bobert.netneutrality.context;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.command.options.CommandOption;
import me.bobert.netneutrality.core.command.options.SlashCommandOption;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.context.InteractionContext;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.interactions.modals.Modal;
import java.util.List;
import java.util.stream.Collectors;
public class SlashCommandContext extends CommandContext implements InteractionContext {
private final SlashCommandInteractionEvent event;
public SlashCommandContext(SlashCommandInteractionEvent event) {
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser());
this.event = event;
}
@Override
public SlashCommandInteractionEvent getEvent() {
return event;
}
@Override
public ChannelType getType() {
return event.getChannelType();
}
@Override
public void deleteMessage() {
// not implemented
}
@Override
public void reply(Modal modal) {
event.replyModal(modal).queue();
}
@Override
public List<CommandOption> parseOptions(String[] args, List<OptionData> commandOptions) {
return event.getOptions().stream().map(SlashCommandOption::new).collect(Collectors.toUnmodifiableList());
}
@Override
public String parseCommandName() {
return event.getName();
}
@Override
public String getFullCommand() {
return event.getFullCommandName();
}
@Override
public String getSubcommandGroup() {
return event.getSubcommandGroup();
}
@Override
public String getSubcommand() {
return event.getSubcommandName();
}
}
@@ -0,0 +1,46 @@
package me.bobert.netneutrality.core.command;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.context.command.MessageMenuContext;
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
import me.bobert.netneutrality.core.context.CommandContext;
import net.dv8tion.jda.api.entities.Message;
public abstract class AbstractMessageMenuCommand extends AbstractCommand {
@Override
public String getDescription() {
return null;
}
@Override
public boolean isSlashCommandOnly() {
return false;
}
@Override
public void registerCommandOptions(CommandOptionsBuilder builder) {
// no options for context commands
}
@Override
public Type getType() {
return Type.MESSAGE;
}
@Override
public CommandExecutionResult execute(CommandContext context) {
if (context instanceof MessageMenuContext mc) {
Message message = mc.getTarget();
return execute(message, context);
}
throw new IllegalArgumentException("Wrong context type for this command");
}
public abstract CommandExecutionResult execute(Message target, CommandContext context);
}
@@ -0,0 +1,46 @@
package me.bobert.netneutrality.core.command;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.context.command.UserMenuContext;
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
import me.bobert.netneutrality.core.context.CommandContext;
import net.dv8tion.jda.api.entities.User;
public abstract class AbstractUserMenuCommand extends AbstractCommand {
@Override
public String getDescription() {
return null;
}
@Override
public boolean isSlashCommandOnly() {
return false;
}
@Override
public void registerCommandOptions(CommandOptionsBuilder builder) {
// no options for context commands
}
@Override
public Type getType() {
return Type.USER;
}
@Override
public CommandExecutionResult execute(CommandContext context) {
if (context instanceof UserMenuContext uc) {
User user = uc.getTarget();
return execute(user, context);
}
throw new IllegalArgumentException("Wrong context type for this command");
}
public abstract CommandExecutionResult execute(User target, CommandContext context);
}
@@ -1,22 +0,0 @@
package me.bobert.netneutrality.hooks.interaction;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.context.tasks.ButtonTaskContext;
import me.bobert.netneutrality.core.tasks.TaskManager;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
public class ButtonInteractionHook extends ListenerAdapter {
@Override
public void onButtonInteraction(@NotNull ButtonInteractionEvent event) {
ButtonTaskContext context = new ButtonTaskContext(event);
TaskManager.getInstance().handle(context);
}
}
@@ -1,22 +0,0 @@
package me.bobert.netneutrality.hooks.interaction;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.context.tasks.ModalTaskContext;
import me.bobert.netneutrality.core.tasks.TaskManager;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
public class ModalInteractionHook extends ListenerAdapter {
@Override
public void onModalInteraction(@NotNull ModalInteractionEvent event) {
ModalTaskContext context = new ModalTaskContext(event);
TaskManager.getInstance().handle(context);
}
}
@@ -1,22 +0,0 @@
package me.bobert.netneutrality.hooks.message;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.context.tasks.EmojiTaskContext;
import me.bobert.netneutrality.core.tasks.TaskManager;
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
public class EmojiReactionHook extends ListenerAdapter {
@Override
public void onMessageReactionAdd(@NotNull MessageReactionAddEvent event) {
EmojiTaskContext context = new EmojiTaskContext(event);
TaskManager.getInstance().handle(context);
}
}