diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index adf8f9b..12ef189 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -21,13 +21,13 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") implementation(libs.log4j) - + implementation(libs.geantyref) } // Apply a specific Java toolchain to ease working on different environments. java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(23) } } diff --git a/lib/src/main/java/net/tomatentum/marinara/Marinara.java b/lib/src/main/java/net/tomatentum/marinara/Marinara.java index cbed606..0896858 100644 --- a/lib/src/main/java/net/tomatentum/marinara/Marinara.java +++ b/lib/src/main/java/net/tomatentum/marinara/Marinara.java @@ -1,22 +1,34 @@ package net.tomatentum.marinara; +import net.tomatentum.marinara.registry.InteractionCheckRegistry; import net.tomatentum.marinara.registry.InteractionRegistry; import net.tomatentum.marinara.wrapper.LibraryWrapper; public class Marinara { public static Marinara load(LibraryWrapper wrapper) { - InteractionRegistry registry = new InteractionRegistry(wrapper); - return new Marinara(registry); + return new Marinara(wrapper); } private InteractionRegistry registry; + private InteractionCheckRegistry checkRegistry; + private LibraryWrapper wrapper; - private Marinara(InteractionRegistry registry) { - this.registry = registry; + private Marinara(LibraryWrapper wrapper) { + this.wrapper = wrapper; + this.registry = new InteractionRegistry(this); + this.checkRegistry = new InteractionCheckRegistry(); } public InteractionRegistry getRegistry() { return registry; } + + public InteractionCheckRegistry getCheckRegistry() { + return checkRegistry; + } + + public LibraryWrapper getWrapper() { + return wrapper; + } } diff --git a/lib/src/main/java/net/tomatentum/marinara/checks/AppliedCheck.java b/lib/src/main/java/net/tomatentum/marinara/checks/AppliedCheck.java new file mode 100644 index 0000000..0383ac6 --- /dev/null +++ b/lib/src/main/java/net/tomatentum/marinara/checks/AppliedCheck.java @@ -0,0 +1,41 @@ +package net.tomatentum.marinara.checks; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +import net.tomatentum.marinara.util.ReflectionUtil; + +public record AppliedCheck(InteractionCheck check, Annotation annotation) { + + public boolean pre(Object context) { + Method[] methods = Arrays.stream(check.getClass().getMethods()) + .filter(x -> x.getName().equals("preExec")) + .filter(x -> !x.isBridge()) + .toArray(s -> new Method[s]); + Method method = ReflectionUtil.getMostSpecificMethod(methods, context.getClass(), annotation.annotationType()); + method.setAccessible(true); + try { + return (boolean) method.invoke(check, context, annotation); + } catch (IllegalAccessException | InvocationTargetException | SecurityException e) { + e.printStackTrace(); + return false; + } + } + + public void post(Object context) { + Method[] methods = Arrays.stream(check.getClass().getMethods()) + .filter(x -> x.getName().equals("postExec")) + .filter(x -> !x.isBridge()) + .toArray(s -> new Method[s]); + Method method = ReflectionUtil.getMostSpecificMethod(methods, context.getClass(), annotation.annotationType()); + method.setAccessible(true); + try { + method.invoke(check, context, annotation); + } catch (IllegalAccessException | InvocationTargetException | SecurityException e) { + e.printStackTrace(); + } + } + +} diff --git a/lib/src/main/java/net/tomatentum/marinara/checks/InteractionCheck.java b/lib/src/main/java/net/tomatentum/marinara/checks/InteractionCheck.java new file mode 100644 index 0000000..4b38aa4 --- /dev/null +++ b/lib/src/main/java/net/tomatentum/marinara/checks/InteractionCheck.java @@ -0,0 +1,10 @@ +package net.tomatentum.marinara.checks; + +import java.lang.annotation.Annotation; + +public interface InteractionCheck { + + public boolean preExec(Object context, A annotation); + public void postExec(Object context, A annotation); + +} diff --git a/lib/src/main/java/net/tomatentum/marinara/interaction/commands/ExecutableSlashCommandDefinition.java b/lib/src/main/java/net/tomatentum/marinara/interaction/commands/ExecutableSlashCommandDefinition.java index 933a4a0..e110098 100644 --- a/lib/src/main/java/net/tomatentum/marinara/interaction/commands/ExecutableSlashCommandDefinition.java +++ b/lib/src/main/java/net/tomatentum/marinara/interaction/commands/ExecutableSlashCommandDefinition.java @@ -16,9 +16,18 @@ public record ExecutableSlashCommandDefinition( if (!(o instanceof ExecutableSlashCommandDefinition)) return false; ExecutableSlashCommandDefinition other = (ExecutableSlashCommandDefinition) o; - return other.applicationCommand.name().equals(this.applicationCommand.name()) && - other.subCommandGroup.name().equals(this.subCommandGroup.name()) && - other.subCommand.name().equals(this.subCommand.name()); + boolean equals = false; + + if (this.applicationCommand() != null && other.subCommandGroup() != null) + equals = this.applicationCommand.name().equals(other.applicationCommand().name()); + + if (this.subCommandGroup() != null && other.subCommandGroup() != null) + equals = this.subCommandGroup().name().equals(other.subCommandGroup().name()); + + if (this.subCommand() != null && other.subCommand() != null) + equals = this.subCommand().name().equals(other.subCommand().name()); + + return equals; } @Override diff --git a/lib/src/main/java/net/tomatentum/marinara/interaction/methods/ButtonInteractionMethod.java b/lib/src/main/java/net/tomatentum/marinara/interaction/methods/ButtonInteractionMethod.java index baf8f4e..f6b2a9e 100644 --- a/lib/src/main/java/net/tomatentum/marinara/interaction/methods/ButtonInteractionMethod.java +++ b/lib/src/main/java/net/tomatentum/marinara/interaction/methods/ButtonInteractionMethod.java @@ -2,39 +2,40 @@ package net.tomatentum.marinara.interaction.methods; import java.lang.reflect.Method; +import net.tomatentum.marinara.Marinara; import net.tomatentum.marinara.interaction.InteractionHandler; import net.tomatentum.marinara.interaction.InteractionType; -import net.tomatentum.marinara.interaction.annotation.Button; -import net.tomatentum.marinara.wrapper.LibraryWrapper; +import net.tomatentum.marinara.parser.AnnotationParser; +import net.tomatentum.marinara.parser.ButtonParser; public class ButtonInteractionMethod extends InteractionMethod { private String customId; - ButtonInteractionMethod(Method method, InteractionHandler handler, LibraryWrapper wrapper) { - super(method, handler, wrapper); - parseMethod(); + ButtonInteractionMethod(Method method, InteractionHandler handler, Marinara marinara) { + super(method, handler, marinara); + } + + @Override + public AnnotationParser[] getParsers() { + return new AnnotationParser[] { + new ButtonParser(method, (x) -> { this.customId = x; } ) + }; } @Override public Object getParameter(Object parameter, int index) { Class type = getMethod().getParameterTypes()[index+1]; - return wrapper.getComponentContextObject(parameter, type); + return marinara.getWrapper().getComponentContextObject(parameter, type); } @Override public boolean canRun(Object context) { - return wrapper.getButtonId(context).equals(customId); + return marinara.getWrapper().getButtonId(context).equals(customId); } @Override public InteractionType getType() { return InteractionType.BUTTON; } - - private void parseMethod() { - Button button = getMethod().getAnnotation(Button.class); - this.customId = button.value(); - } - } diff --git a/lib/src/main/java/net/tomatentum/marinara/interaction/methods/InteractionMethod.java b/lib/src/main/java/net/tomatentum/marinara/interaction/methods/InteractionMethod.java index 0f6b09e..f9c5a1c 100644 --- a/lib/src/main/java/net/tomatentum/marinara/interaction/methods/InteractionMethod.java +++ b/lib/src/main/java/net/tomatentum/marinara/interaction/methods/InteractionMethod.java @@ -7,35 +7,52 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import net.tomatentum.marinara.Marinara; +import net.tomatentum.marinara.checks.AppliedCheck; import net.tomatentum.marinara.interaction.InteractionHandler; import net.tomatentum.marinara.interaction.InteractionType; import net.tomatentum.marinara.interaction.annotation.Button; import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand; import net.tomatentum.marinara.interaction.commands.annotation.SubCommand; -import net.tomatentum.marinara.wrapper.LibraryWrapper; +import net.tomatentum.marinara.parser.AnnotationParser; +import net.tomatentum.marinara.parser.InteractionCheckParser; public abstract class InteractionMethod { - public static InteractionMethod create(Method method, InteractionHandler handler, LibraryWrapper wrapper) { + public static InteractionMethod create(Method method, InteractionHandler handler, Marinara marinara) { if (method.isAnnotationPresent(SlashCommand.class) || method.isAnnotationPresent(SubCommand.class)) - return new SlashCommandInteractionMethod(method, handler, wrapper); + return new SlashCommandInteractionMethod(method, handler, marinara); if (method.isAnnotationPresent(Button.class)) - return new ButtonInteractionMethod(method, handler, wrapper); + return new ButtonInteractionMethod(method, handler, marinara); return null; } protected Method method; protected InteractionHandler handler; - protected LibraryWrapper wrapper; + protected Marinara marinara; + protected List parsers; + protected List appliedChecks; - protected InteractionMethod(Method method, InteractionHandler handler, LibraryWrapper wrapper) { + protected InteractionMethod(Method method, + InteractionHandler handler, + Marinara marinara + ) { if (!Arrays.asList(handler.getClass().getMethods()).contains(method)) throw new InvalidParameterException("Method does not apply to specified handler"); + this.method = method; this.handler = handler; - this.wrapper = wrapper; + this.marinara = marinara; + this.parsers = new ArrayList<>(Arrays.asList(getParsers())); + this.appliedChecks = new ArrayList<>(); + + parsers.add(new InteractionCheckParser(method, appliedChecks::add, marinara.getCheckRegistry())); + + parsers.stream().forEach(AnnotationParser::parse); } + public abstract AnnotationParser[] getParsers(); + public abstract Object getParameter(Object parameter, int index); public abstract boolean canRun(Object context); @@ -43,6 +60,24 @@ public abstract class InteractionMethod { public abstract InteractionType getType(); public void run(Object context) { + if (this.appliedChecks.stream().filter(x -> !x.pre(context)).count() > 0) + return; + + method.setAccessible(true); + try { + method.invoke(handler, getParameters(context)); + }catch (IllegalAccessException | InvocationTargetException ex) { + throw new RuntimeException(ex); + } + + this.appliedChecks.forEach(x -> x.post(context)); + } + + public Method getMethod() { + return method; + } + + private Object[] getParameters(Object context) { int parameterCount = method.getParameterCount(); List parameters = new ArrayList<>(); @@ -53,16 +88,7 @@ public abstract class InteractionMethod { } parameters.add(getParameter(context, i-1)); } - method.setAccessible(true); - try { - method.invoke(handler, parameters.toArray()); - }catch (IllegalAccessException | InvocationTargetException ex) { - throw new RuntimeException(ex); - } - } - - public Method getMethod() { - return method; + return parameters.toArray(); } } diff --git a/lib/src/main/java/net/tomatentum/marinara/interaction/methods/SlashCommandInteractionMethod.java b/lib/src/main/java/net/tomatentum/marinara/interaction/methods/SlashCommandInteractionMethod.java index 6498078..666dd3c 100644 --- a/lib/src/main/java/net/tomatentum/marinara/interaction/methods/SlashCommandInteractionMethod.java +++ b/lib/src/main/java/net/tomatentum/marinara/interaction/methods/SlashCommandInteractionMethod.java @@ -2,32 +2,36 @@ package net.tomatentum.marinara.interaction.methods; import java.lang.reflect.Method; +import net.tomatentum.marinara.Marinara; import net.tomatentum.marinara.interaction.InteractionHandler; import net.tomatentum.marinara.interaction.InteractionType; import net.tomatentum.marinara.interaction.commands.ExecutableSlashCommandDefinition; -import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand; -import net.tomatentum.marinara.interaction.commands.annotation.SubCommand; -import net.tomatentum.marinara.interaction.commands.annotation.SubCommandGroup; -import net.tomatentum.marinara.util.ReflectionUtil; -import net.tomatentum.marinara.wrapper.LibraryWrapper; +import net.tomatentum.marinara.parser.AnnotationParser; +import net.tomatentum.marinara.parser.SlashCommandParser; public class SlashCommandInteractionMethod extends InteractionMethod { private ExecutableSlashCommandDefinition commandDefinition; - SlashCommandInteractionMethod(Method method, InteractionHandler handler, LibraryWrapper wrapper) { - super(method, handler, wrapper); - parseMethod(); + SlashCommandInteractionMethod(Method method, InteractionHandler handler, Marinara marinara) { + super(method, handler, marinara); + } + + @Override + public AnnotationParser[] getParsers() { + return new AnnotationParser[] { + new SlashCommandParser(method, (x) -> { this.commandDefinition = x; } ) + }; } @Override public Object getParameter(Object context, int index) { - return wrapper.convertCommandOption(context, commandDefinition.options()[index].type(), commandDefinition.options()[index].name()); + return marinara.getWrapper().convertCommandOption(context, commandDefinition.options()[index].type(), commandDefinition.options()[index].name()); } @Override public boolean canRun(Object context) { - ExecutableSlashCommandDefinition other = wrapper.getCommandDefinition(context); + ExecutableSlashCommandDefinition other = marinara.getWrapper().getCommandDefinition(context); return commandDefinition.equals(other); } @@ -40,24 +44,8 @@ public class SlashCommandInteractionMethod extends InteractionMethod { return commandDefinition; } - private void parseMethod() { - ReflectionUtil.checkValidCommandMethod(method); - - SlashCommand cmd = ReflectionUtil.getAnnotation(method, SlashCommand.class); - ExecutableSlashCommandDefinition.Builder builder = new ExecutableSlashCommandDefinition.Builder(); - builder.setApplicationCommand(cmd); - - if (ReflectionUtil.isAnnotationPresent(method, SubCommandGroup.class)) { - SubCommandGroup cmdGroup = ReflectionUtil.getAnnotation(method, SubCommandGroup.class); - builder.setSubCommandGroup(cmdGroup); - } - - if (ReflectionUtil.isAnnotationPresent(method, SubCommand.class)) { - SubCommand subCmd = ReflectionUtil.getAnnotation(method, SubCommand.class); - builder.setSubCommand(subCmd); - } - - this.commandDefinition = builder.build(); + public void setCommandDefinition(ExecutableSlashCommandDefinition commandDefinition) { + this.commandDefinition = commandDefinition; } } diff --git a/lib/src/main/java/net/tomatentum/marinara/parser/AnnotationParser.java b/lib/src/main/java/net/tomatentum/marinara/parser/AnnotationParser.java new file mode 100644 index 0000000..187a4a4 --- /dev/null +++ b/lib/src/main/java/net/tomatentum/marinara/parser/AnnotationParser.java @@ -0,0 +1,8 @@ +package net.tomatentum.marinara.parser; + +import java.lang.reflect.Method; + +public interface AnnotationParser { + void parse(); + Method getMethod(); +} diff --git a/lib/src/main/java/net/tomatentum/marinara/parser/ButtonParser.java b/lib/src/main/java/net/tomatentum/marinara/parser/ButtonParser.java new file mode 100644 index 0000000..1b7dcbb --- /dev/null +++ b/lib/src/main/java/net/tomatentum/marinara/parser/ButtonParser.java @@ -0,0 +1,29 @@ +package net.tomatentum.marinara.parser; + +import java.lang.reflect.Method; +import java.util.function.Consumer; + +import net.tomatentum.marinara.interaction.annotation.Button; + +public class ButtonParser implements AnnotationParser { + + private Method method; + private Consumer consumer; + + public ButtonParser(Method method, Consumer consumer) { + this.method = method; + this.consumer = consumer; + } + + @Override + public void parse() { + Button button = getMethod().getAnnotation(Button.class); + this.consumer.accept(button.value()); + } + + @Override + public Method getMethod() { + return this.method; + } + +} diff --git a/lib/src/main/java/net/tomatentum/marinara/parser/InteractionCheckParser.java b/lib/src/main/java/net/tomatentum/marinara/parser/InteractionCheckParser.java new file mode 100644 index 0000000..abba9e9 --- /dev/null +++ b/lib/src/main/java/net/tomatentum/marinara/parser/InteractionCheckParser.java @@ -0,0 +1,42 @@ +package net.tomatentum.marinara.parser; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Consumer; + +import net.tomatentum.marinara.checks.AppliedCheck; +import net.tomatentum.marinara.checks.InteractionCheck; +import net.tomatentum.marinara.registry.InteractionCheckRegistry; + +public class InteractionCheckParser implements AnnotationParser { + + private InteractionCheckRegistry checkRegistry; + private Method method; + private Consumer consumer; + + public InteractionCheckParser(Method method, Consumer consumer, InteractionCheckRegistry checkRegistry) { + this.checkRegistry = checkRegistry; + this.method = method; + this.consumer = consumer; + } + + @Override + public void parse() { + Annotation[] annotations = method.getAnnotations(); + Arrays.stream(annotations).forEach(this::convertAnnotation); + } + + private void convertAnnotation(Annotation annotation) { + Optional> check = this.checkRegistry.getCheckFromAnnotation(annotation.annotationType()); + if (check.isPresent()) + consumer.accept(new AppliedCheck(check.get(), annotation)); + } + + @Override + public Method getMethod() { + return this.method; + } + +} diff --git a/lib/src/main/java/net/tomatentum/marinara/parser/SlashCommandParser.java b/lib/src/main/java/net/tomatentum/marinara/parser/SlashCommandParser.java new file mode 100644 index 0000000..8997d9f --- /dev/null +++ b/lib/src/main/java/net/tomatentum/marinara/parser/SlashCommandParser.java @@ -0,0 +1,63 @@ +package net.tomatentum.marinara.parser; + +import java.lang.reflect.Method; +import java.util.function.Consumer; + +import net.tomatentum.marinara.interaction.commands.ExecutableSlashCommandDefinition; +import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand; +import net.tomatentum.marinara.interaction.commands.annotation.SubCommand; +import net.tomatentum.marinara.interaction.commands.annotation.SubCommandGroup; +import net.tomatentum.marinara.util.ReflectionUtil; + +public class SlashCommandParser implements AnnotationParser { + + private Method method; + private Consumer consumer; + + public SlashCommandParser(Method method, Consumer consumer) { + this.method = method; + this.consumer = consumer; + } + + @Override + public void parse() { + this.checkValidCommandMethod(method); + + SlashCommand cmd = ReflectionUtil.getAnnotation(method, SlashCommand.class); + ExecutableSlashCommandDefinition.Builder builder = new ExecutableSlashCommandDefinition.Builder(); + builder.setApplicationCommand(cmd); + + if (ReflectionUtil.isAnnotationPresent(method, SubCommandGroup.class)) { + SubCommandGroup cmdGroup = ReflectionUtil.getAnnotation(method, SubCommandGroup.class); + builder.setSubCommandGroup(cmdGroup); + } + + if (ReflectionUtil.isAnnotationPresent(method, SubCommand.class)) { + SubCommand subCmd = ReflectionUtil.getAnnotation(method, SubCommand.class); + builder.setSubCommand(subCmd); + } + + consumer.accept(builder.build()); + } + + @Override + public Method getMethod() { + return this.method; + } + + private void checkValidCommandMethod(Method method) { + if (method.isAnnotationPresent(SlashCommand.class) && + method.getDeclaringClass().isAnnotationPresent(SlashCommand.class)) { + throw new RuntimeException(method.getName() + ": Can't have ApplicationCommand Annotation on Class and Method"); + } + + if (!ReflectionUtil.isAnnotationPresent(method, SlashCommand.class)) + throw new RuntimeException(method.getName() + ": Missing ApplicationCommand Annotation on either Class or Method"); + + if ((method.isAnnotationPresent(SubCommand.class) && + !ReflectionUtil.isAnnotationPresent(method, SlashCommand.class))) { + throw new RuntimeException(method.getName() + ": Missing ApplicationCommand Annotation on either Method or Class"); + } + } + +} diff --git a/lib/src/main/java/net/tomatentum/marinara/registry/InteractionCheckRegistry.java b/lib/src/main/java/net/tomatentum/marinara/registry/InteractionCheckRegistry.java new file mode 100644 index 0000000..370419e --- /dev/null +++ b/lib/src/main/java/net/tomatentum/marinara/registry/InteractionCheckRegistry.java @@ -0,0 +1,34 @@ +package net.tomatentum.marinara.registry; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import io.leangen.geantyref.GenericTypeReflector; +import net.tomatentum.marinara.checks.InteractionCheck; + +public class InteractionCheckRegistry { + + private List> checks; + + public InteractionCheckRegistry() { + this.checks = new ArrayList<>(); + } + + public void addCheck(InteractionCheck check) { + checks.add(check); + } + + public Optional> getCheckFromAnnotation(Type annotation) { + for (InteractionCheck interactionCheck : checks) { + ParameterizedType type = (ParameterizedType) GenericTypeReflector.getExactSuperType(interactionCheck.getClass(), InteractionCheck.class); + Type typeParam = type.getActualTypeArguments()[0]; + if (typeParam.equals(annotation)) + return Optional.of(interactionCheck); + } + return Optional.empty(); + } + +} diff --git a/lib/src/main/java/net/tomatentum/marinara/registry/InteractionRegistry.java b/lib/src/main/java/net/tomatentum/marinara/registry/InteractionRegistry.java index 31e3ad1..d1640d6 100644 --- a/lib/src/main/java/net/tomatentum/marinara/registry/InteractionRegistry.java +++ b/lib/src/main/java/net/tomatentum/marinara/registry/InteractionRegistry.java @@ -5,27 +5,27 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import net.tomatentum.marinara.Marinara; import net.tomatentum.marinara.interaction.InteractionHandler; import net.tomatentum.marinara.interaction.InteractionType; import net.tomatentum.marinara.interaction.commands.SlashCommandDefinition; import net.tomatentum.marinara.interaction.commands.ExecutableSlashCommandDefinition; import net.tomatentum.marinara.interaction.methods.SlashCommandInteractionMethod; import net.tomatentum.marinara.interaction.methods.InteractionMethod; -import net.tomatentum.marinara.wrapper.LibraryWrapper; public class InteractionRegistry { private List interactionMethods; - private LibraryWrapper wrapper; + private Marinara marinara; - public InteractionRegistry(LibraryWrapper wrapper) { + public InteractionRegistry(Marinara marinara) { this.interactionMethods = new ArrayList<>(); - this.wrapper = wrapper; - wrapper.subscribeInteractions(this::handle); + this.marinara = marinara; + marinara.getWrapper().subscribeInteractions(this::handle); } public void addInteractions(InteractionHandler interactionHandler) { for (Method method : interactionHandler.getClass().getMethods()) { - InteractionMethod iMethod = InteractionMethod.create(method, interactionHandler, wrapper); + InteractionMethod iMethod = InteractionMethod.create(method, interactionHandler, marinara); if (iMethod != null) this.interactionMethods.add(iMethod); } @@ -48,13 +48,13 @@ public class InteractionRegistry { defs.add(new SlashCommandDefinition(def.applicationCommand()).addExecutableCommand(def)); }); - wrapper.registerSlashCommands(defs.toArray(new SlashCommandDefinition[0])); + marinara.getWrapper().registerSlashCommands(defs.toArray(new SlashCommandDefinition[0])); } public void handle(Object context) { interactionMethods.forEach((m) -> { - InteractionType type = wrapper.getInteractionType(context.getClass()); - if (m.getType().equals(type)) + InteractionType type = marinara.getWrapper().getInteractionType(context.getClass()); + if (m.getType().equals(type) && m.canRun(context)) m.run(context); }); } diff --git a/lib/src/main/java/net/tomatentum/marinara/util/ReflectionUtil.java b/lib/src/main/java/net/tomatentum/marinara/util/ReflectionUtil.java index 63105bf..2a65282 100644 --- a/lib/src/main/java/net/tomatentum/marinara/util/ReflectionUtil.java +++ b/lib/src/main/java/net/tomatentum/marinara/util/ReflectionUtil.java @@ -2,9 +2,10 @@ package net.tomatentum.marinara.util; import java.lang.annotation.Annotation; import java.lang.reflect.Method; - -import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand; -import net.tomatentum.marinara.interaction.commands.annotation.SubCommand; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; public final class ReflectionUtil { @@ -21,19 +22,82 @@ public final class ReflectionUtil { method.getDeclaringClass().getAnnotation(annotationClass); } - public static void checkValidCommandMethod(Method method) { - if (method.isAnnotationPresent(SlashCommand.class) && - method.getDeclaringClass().isAnnotationPresent(SlashCommand.class)) { - throw new RuntimeException(method.getName() + ": Can't have ApplicationCommand Annotation on Class and Method"); + public static int getCastDepth(Class child, Class parent) { + + if (parent.equals(Object.class)) + return Integer.MAX_VALUE; + + if (!parent.isAssignableFrom(child)) { + throw new IllegalArgumentException("The specified class is not a child class of the specified parent."); } - if (!isAnnotationPresent(method, SlashCommand.class)) - throw new RuntimeException(method.getName() + ": Missing ApplicationCommand Annotation on either Class or Method"); + int depth = 0; + Class curr = child; + List> parents = new ArrayList<>(); - if ((method.isAnnotationPresent(SubCommand.class) && - !isAnnotationPresent(method, SlashCommand.class))) { - throw new RuntimeException(method.getName() + ": Missing ApplicationCommand Annotation on either Method or Class"); + while (!curr.equals(parent)) { + depth++; + parents.add(curr.getSuperclass()); + parents.addAll(Arrays.asList(curr.getInterfaces())); + + for (Class currParent : parents) { + if (currParent != null && parent.isAssignableFrom(currParent)) { + curr = currParent; + break; + } + } + parents.clear(); } + + return depth; } + public static Method getMostSpecificMethod(Method[] methods, Class... parameters) { + List compatibleMethods = Arrays.stream(methods) + .filter(x -> isMethodCallable(x, parameters)) + .toList(); + + if (compatibleMethods.size() == 0) + throw new IllegalArgumentException("There are no compatible Methods provided"); + + for (int i = 0; i < parameters.length; i++) { + final int currI = i; + Class[] parameterTypes = compatibleMethods.stream() + .map(x -> x.getParameterTypes()[currI]) + .toArray(x -> new Class[x]); + + Class mostSpecific = getMostSpecificClass(parameterTypes, parameters[i]); + + compatibleMethods = compatibleMethods.stream() + .filter(x -> Objects.equals(x.getParameterTypes()[currI], mostSpecific)) + .toList(); + } + + return compatibleMethods.getFirst(); + } + + public static Class getMostSpecificClass(Class[] classes, Class base) { + int min = Integer.MAX_VALUE; + Class currMostSpecific = null; + for (Class currClass : classes) { + int currCastDepth = getCastDepth(base, currClass); + if (currCastDepth <= min) { + min = currCastDepth; + currMostSpecific = currClass; + } + } + return currMostSpecific; + } + + public static boolean isMethodCallable(Method method, Class... parameters) { + if (!Objects.equals(method.getParameterCount(), parameters.length)) + return false; + + Class[] methodParams = method.getParameterTypes(); + for (int i = 0; i < parameters.length; i++) { + if (!methodParams[i].isAssignableFrom(parameters[i])) + return false; + } + return true; + } } diff --git a/wrapper/javacord/build.gradle.kts b/wrapper/javacord/build.gradle.kts index 9ec7322..2d1e7c7 100644 --- a/wrapper/javacord/build.gradle.kts +++ b/wrapper/javacord/build.gradle.kts @@ -29,7 +29,7 @@ dependencies { // Apply a specific Java toolchain to ease working on different environments. java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(23) } } diff --git a/wrapper/javacord/src/main/java/net/tomatentum/marinara/wrapper/javacord/checks/PermissionCheck.java b/wrapper/javacord/src/main/java/net/tomatentum/marinara/wrapper/javacord/checks/PermissionCheck.java new file mode 100644 index 0000000..0355541 --- /dev/null +++ b/wrapper/javacord/src/main/java/net/tomatentum/marinara/wrapper/javacord/checks/PermissionCheck.java @@ -0,0 +1,41 @@ +package net.tomatentum.marinara.wrapper.javacord.checks; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Optional; + +import org.javacord.api.entity.permission.PermissionType; +import org.javacord.api.entity.server.Server; +import org.javacord.api.interaction.InteractionBase; + +import net.tomatentum.marinara.checks.InteractionCheck; + +public class PermissionCheck implements InteractionCheck { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public static @interface HasPermission { + public PermissionType[] value(); + } + + @Override + public boolean preExec(Object context, HasPermission annotation) { + throw new UnsupportedOperationException("Unimplemented method 'preExec'"); + } + + public boolean preExec(InteractionBase context, HasPermission annotation) { + Optional server = context.getServer(); + if (!server.isPresent()) + return false; + + return server.get().hasPermissions(context.getUser(), annotation.value()); + } + + @Override + public void postExec(Object context, HasPermission annotation) { + + } + +} diff --git a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/ButtonTest.java b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/ButtonTest.java index b8ffa0f..db08352 100644 --- a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/ButtonTest.java +++ b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/ButtonTest.java @@ -20,7 +20,7 @@ public class ButtonTest { LibraryWrapper wrapper = new JavacordWrapper(new DiscordApiMock()); //null okay as we don't use the discord API in this test. Marinara marinara = Marinara.load(wrapper); marinara.getRegistry().addInteractions(new TestButton()); - wrapper.handleInteraction(new ButtonInteractionMock()); + wrapper.handleInteraction(new ButtonInteractionMock("test")); assertTrue(TestButton.didRun); } diff --git a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/InteractionCheckTest.java b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/InteractionCheckTest.java new file mode 100644 index 0000000..aed9f5a --- /dev/null +++ b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/InteractionCheckTest.java @@ -0,0 +1,47 @@ +package net.tomatentum.marinara.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.javacord.api.entity.permission.PermissionType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +import net.tomatentum.marinara.Marinara; +import net.tomatentum.marinara.test.mocks.ButtonInteractionMock; +import net.tomatentum.marinara.test.mocks.DiscordApiMock; +import net.tomatentum.marinara.test.mocks.ServerMock; +import net.tomatentum.marinara.wrapper.LibraryWrapper; +import net.tomatentum.marinara.wrapper.javacord.JavacordWrapper; +import net.tomatentum.marinara.wrapper.javacord.checks.PermissionCheck; + +@TestInstance(Lifecycle.PER_CLASS) +public class InteractionCheckTest { + + @Test + public void testInteractionCheck() { + LibraryWrapper wrapper = new JavacordWrapper(new DiscordApiMock()); + Marinara marinara = Marinara.load(wrapper); + marinara.getCheckRegistry().addCheck(new TestInteractionCheck()); + marinara.getRegistry().addInteractions(new TestButton()); + wrapper.handleInteraction(new ButtonInteractionMock("test")); + assertTrue(TestInteractionCheck.preExecuted); + assertTrue(TestInteractionCheck.postExecuted); + } + + @Test + public void testPermissionCheck() { + LibraryWrapper wrapper = new JavacordWrapper(new DiscordApiMock()); + Marinara marinara = Marinara.load(wrapper); + marinara.getCheckRegistry().addCheck(new PermissionCheck()); + marinara.getRegistry().addInteractions(new TestButton()); + wrapper.handleInteraction(new ButtonInteractionMock("permissionCheck")); + assertTrue(TestButton.didPermRun); + TestButton.didPermRun = false; + ServerMock.TESTPERMISSION = PermissionType.ATTACH_FILE; + wrapper.handleInteraction(new ButtonInteractionMock("permissionCheck")); + assertFalse(TestButton.didPermRun); + } + +} diff --git a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/TestButton.java b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/TestButton.java index c72b8f9..23a7dbf 100644 --- a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/TestButton.java +++ b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/TestButton.java @@ -4,18 +4,22 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import org.javacord.api.entity.channel.TextChannel; import org.javacord.api.entity.message.Message; +import org.javacord.api.entity.permission.PermissionType; import org.javacord.api.entity.server.Server; import org.javacord.api.entity.user.User; import org.javacord.api.interaction.ButtonInteraction; import net.tomatentum.marinara.interaction.InteractionHandler; import net.tomatentum.marinara.interaction.annotation.Button; +import net.tomatentum.marinara.test.TestInteractionCheck.TestCheck; +import net.tomatentum.marinara.wrapper.javacord.checks.PermissionCheck.HasPermission; public class TestButton implements InteractionHandler { public static boolean didRun = false; @Button("test") + @TestCheck public void exec(ButtonInteraction interaction, TextChannel channel, Message message, User member, Server server) { assertNotNull(interaction); assertNotNull(channel); @@ -25,5 +29,14 @@ public class TestButton implements InteractionHandler { didRun = true; System.out.println("Success!"); } + + public static boolean didPermRun = false; + + @Button("permissionCheck") + @HasPermission({PermissionType.ADMINISTRATOR}) + public void exec(ButtonInteraction interaction) { + didPermRun = true; + System.out.println("It worked!"); + } } diff --git a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/TestInteractionCheck.java b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/TestInteractionCheck.java new file mode 100644 index 0000000..ed1339b --- /dev/null +++ b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/TestInteractionCheck.java @@ -0,0 +1,37 @@ +package net.tomatentum.marinara.test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import net.tomatentum.marinara.checks.InteractionCheck; + +public class TestInteractionCheck implements InteractionCheck { + + public static boolean preExecuted = false; + public static boolean postExecuted = false; + + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + public static @interface TestCheck { + } + + @Override + public boolean preExec(Object context, TestCheck annotation) { + assertNotNull(annotation); + assertNotNull(context); + preExecuted = true; + return true; + } + + @Override + public void postExec(Object context, TestCheck annotation) { + assertNotNull(annotation); + assertNotNull(context); + postExecuted = true; + } + +} diff --git a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/mocks/ButtonInteractionMock.java b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/mocks/ButtonInteractionMock.java index c9ee36b..3e3e8fd 100644 --- a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/mocks/ButtonInteractionMock.java +++ b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/mocks/ButtonInteractionMock.java @@ -23,6 +23,12 @@ import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater; public class ButtonInteractionMock implements ButtonInteraction { + private String customId; + + public ButtonInteractionMock(String customId) { + this.customId = customId; + } + @Override public Message getMessage() { return new MessageMock(); @@ -30,7 +36,7 @@ public class ButtonInteractionMock implements ButtonInteraction { @Override public String getCustomId() { - return "test"; + return this.customId; } @Override diff --git a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/mocks/ServerMock.java b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/mocks/ServerMock.java index 5a0daaa..275c8ab 100644 --- a/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/mocks/ServerMock.java +++ b/wrapper/javacord/src/test/java/net/tomatentum/marinara/test/mocks/ServerMock.java @@ -1,5 +1,7 @@ package net.tomatentum.marinara.test.mocks; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import java.time.Instant; import java.util.Collection; import java.util.EnumSet; @@ -27,6 +29,7 @@ import org.javacord.api.entity.channel.ServerTextChannel; import org.javacord.api.entity.channel.ServerThreadChannel; import org.javacord.api.entity.channel.ServerVoiceChannel; import org.javacord.api.entity.emoji.KnownCustomEmoji; +import org.javacord.api.entity.permission.PermissionType; import org.javacord.api.entity.permission.Role; import org.javacord.api.entity.server.ActiveThreads; import org.javacord.api.entity.server.Ban; @@ -2259,5 +2262,12 @@ public class ServerMock implements Server { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getSystemChannelFlags'"); } + public static PermissionType TESTPERMISSION = PermissionType.ADMINISTRATOR; + @Override + public boolean hasPermissions(User user, PermissionType... type) { + assertNotNull(user); + assertNotNull(type); + return TESTPERMISSION.equals(type[0]); + } }