diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/CommandOptionType.java b/annotations/src/main/java/com/javadiscord/jdi/core/CommandOptionType.java new file mode 100644 index 00000000..4e89d862 --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/core/CommandOptionType.java @@ -0,0 +1,26 @@ +package com.javadiscord.jdi.core; + +public enum CommandOptionType { + SUB_COMMAND(1), + SUB_COMMAND_GROUP(2), + STRING(3), + INTEGER(4), + BOOLEAN(5), + USER(6), + CHANNEL(7), + ROLE(8), + MENTIONABLE(9), + NUMBER(10), + ATTACHMENT(11), + ; + + private final int value; + + CommandOptionType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/annotations/CommandOption.java b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/CommandOption.java new file mode 100644 index 00000000..aadce25a --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/CommandOption.java @@ -0,0 +1,21 @@ +package com.javadiscord.jdi.core.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.javadiscord.jdi.core.CommandOptionType; + +@Retention(RetentionPolicy.RUNTIME) +@Target({}) +public @interface CommandOption { + String name(); + + String description(); + + CommandOptionType type(); + + CommandOptionChoice[] choices() default {}; + + boolean required() default true; +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/annotations/CommandOptionChoice.java b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/CommandOptionChoice.java new file mode 100644 index 00000000..8ae2513a --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/CommandOptionChoice.java @@ -0,0 +1,13 @@ +package com.javadiscord.jdi.core.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({}) +public @interface CommandOptionChoice { + String name(); + + String value(); +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/annotations/Component.java b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/Component.java new file mode 100644 index 00000000..6004f9fb --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/Component.java @@ -0,0 +1,10 @@ +package com.javadiscord.jdi.core.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface Component {} diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/annotations/Inject.java b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/Inject.java new file mode 100644 index 00000000..67aebb96 --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/Inject.java @@ -0,0 +1,10 @@ +package com.javadiscord.jdi.core.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface Inject {} diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/annotations/SlashCommand.java b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/SlashCommand.java new file mode 100644 index 00000000..3cbe6d0c --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/core/annotations/SlashCommand.java @@ -0,0 +1,16 @@ +package com.javadiscord.jdi.core.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface SlashCommand { + String name(); + + String description(); + + CommandOption[] options() default {}; +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/ComponentInjectionException.java b/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/ComponentInjectionException.java new file mode 100644 index 00000000..42e2407b --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/ComponentInjectionException.java @@ -0,0 +1,13 @@ +package com.javadiscord.jdi.internal.exceptions; + +public class ComponentInjectionException extends RuntimeException { + + public ComponentInjectionException() { + super(); + } + + public ComponentInjectionException(String message) { + super(message); + } + +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/NoZeroArgConstructorException.java b/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/NoZeroArgConstructorException.java new file mode 100644 index 00000000..15b71d21 --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/NoZeroArgConstructorException.java @@ -0,0 +1,13 @@ +package com.javadiscord.jdi.internal.exceptions; + +public class NoZeroArgConstructorException extends RuntimeException { + + public NoZeroArgConstructorException() { + super(); + } + + public NoZeroArgConstructorException(String message) { + super(message); + } + +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/ValidationException.java b/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/ValidationException.java new file mode 100644 index 00000000..2e610070 --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/exceptions/ValidationException.java @@ -0,0 +1,12 @@ +package com.javadiscord.jdi.internal.exceptions; + +public class ValidationException extends RuntimeException { + + public ValidationException() { + super(); + } + + public ValidationException(String message) { + super(message); + } +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/processor/ClassFileUtil.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/ClassFileUtil.java similarity index 61% rename from annotations/src/main/java/com/javadiscord/jdi/core/processor/ClassFileUtil.java rename to annotations/src/main/java/com/javadiscord/jdi/internal/processor/ClassFileUtil.java index a03aafbb..42acacfb 100644 --- a/annotations/src/main/java/com/javadiscord/jdi/core/processor/ClassFileUtil.java +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/ClassFileUtil.java @@ -1,10 +1,13 @@ -package com.javadiscord.jdi.core.processor; +package com.javadiscord.jdi.internal.processor; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; @@ -12,52 +15,61 @@ import javassist.bytecode.ClassFile; public class ClassFileUtil { + private static final List classesInPath = new ArrayList<>(); + private static boolean loadedParentJar = false; - private ClassFileUtil() {} + private ClassFileUtil() { + throw new UnsupportedOperationException("Utility class"); + } public static List getClassesInClassPath() { - List classes = new ArrayList<>(); - String classpath = System.getProperty("java.class.path"); - String[] classpathEntries = classpath.split(File.pathSeparator); - for (String entry : classpathEntries) { - File file = new File(entry); - try { - classes.addAll(getClasses(file)); - } catch (IOException ignore) { - /* Ignore */ + if (classesInPath.isEmpty()) { + String classpath = System.getProperty("java.class.path"); + String[] classpathEntries = classpath.split(File.pathSeparator); + + for (String entry : classpathEntries) { + File file = new File(entry); + try { + classesInPath.addAll(getClasses(file)); + } catch (IOException ignore) { + /* Ignore */ + } } } - return classes; + return classesInPath; } public static String getClassName(File file) throws IOException { - String className = null; try ( FileInputStream fis = new FileInputStream(file); DataInputStream dis = new DataInputStream(fis) ) { if (isJarFile(file)) { - try (ZipInputStream zip = new ZipInputStream(fis)) { - ZipEntry entry; - while ((entry = zip.getNextEntry()) != null) { - if (!entry.isDirectory() && entry.getName().endsWith(".class")) { - className = extractClassName(zip); - break; - } - } - } + return getClassNameFromJar(fis); } else { - className = extractClassName(dis); + return extractClassName(dis); + } + } + } + + private static String getClassNameFromJar(FileInputStream fis) throws IOException { + try (ZipInputStream zip = ZipSecurity.createSecureInputStream(new ZipInputStream(fis))) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + if (!entry.isDirectory() && entry.getName().endsWith(".class")) { + return extractClassName(zip); + } } } - return className; + return null; } private static List getClasses(File file) throws IOException { List classFiles = new ArrayList<>(); if (file.isDirectory()) { classFiles.addAll(getClassesFromDirectory(file)); - } else if (isJarFile(file)) { + } else if (isJarFile(file) && !loadedParentJar) { + loadedParentJar = true; classFiles.addAll(getClassesFromJar(file)); } else if (file.getName().endsWith(".class")) { classFiles.add(file); @@ -80,7 +92,7 @@ private static List getClassesFromJar(File jarFile) throws IOException { List classFiles = new ArrayList<>(); try ( FileInputStream fis = new FileInputStream(jarFile); - ZipInputStream zip = new ZipInputStream(fis) + ZipInputStream zip = ZipSecurity.createSecureInputStream(new ZipInputStream(fis)) ) { ZipEntry entry; while ((entry = zip.getNextEntry()) != null) { @@ -109,7 +121,7 @@ private static File extractClassFileFromJar( ZipInputStream zip, String entryName ) throws IOException { - File tempFile = File.createTempFile(entryName.replace('/', '_'), ".class"); + File tempFile = safeTempFile(entryName).toFile(); tempFile.deleteOnExit(); try (FileOutputStream fos = new FileOutputStream(tempFile)) { byte[] buffer = new byte[1024]; @@ -120,4 +132,10 @@ private static File extractClassFileFromJar( } return tempFile; } + + private static Path safeTempFile(String entryName) throws IOException { + String sanitizedEntryName = entryName.replace('/', '_'); + Path secureTempDir = Paths.get(System.getProperty("java.io.tmpdir")); + return Files.createTempFile(secureTempDir, sanitizedEntryName, ".class"); + } } diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/processor/SlashCommandClassMethod.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/SlashCommandClassMethod.java new file mode 100644 index 00000000..02a8cb5f --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/SlashCommandClassMethod.java @@ -0,0 +1,5 @@ +package com.javadiscord.jdi.internal.processor; + +import java.lang.reflect.Method; + +public record SlashCommandClassMethod(Class clazz, Method method) {} diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/processor/ZipSecurity.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/ZipSecurity.java new file mode 100644 index 00000000..552941b1 --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/ZipSecurity.java @@ -0,0 +1,68 @@ +package com.javadiscord.jdi.internal.processor; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ZipSecurity { + + public static ZipInputStream createSecureInputStream(InputStream stream) { + return new SecureZipInputStream(stream); + } + + private static class SecureZipInputStream extends ZipInputStream { + + public SecureZipInputStream(InputStream in) { + super(in); + } + + @Override + public ZipEntry getNextEntry() throws IOException { + ZipEntry entry = super.getNextEntry(); + if (entry == null) { + return null; + } + String entryName = entry.getName(); + if (!entryName.trim().isEmpty()) { + if (isAbsolutePath(entryName)) { + throw new SecurityException( + "Encountered zip file with absolute path: " + entryName + ); + } + if (containsPathTraversal(entryName)) { + throw new SecurityException( + "Path contains traversal to sensitive locations: " + entryName + ); + } + } + return entry; + } + + private boolean containsPathTraversal(String entryName) { + if (entryName.contains("../") || entryName.contains("..\\")) { + try { + if (isPathOutsideCurrentDirectory(entryName)) { + return true; + } + } catch (IOException ignore) { + /* Ignore */ + } + } + return false; + } + + private boolean isPathOutsideCurrentDirectory(String entryName) throws IOException { + File currentDirectory = new File("").getCanonicalFile(); + File untrustedFile = new File(currentDirectory, entryName); + Path untrustedPath = untrustedFile.getCanonicalFile().toPath(); + return !untrustedPath.startsWith(currentDirectory.toPath()); + } + + private boolean isAbsolutePath(String entryName) { + return entryName.startsWith("/"); + } + } +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/ComponentLoader.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/ComponentLoader.java new file mode 100644 index 00000000..2ad035c3 --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/ComponentLoader.java @@ -0,0 +1,113 @@ +package com.javadiscord.jdi.internal.processor.loader; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.javadiscord.jdi.core.annotations.Component; +import com.javadiscord.jdi.core.annotations.Inject; +import com.javadiscord.jdi.internal.exceptions.ComponentInjectionException; +import com.javadiscord.jdi.internal.processor.ClassFileUtil; +import com.javadiscord.jdi.internal.processor.validator.ComponentValidator; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ComponentLoader { + private static final Logger LOGGER = LogManager.getLogger(ComponentLoader.class); + private static final Map, Object> COMPONENTS = new HashMap<>(); + private final ComponentValidator componentValidator = new ComponentValidator(); + + public void loadComponents() { + List classes = ClassFileUtil.getClassesInClassPath(); + for (File classFile : classes) { + processClassFile(classFile); + } + } + + private void processClassFile(File classFile) { + try { + Class clazz = Class.forName(ClassFileUtil.getClassName(classFile)); + if (componentValidator.validate(clazz)) { + processClassMethods(clazz); + } else { + LOGGER.error("{} failed validation", clazz.getName()); + } + } catch (Exception ignore) { + // Ignore + } + } + + private void processClassMethods(Class clazz) { + for (Method method : clazz.getMethods()) { + if (method.isAnnotationPresent(Component.class)) { + registerComponent(method); + } + } + } + + private void registerComponent( + Method method + ) { + Class returnType = method.getReturnType(); + if (!COMPONENTS.containsKey(returnType)) { + try { + COMPONENTS.put(returnType, method.invoke(null)); + } catch (IllegalAccessException | InvocationTargetException e) { + LOGGER.error("Failed to register component {}", method.getName(), e); + return; + } + LOGGER.info("Loaded component {}", returnType.getName()); + } else { + LOGGER.error("Component {} already loaded", returnType.getName()); + } + } + + public static void injectComponents(Object component) { + try { + injectFields(component); + } catch (Exception e) { + LOGGER.error("Failed to inject components into {}", component.getClass().getName(), e); + } + } + + private static void injectFields(Object component) { + Class clazz = component.getClass(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Inject.class)) { + injectField(component, field); + } + } + } + + private static void injectField(Object component, Field field) { + Class fieldType = field.getType(); + if (COMPONENTS.containsKey(fieldType)) { + injectDependency(component, field, COMPONENTS.get(fieldType)); + } else { + LOGGER.error("No object {} was found in field {}", fieldType, field.getName()); + } + } + + private static void injectDependency(Object component, Field field, Object dependency) { + if (dependency != null) { + try { + field.setAccessible(true); + field.set(component, dependency); + LOGGER.info( + "Injected component {} into {}", dependency.getClass().getName(), + field.getType() + ); + } catch (IllegalAccessException e) { + throw new ComponentInjectionException( + "Failed to inject dependency into field: " + field.getName() + ", " + + e.getMessage() + ); + } + } + } +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/processor/ListenerLoader.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/ListenerLoader.java similarity index 78% rename from annotations/src/main/java/com/javadiscord/jdi/core/processor/ListenerLoader.java rename to annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/ListenerLoader.java index b7d9a79d..2f0a5444 100644 --- a/annotations/src/main/java/com/javadiscord/jdi/core/processor/ListenerLoader.java +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/ListenerLoader.java @@ -1,10 +1,13 @@ -package com.javadiscord.jdi.core.processor; +package com.javadiscord.jdi.internal.processor.loader; import java.io.File; import java.lang.reflect.Constructor; import java.util.List; import com.javadiscord.jdi.core.annotations.EventListener; +import com.javadiscord.jdi.internal.exceptions.NoZeroArgConstructorException; +import com.javadiscord.jdi.internal.processor.ClassFileUtil; +import com.javadiscord.jdi.internal.processor.validator.EventListenerValidator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,7 +46,9 @@ public void loadListeners() { private void registerListener(Class clazz) { try { - eventListeners.add(getZeroArgConstructor(clazz).newInstance()); + Object instance = getZeroArgConstructor(clazz).newInstance(); + ComponentLoader.injectComponents(instance); + eventListeners.add(instance); LOGGER.info("Registered listener {}", clazz.getName()); } catch (Exception e) { LOGGER.error("Failed to create {} instance", clazz.getName(), e); @@ -61,6 +66,8 @@ public static Constructor getZeroArgConstructor(Class clazz) { return constructor; } } - throw new RuntimeException("No zero arg constructor found for " + clazz.getName()); + throw new NoZeroArgConstructorException( + "No zero arg constructor found for " + clazz.getName() + ); } } diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/SlashCommandLoader.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/SlashCommandLoader.java new file mode 100644 index 00000000..128bd85c --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/loader/SlashCommandLoader.java @@ -0,0 +1,89 @@ +package com.javadiscord.jdi.internal.processor.loader; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import com.javadiscord.jdi.core.annotations.SlashCommand; +import com.javadiscord.jdi.internal.exceptions.ValidationException; +import com.javadiscord.jdi.internal.processor.ClassFileUtil; +import com.javadiscord.jdi.internal.processor.SlashCommandClassMethod; +import com.javadiscord.jdi.internal.processor.validator.SlashCommandValidator; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SlashCommandLoader { + private static final Logger LOGGER = LogManager.getLogger(SlashCommandLoader.class); + private final Map interactionListeners; + private final SlashCommandValidator validator = new SlashCommandValidator(); + + public SlashCommandLoader(Map interactionListeners) { + this.interactionListeners = interactionListeners; + loadInteractionListeners(); + } + + private void loadInteractionListeners() { + List classes = ClassFileUtil.getClassesInClassPath(); + for (File classFile : classes) { + try { + Class clazz = Class.forName(ClassFileUtil.getClassName(classFile)); + + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.getAnnotation(SlashCommand.class) != null) { + if (validator.validate(method) && hasZeroArgsConstructor(clazz)) { + registerListener( + clazz, method, method.getAnnotation(SlashCommand.class).name() + ); + } else { + throw new ValidationException(method.getName() + " failed validation"); + } + } + } + } catch (Exception ignore) { + /* Ignore */ + } + } + } + + private void registerListener(Class clazz, Method method, String name) { + try { + if (interactionListeners.containsKey(name)) { + LOGGER.error( + "Failed to register command {} from {} as that name already exists in {}", + name, + clazz.getName(), + interactionListeners.get(name).getClass().getName() + ); + return; + } + interactionListeners.put(name, new SlashCommandClassMethod(clazz, method)); + LOGGER.info("Found slash command handler {}", clazz.getName()); + } catch (Exception e) { + LOGGER.error("Failed to create {} instance", clazz.getName(), e); + } + } + + public SlashCommandClassMethod getSlashCommandClassMethod(String name) { + return interactionListeners.get(name); + } + + private boolean hasZeroArgsConstructor(Class clazz) { + Constructor[] constructors = clazz.getConstructors(); + for (Constructor constructor : constructors) { + if (constructor.getParameterCount() == 0) { + return true; + } + } + LOGGER.error("{} does not have a 0 arg constructor", clazz.getName()); + return false; + } + + public void injectComponents(Object object) { + ComponentLoader.injectComponents(object); + } + +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/ComponentValidator.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/ComponentValidator.java new file mode 100644 index 00000000..f8eefbc1 --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/ComponentValidator.java @@ -0,0 +1,35 @@ +package com.javadiscord.jdi.internal.processor.validator; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import com.javadiscord.jdi.core.annotations.Component; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ComponentValidator { + private static final Logger LOGGER = LogManager.getLogger(ComponentValidator.class); + + public boolean validate(Class clazz) { + return validateMethods(clazz); + } + + private boolean validateMethods(Class clazz) { + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(Component.class)) { + if (method.getParameterCount() != 0) { + LOGGER.error("Methods annotated with @Component requires 0 parameters"); + return false; + } + if (!Modifier.isStatic(method.getModifiers())) { + LOGGER.error("Methods annotated with @Component must be static"); + return false; + } + } + } + return true; + } + +} diff --git a/annotations/src/main/java/com/javadiscord/jdi/core/processor/EventListenerValidator.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/EventListenerValidator.java similarity index 87% rename from annotations/src/main/java/com/javadiscord/jdi/core/processor/EventListenerValidator.java rename to annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/EventListenerValidator.java index 06e66dc4..1cdc3766 100644 --- a/annotations/src/main/java/com/javadiscord/jdi/core/processor/EventListenerValidator.java +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/EventListenerValidator.java @@ -1,4 +1,4 @@ -package com.javadiscord.jdi.core.processor; +package com.javadiscord.jdi.internal.processor.validator; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; @@ -308,7 +308,7 @@ public class EventListenerValidator { EXPECTED_PARAM_TYPES_MAP.put( ThreadCreate.class, new String[] { - "com.javadiscord.jdi.core.gateway.handlers.events.codec.models.channel.Thread", + "com.javadiscord.jdi.internal.gateway.handlers.events.codec.models.channel.Thread", "com.javadiscord.jdi.core.Discord", "com.javadiscord.jdi.core.Guild" } @@ -405,37 +405,56 @@ public boolean hasZeroArgsConstructor(Class clazz) { private boolean validateMethods(Class clazz) { Method[] methods = clazz.getMethods(); for (Method method : methods) { - for (Map.Entry, String[]> entry : EXPECTED_PARAM_TYPES_MAP - .entrySet()) { - Class annotationClass = entry.getKey(); - if (method.isAnnotationPresent(annotationClass)) { - String[] expectedParamTypes = entry.getValue(); - if (method.getParameterCount() > 0) { - Class[] paramTypes = method.getParameterTypes(); - for (Class type : paramTypes) { - boolean isExpectedType = false; - for (String expectedType : expectedParamTypes) { - if (type.getName().equals(expectedType)) { - isExpectedType = true; - break; - } - } - if (!isExpectedType) { - LOGGER.error("Unexpected parameter found: {}", type.getName()); - return false; - } else { - LOGGER.trace("Loaded {}", clazz.getName()); - } - } - } else if (method.getParameterCount() != 0) { - LOGGER.error( - "{} does not have the expected parameter types", method.getName() - ); - return false; - } - } + if (!validateMethodAnnotations(method)) { + return false; + } + } + return true; + } + + private boolean validateMethodAnnotations(Method method) { + for (Map.Entry, String[]> entry : EXPECTED_PARAM_TYPES_MAP + .entrySet()) { + Class annotationClass = entry.getKey(); + if ( + method.isAnnotationPresent(annotationClass) + && !validateMethodParameters(method, entry.getValue()) + ) { + return false; } } return true; } + + private boolean validateMethodParameters(Method method, String[] expectedParamTypes) { + if (method.getParameterCount() > 0) { + return checkParameterTypes(method, expectedParamTypes); + } else if (method.getParameterCount() != 0) { + LOGGER.error("{} does not have the expected parameter types", method.getName()); + return false; + } + return true; + } + + private boolean checkParameterTypes(Method method, String[] expectedParamTypes) { + Class[] paramTypes = method.getParameterTypes(); + for (Class type : paramTypes) { + if (!isExpectedType(type, expectedParamTypes)) { + LOGGER.error("Unexpected parameter found: {}", type.getName()); + return false; + } else { + LOGGER.trace("Loaded {}", method.getDeclaringClass().getName()); + } + } + return true; + } + + private boolean isExpectedType(Class type, String[] expectedParamTypes) { + for (String expectedType : expectedParamTypes) { + if (type.getName().equals(expectedType)) { + return true; + } + } + return false; + } } diff --git a/annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/SlashCommandValidator.java b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/SlashCommandValidator.java new file mode 100644 index 00000000..38bc915c --- /dev/null +++ b/annotations/src/main/java/com/javadiscord/jdi/internal/processor/validator/SlashCommandValidator.java @@ -0,0 +1,70 @@ +package com.javadiscord.jdi.internal.processor.validator; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import com.javadiscord.jdi.core.annotations.SlashCommand; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SlashCommandValidator { + private static final Logger LOGGER = LogManager.getLogger(SlashCommandValidator.class); + private static final Map, String[]> EXPECTED_PARAM_TYPES_MAP = + new HashMap<>(); + + static { + EXPECTED_PARAM_TYPES_MAP.put( + SlashCommand.class, + new String[] { + "com.javadiscord.jdi.core.models.guild.Interaction", + "com.javadiscord.jdi.core.Discord", + "com.javadiscord.jdi.core.Guild", + "com.javadiscord.jdi.core.interaction.SlashCommandEvent" + } + ); + } + + public boolean validate(Method method) { + for (Map.Entry, String[]> entry : EXPECTED_PARAM_TYPES_MAP + .entrySet()) { + Class annotationClass = entry.getKey(); + if ( + method.isAnnotationPresent(annotationClass) + && !validateMethodParameters(method, entry.getValue()) + ) { + return false; + } + } + return true; + } + + private boolean validateMethodParameters(Method method, String[] expectedParamTypes) { + if (method.getParameterCount() > 0) { + return checkParameterTypes(method, expectedParamTypes); + } + return true; + } + + private boolean checkParameterTypes(Method method, String[] expectedParamTypes) { + Class[] paramTypes = method.getParameterTypes(); + for (Class type : paramTypes) { + if (!isExpectedType(type, expectedParamTypes)) { + LOGGER.error("Unexpected parameter found: {}", type.getName()); + return false; + } + } + return true; + } + + private boolean isExpectedType(Class type, String[] expectedParamTypes) { + for (String expectedType : expectedParamTypes) { + if (type.getName().equals(expectedType)) { + return true; + } + } + return false; + } +} diff --git a/annotations/src/test/unit/com/javadiscord/jdi/core/processor/ClassFileUtilTest.java b/annotations/src/test/unit/com/javadiscord/jdi/core/processor/ClassFileUtilTest.java index 989b0587..d6303580 100644 --- a/annotations/src/test/unit/com/javadiscord/jdi/core/processor/ClassFileUtilTest.java +++ b/annotations/src/test/unit/com/javadiscord/jdi/core/processor/ClassFileUtilTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; +import com.javadiscord.jdi.internal.processor.ClassFileUtil; import org.junit.jupiter.api.Test; import java.io.File; diff --git a/annotations/src/test/unit/com/javadiscord/jdi/core/processor/EventListenerValidatorTest.java b/annotations/src/test/unit/com/javadiscord/jdi/core/processor/EventListenerValidatorTest.java index 49b03d65..4eeb6cfb 100644 --- a/annotations/src/test/unit/com/javadiscord/jdi/core/processor/EventListenerValidatorTest.java +++ b/annotations/src/test/unit/com/javadiscord/jdi/core/processor/EventListenerValidatorTest.java @@ -10,6 +10,7 @@ import com.javadiscord.jdi.core.models.channel.Channel; import com.javadiscord.jdi.core.models.message.Message; +import com.javadiscord.jdi.internal.processor.validator.EventListenerValidator; import org.junit.jupiter.api.Test; class EventListenerValidatorTest { diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/AutoModerationRequest.java b/api/src/main/java/com/javadiscord/jdi/core/api/AutoModerationRequest.java index b6d30fd8..f47fdc79 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/AutoModerationRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/AutoModerationRequest.java @@ -2,9 +2,9 @@ import java.util.List; +import com.javadiscord.jdi.core.api.builders.CreateAutoModerationRuleBuilder; import com.javadiscord.jdi.core.api.builders.ModifyAutoModerationRuleBuilder; import com.javadiscord.jdi.core.models.auto_moderation.AutoModerationRule; -import com.javadiscord.jdi.core.request.builders.CreateAutoModerationRuleBuilder; import com.javadiscord.jdi.internal.api.auto_moderation.*; public class AutoModerationRequest { diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/ChannelRequest.java b/api/src/main/java/com/javadiscord/jdi/core/api/ChannelRequest.java index d3e51c6c..815fa4fd 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/ChannelRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/ChannelRequest.java @@ -9,6 +9,7 @@ import com.javadiscord.jdi.core.models.emoji.Emoji; import com.javadiscord.jdi.core.models.invite.Invite; import com.javadiscord.jdi.core.models.message.Message; +import com.javadiscord.jdi.core.models.message.embed.Embed; import com.javadiscord.jdi.core.models.user.User; import com.javadiscord.jdi.internal.api.channel.*; @@ -49,6 +50,18 @@ public AsyncResponse createMessage(CreateMessageBuilder builder) { return responseParser.callAndParse(Message.class, builder.build()); } + public AsyncResponse sendMessage(long channelId, String message) { + return responseParser.callAndParse( + Message.class, new CreateMessageBuilder(channelId).content(message).build() + ); + } + + public AsyncResponse sendEmbed(long channelId, Embed... embeds) { + return responseParser.callAndParse( + Message.class, new CreateMessageBuilder(channelId).embeds(embeds).build() + ); + } + public AsyncResponse createReaction( long channelId, long messageId, diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/DiscordResponseParser.java b/api/src/main/java/com/javadiscord/jdi/core/api/DiscordResponseParser.java index 7f188669..ed4fa7ae 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/DiscordResponseParser.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/DiscordResponseParser.java @@ -7,16 +7,23 @@ import com.javadiscord.jdi.internal.api.DiscordRequestDispatcher; import com.javadiscord.jdi.internal.api.DiscordResponse; import com.javadiscord.jdi.internal.api.DiscordResponseFuture; +import com.javadiscord.jdi.internal.cache.Cache; +import com.javadiscord.jdi.internal.utils.CacheUpdater; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class DiscordResponseParser { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); private final DiscordRequestDispatcher dispatcher; + private final CacheUpdater cacheUpdater; - public DiscordResponseParser(DiscordRequestDispatcher dispatcher) { + public DiscordResponseParser(DiscordRequestDispatcher dispatcher, Cache cache) { this.dispatcher = dispatcher; + this.cacheUpdater = new CacheUpdater(cache); } public AsyncResponse> callAndParseList(Class clazz, DiscordRequest request) { @@ -28,6 +35,7 @@ public AsyncResponse> callAndParseList(Class clazz, DiscordReques try { List resultList = parseResponseFromList(clazz, response.body()); asyncResponse.setResult(resultList); + cacheUpdater.updateCache(resultList); } catch (Exception e) { asyncResponse.setException(e); } @@ -49,6 +57,7 @@ public AsyncResponse> callAndParseMap(String key, DiscordRequest req try { List resultList = parseResponseFromMap(key, response.body()); asyncResponse.setResult(resultList); + cacheUpdater.updateCache(resultList); } catch (Exception e) { asyncResponse.setException(e); } @@ -105,6 +114,7 @@ private void success( result = OBJECT_MAPPER.readValue(response.body(), type); } asyncResponse.setResult(result); + cacheUpdater.updateCache(result); } catch (JsonProcessingException e) { asyncResponse.setException(e); } diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/GuildRequest.java b/api/src/main/java/com/javadiscord/jdi/core/api/GuildRequest.java index 51f11bb3..b442dbee 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/GuildRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/GuildRequest.java @@ -49,8 +49,8 @@ public AsyncResponse createGuildChannel(CreateGuildChannelBuilder build return responseParser.callAndParse(Channel.class, builder.guildId(guildId).build()); } - public AsyncResponse createGuild(CreateGuildBuilder builder) { - return responseParser.callAndParse(Guild.class, builder.build()); + public AsyncResponse createGuild(CreateGuildBuilder builder) { + return responseParser.callAndParse(GuildModel.class, builder.build()); } public AsyncResponse createGuildRole(CreateGuildRoleBuilder builder) { @@ -63,8 +63,8 @@ Void.class, new DeleteGuildIntegrationRequest(guildId, integrationId) ); } - public AsyncResponse deleteGuild() { - return responseParser.callAndParse(Guild.class, new DeleteGuildRequest(guildId)); + public AsyncResponse deleteGuild() { + return responseParser.callAndParse(GuildModel.class, new DeleteGuildRequest(guildId)); } public AsyncResponse deleteGuildRole(long roleId) { @@ -105,16 +105,16 @@ Onboarding.class, new GetGuildOnboardingRequest(guildId) ); } - public AsyncResponse guildPreview() { - return responseParser.callAndParse(Guild.class, new GetGuildPreviewRequest(guildId)); + public AsyncResponse guildPreview() { + return responseParser.callAndParse(GuildModel.class, new GetGuildPreviewRequest(guildId)); } public AsyncResponse guildPruneCount(GetGuildPruneCountBuilder builder) { return responseParser.callAndParse(PruneCount.class, builder.guildId(guildId).build()); } - public AsyncResponse guild(GetGuildBuilder builder) { - return responseParser.callAndParse(Guild.class, builder.guildId(guildId).build()); + public AsyncResponse guild(GetGuildBuilder builder) { + return responseParser.callAndParse(GuildModel.class, builder.guildId(guildId).build()); } public AsyncResponse> guildRoles() { @@ -212,8 +212,8 @@ public AsyncResponse modifyGuildOnboarding( ); } - public AsyncResponse modifyGuild(ModifyGuildBuilder builder) { - return responseParser.callAndParse(Guild.class, builder.guildId(guildId).build()); + public AsyncResponse modifyGuild(ModifyGuildBuilder builder) { + return responseParser.callAndParse(GuildModel.class, builder.guildId(guildId).build()); } public AsyncResponse> modifyGuildRolePositions( @@ -242,8 +242,9 @@ public AsyncResponse modifyUserVoiceState(ModifyUserVoiceStateBuilde return responseParser.callAndParse(VoiceState.class, builder.guildId(guildId).build()); } - public AsyncResponse removeGuildBan(long userId) { - return responseParser.callAndParse(Guild.class, new RemoveGuildBanRequest(guildId, userId)); + public AsyncResponse removeGuildBan(long userId) { + return responseParser + .callAndParse(GuildModel.class, new RemoveGuildBanRequest(guildId, userId)); } public AsyncResponse removeGuildMemberRole(long userId, long roleId) { diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/GuildTemplateRequest.java b/api/src/main/java/com/javadiscord/jdi/core/api/GuildTemplateRequest.java index c439b747..2faf3580 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/GuildTemplateRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/GuildTemplateRequest.java @@ -5,7 +5,7 @@ import com.javadiscord.jdi.core.api.builders.CreateGuildFromTemplateBuilder; import com.javadiscord.jdi.core.api.builders.CreateGuildTemplateBuilder; import com.javadiscord.jdi.core.api.builders.ModifyGuildTemplateBuilder; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.core.models.guild_template.GuildTemplate; import com.javadiscord.jdi.internal.api.guild_template.*; @@ -18,9 +18,11 @@ public GuildTemplateRequest(DiscordResponseParser responseParser, long guildId) this.guildId = guildId; } - public AsyncResponse createGuildFromTemplate(CreateGuildFromTemplateBuilder builder) { + public AsyncResponse createGuildFromTemplate( + CreateGuildFromTemplateBuilder builder + ) { return responseParser.callAndParse( - Guild.class, builder.build() + GuildModel.class, builder.build() ); } diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/InteractionRequest.java b/api/src/main/java/com/javadiscord/jdi/core/api/InteractionRequest.java new file mode 100644 index 00000000..fe023719 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/core/api/InteractionRequest.java @@ -0,0 +1,40 @@ +package com.javadiscord.jdi.core.api; + +import com.javadiscord.jdi.core.api.builders.command.CommandBuilder; +import com.javadiscord.jdi.core.api.builders.command.CommandOption; + +public class InteractionRequest { + private final DiscordResponseParser responseParser; + private final long guildId; + private final long applicationId; + + public InteractionRequest( + DiscordResponseParser responseParser, long guildId, long applicationId + ) { + this.responseParser = responseParser; + this.guildId = guildId; + this.applicationId = applicationId; + } + + public AsyncResponse createInteraction(CommandBuilder builder) { + return responseParser + .callAndParse(Void.class, builder.applicationId(applicationId).build()); + } + + public AsyncResponse createSlashCommand( + String name, + String description, + CommandOption... options + ) { + CommandBuilder builder = + new CommandBuilder( + name, + description + ); + for (CommandOption option : options) { + builder.addOption(option); + } + builder.applicationId(applicationId); + return createInteraction(builder); + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/UserRequest.java b/api/src/main/java/com/javadiscord/jdi/core/api/UserRequest.java index 8bcba63d..39406fb1 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/UserRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/UserRequest.java @@ -7,7 +7,7 @@ import com.javadiscord.jdi.core.api.builders.ModifyCurrentUserBuilder; import com.javadiscord.jdi.core.api.builders.UpdateCurrentUserApplicationRoleConnectionBuilder; import com.javadiscord.jdi.core.models.channel.Channel; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.core.models.user.Connection; import com.javadiscord.jdi.core.models.user.Member; import com.javadiscord.jdi.core.models.user.User; @@ -54,8 +54,10 @@ Member.class, new GetCurrentUserGuildMemberRequest(guildId) ); } - public AsyncResponse> getCurrentUserGuilds(GetCurrentUserGuildsBuilder builder) { - return responseParser.callAndParseList(Guild.class, builder.build()); + public AsyncResponse> getCurrentUserGuilds( + GetCurrentUserGuildsBuilder builder + ) { + return responseParser.callAndParseList(GuildModel.class, builder.build()); } public AsyncResponse getCurrentUser() { diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/CreateAutoModerationRuleBuilder.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/CreateAutoModerationRuleBuilder.java index 9d64000e..0258c855 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/builders/CreateAutoModerationRuleBuilder.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/CreateAutoModerationRuleBuilder.java @@ -1,4 +1,4 @@ -package com.javadiscord.jdi.core.request.builders; +package com.javadiscord.jdi.core.api.builders; import java.util.List; import java.util.Optional; diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/CreateMessageBuilder.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/CreateMessageBuilder.java index dedd41a0..f82447cf 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/builders/CreateMessageBuilder.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/CreateMessageBuilder.java @@ -56,8 +56,8 @@ public CreateMessageBuilder tts(boolean tts) { return this; } - public CreateMessageBuilder embeds(List embeds) { - this.embeds = Optional.of(embeds); + public CreateMessageBuilder embeds(Embed... embeds) { + this.embeds = Optional.of(List.of(embeds)); return this; } diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/ListPrivateArchivedThreadsBuilder.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/ListPrivateArchivedThreadsBuilder.java index db3eb713..1d05a182 100644 --- a/api/src/main/java/com/javadiscord/jdi/core/api/builders/ListPrivateArchivedThreadsBuilder.java +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/ListPrivateArchivedThreadsBuilder.java @@ -22,7 +22,7 @@ public ListPrivateArchivedThreadsBuilder before(OffsetDateTime before) { } public ListPrivateArchivedThreadsBuilder limit(int limit) { - this.limit = Optional.ofNullable(limit); + this.limit = Optional.of(limit); return this; } diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackMessage.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackMessage.java new file mode 100644 index 00000000..f7cfb49f --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackMessage.java @@ -0,0 +1,108 @@ +package com.javadiscord.jdi.core.api.builders.command; + +import java.util.List; + +import com.javadiscord.jdi.core.models.channel.AllowedMentions; +import com.javadiscord.jdi.core.models.channel.Attachment; +import com.javadiscord.jdi.core.models.message.Component; +import com.javadiscord.jdi.core.models.message.embed.Embed; +import com.javadiscord.jdi.core.models.poll.Poll; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CallbackMessage { + @JsonProperty("tts") + private boolean tts; + + @JsonProperty("content") + private String content; + + @JsonProperty("embeds") + private List embeds; + + @JsonProperty("allowed_mentions") + private AllowedMentions allowedMentions; + + @JsonProperty("flags") + private int flags; + + @JsonProperty("components") + private List components; + + @JsonProperty("attachments") + private List attachments; + + @JsonProperty("poll") + private Poll poll; + + public boolean isTts() { + return tts; + } + + public void setTts(boolean tts) { + this.tts = tts; + } + + public String getContent() { + return content; + } + + public CallbackMessage setContent(String content) { + this.content = content; + return this; + } + + public List getEmbeds() { + return embeds; + } + + public CallbackMessage setEmbeds(List embeds) { + this.embeds = embeds; + return this; + } + + public AllowedMentions getAllowedMentions() { + return allowedMentions; + } + + public CallbackMessage setAllowedMentions(AllowedMentions allowedMentions) { + this.allowedMentions = allowedMentions; + return this; + } + + public int getFlags() { + return flags; + } + + public CallbackMessage setFlags(int flags) { + this.flags = flags; + return this; + } + + public List getComponents() { + return components; + } + + public CallbackMessage setComponents(List components) { + this.components = components; + return this; + } + + public List getAttachments() { + return attachments; + } + + public CallbackMessage setAttachments(List attachments) { + this.attachments = attachments; + return this; + } + + public Poll getPoll() { + return poll; + } + + public CallbackMessage setPoll(Poll poll) { + this.poll = poll; + return this; + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackMessageBuilder.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackMessageBuilder.java new file mode 100644 index 00000000..5dae2837 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackMessageBuilder.java @@ -0,0 +1,41 @@ +package com.javadiscord.jdi.core.api.builders.command; + +import java.util.Optional; + +import com.javadiscord.jdi.internal.api.application_commands.EditCommandRequest; +import com.javadiscord.jdi.internal.api.application_commands.RespondCommandRequest; + +public class CallbackMessageBuilder { + private final CallbackResponseType type; + private final long interactionId; + private final String interactionToken; + private Optional message; + private long applicationId; + + public CallbackMessageBuilder( + CallbackResponseType type, long interactionId, String interactionToken + ) { + this.type = type; + this.interactionId = interactionId; + this.interactionToken = interactionToken; + this.message = Optional.empty(); + } + + public CallbackMessageBuilder message(CallbackMessage message) { + this.message = Optional.of(message); + return this; + } + + public CallbackMessageBuilder applicationId(long applicationId) { + this.applicationId = applicationId; + return this; + } + + public RespondCommandRequest build() { + return new RespondCommandRequest(type, message, interactionId, interactionToken); + } + + public EditCommandRequest buildEdit() { + return new EditCommandRequest(type, message, applicationId, interactionToken); + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackResponseType.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackResponseType.java new file mode 100644 index 00000000..429f8264 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CallbackResponseType.java @@ -0,0 +1,22 @@ +package com.javadiscord.jdi.core.api.builders.command; + +public enum CallbackResponseType { + PONG(1), + CHANNEL_MESSAGE_WITH_SOURCE(4), + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE(5), + DEFERRED_UPDATE_MESSAGE(6), + UPDATE_MESSAGE(7), + APPLICATION_COMMAND_AUTOCOMPLETE_RESULT(8), + MODAL(9), + PREMIUM_REQUIRED(10); + + private final int value; + + CallbackResponseType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandBuilder.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandBuilder.java new file mode 100644 index 00000000..d381464c --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandBuilder.java @@ -0,0 +1,61 @@ +package com.javadiscord.jdi.core.api.builders.command; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.javadiscord.jdi.internal.api.application_commands.CreateCommandRequest; + +public class CommandBuilder { + private final String name; + private final CommandOptionType type; + private final String description; + private final List options; + + private Optional global; + + private long applicationId; + private long guildId; + + public CommandBuilder( + String name, String description + ) { + this.name = name; + this.type = CommandOptionType.SUB_COMMAND; + this.description = description; + this.options = new ArrayList<>(); + this.global = Optional.empty(); + } + + public CommandBuilder addOption(CommandOption option) { + options.add(option); + return this; + } + + public CommandBuilder global(boolean global) { + this.global = Optional.of(global); + return this; + } + + public CommandBuilder guildId(long guildId) { + this.guildId = guildId; + return this; + } + + public CommandBuilder applicationId(long applicationId) { + this.applicationId = applicationId; + return this; + } + + public CreateCommandRequest build() { + return new CreateCommandRequest( + name, + type, + description, + options, + global, + guildId, + applicationId + ); + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOption.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOption.java new file mode 100644 index 00000000..42aa7f0d --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOption.java @@ -0,0 +1,61 @@ +package com.javadiscord.jdi.core.api.builders.command; + +import java.util.ArrayList; +import java.util.List; + +public class CommandOption { + private final String name; + private final String description; + private final CommandOptionType type; + private boolean required; + private final List choices; + + public CommandOption(String name, String description, CommandOptionType type) { + this(name, description, type, true); + } + + public CommandOption( + String name, String description, CommandOptionType type, boolean required + ) { + this.name = name; + this.description = description; + this.type = type; + this.required = required; + this.choices = new ArrayList<>(); + } + + public CommandOption addChoice(String name, String value) { + choices.add(new CommandOptionChoice(name, value)); + return this; + } + + public CommandOption addChoice(List commandOptionChoices) { + choices.addAll(commandOptionChoices); + return this; + } + + public CommandOption setRequired(boolean required) { + this.required = required; + return this; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public CommandOptionType getType() { + return type; + } + + public boolean isRequired() { + return required; + } + + public List getChoices() { + return choices; + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOptionChoice.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOptionChoice.java new file mode 100644 index 00000000..1358a324 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOptionChoice.java @@ -0,0 +1,3 @@ +package com.javadiscord.jdi.core.api.builders.command; + +public record CommandOptionChoice(String name, String value) {} diff --git a/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOptionType.java b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOptionType.java new file mode 100644 index 00000000..81ce8eb1 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/core/api/builders/command/CommandOptionType.java @@ -0,0 +1,38 @@ +package com.javadiscord.jdi.core.api.builders.command; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum CommandOptionType { + SUB_COMMAND(1), + SUB_COMMAND_GROUP(2), + STRING(3), + INTEGER(4), + BOOLEAN(5), + USER(6), + CHANNEL(7), + ROLE(8), + MENTIONABLE(9), + NUMBER(10), + ATTACHMENT(11), + ; + + private final int value; + + CommandOptionType(int value) { + this.value = value; + } + + @JsonValue + public int getValue() { + return value; + } + + public static CommandOptionType fromName(String name) { + for (CommandOptionType type : CommandOptionType.values()) { + if (type.name().equalsIgnoreCase(name)) { + return type; + } + } + throw new IllegalArgumentException("Unknown command option type: " + name); + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/DiscordRequestDispatcher.java b/api/src/main/java/com/javadiscord/jdi/internal/api/DiscordRequestDispatcher.java index 3677b791..e31d8df9 100644 --- a/api/src/main/java/com/javadiscord/jdi/internal/api/DiscordRequestDispatcher.java +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/DiscordRequestDispatcher.java @@ -2,11 +2,13 @@ import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; @@ -24,6 +26,8 @@ public class DiscordRequestDispatcher implements Runnable { private final BlockingQueue queue; private final String botToken; private final AtomicBoolean running = new AtomicBoolean(false); + private final RateLimit rateLimit = new RateLimit(); + private int numberOfRequestsSent; private long timeSinceLastRequest; @@ -48,22 +52,24 @@ public void run() { LOGGER.info("Request dispatcher has started"); while (running.get()) { - long currentTime = System.currentTimeMillis(); - long elapsed = currentTime - timeSinceLastRequest; - - if (elapsed < 1000 && numberOfRequestsSent >= 50) { - try { - Thread.sleep(1000 - elapsed); - } catch (InterruptedException e) { - /* Ignore */ + try { + long currentTime = System.currentTimeMillis(); + long elapsed = currentTime - timeSinceLastRequest; + + if (rateLimit.getRemaining() == 0 && elapsed < rateLimit.getResetAfter()) { + TimeUnit.MILLISECONDS.sleep(rateLimit.getResetAfter() - elapsed); + + } + + if (elapsed < 1000 && numberOfRequestsSent >= 50) { + TimeUnit.MILLISECONDS.sleep(1000 - elapsed); + numberOfRequestsSent = 0; } - numberOfRequestsSent = 0; - } - try { sendRequest(queue.take()); } catch (InterruptedException e) { - /* Ignore */ + LOGGER.warn("Request dispatcher has interrupted"); + Thread.currentThread().interrupt(); } } @@ -119,6 +125,17 @@ private void sendRequest(DiscordRequestBuilder discordRequestBuilder) { HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + HttpHeaders headers = response.headers(); + headers.firstValue("x-ratelimit-bucket").ifPresent(rateLimit::setBucket); + headers.firstValue("x-ratelimit-limit") + .ifPresent(val -> rateLimit.setLimit(Integer.parseInt(val))); + headers.firstValue("x-ratelimit-remaining") + .ifPresent(val -> rateLimit.setRemaining(Integer.parseInt(val))); + headers.firstValue("x-ratelimit-reset") + .ifPresent(val -> rateLimit.setReset(Long.parseLong(val))); + headers.firstValue("x-ratelimit-reset-after") + .ifPresent(val -> rateLimit.setResetAfter(Integer.parseInt(val))); + numberOfRequestsSent++; timeSinceLastRequest = System.currentTimeMillis(); diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/RateLimit.java b/api/src/main/java/com/javadiscord/jdi/internal/api/RateLimit.java new file mode 100644 index 00000000..84294c17 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/RateLimit.java @@ -0,0 +1,58 @@ +package com.javadiscord.jdi.internal.api; + +public class RateLimit { + private String bucket; + private int limit; + private int remaining; + private long reset; + private int resetAfter; + private boolean globalRateLimit; + + public String getBucket() { + return bucket; + } + + public void setBucket(String bucket) { + this.bucket = bucket; + } + + public int getLimit() { + return limit; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + public int getRemaining() { + return remaining; + } + + public void setRemaining(int remaining) { + this.remaining = remaining; + } + + public long getReset() { + return reset; + } + + public void setReset(long reset) { + this.reset = reset; + } + + public int getResetAfter() { + return resetAfter; + } + + public void setResetAfter(int resetAfter) { + this.resetAfter = resetAfter; + } + + public boolean isGlobalRateLimit() { + return globalRateLimit; + } + + public void setGlobalRateLimit(boolean globalRateLimit) { + this.globalRateLimit = globalRateLimit; + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/CreateCommandRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/CreateCommandRequest.java new file mode 100644 index 00000000..29cf3128 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/CreateCommandRequest.java @@ -0,0 +1,48 @@ +package com.javadiscord.jdi.internal.api.application_commands; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import com.javadiscord.jdi.core.api.builders.command.CommandOption; +import com.javadiscord.jdi.core.api.builders.command.CommandOptionType; +import com.javadiscord.jdi.internal.api.DiscordRequest; +import com.javadiscord.jdi.internal.api.DiscordRequestBuilder; + +public record CreateCommandRequest( + String name, + CommandOptionType type, + String description, + List options, + Optional global, + long guildId, + long applicationId +) implements DiscordRequest { + + @Override + public DiscordRequestBuilder create() { + + AtomicReference path = + new AtomicReference<>("/applications/%s/commands".formatted(applicationId)); + + global.ifPresent( + val -> path.set("/applications/%s/guilds/%s/commands".formatted(applicationId, guildId)) + ); + + Map body = new HashMap<>(); + body.put("name", name); + body.put("type", type); + body.put("description", description); + + if (!options.isEmpty()) { + body.put("options", options); + } + + return new DiscordRequestBuilder() + .post() + .body(body) + .path(path.get()); + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/DeleteCommandRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/DeleteCommandRequest.java new file mode 100644 index 00000000..95e52773 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/DeleteCommandRequest.java @@ -0,0 +1,29 @@ +package com.javadiscord.jdi.internal.api.application_commands; + +import com.javadiscord.jdi.internal.api.DiscordRequest; +import com.javadiscord.jdi.internal.api.DiscordRequestBuilder; + +public record DeleteCommandRequest( + long applicationId, + long guildId, + long commandId, + boolean global +) implements DiscordRequest { + + @Override + public DiscordRequestBuilder create() { + String path; + + if (global) { + path = "applications/%s/commands/%s".formatted(applicationId, commandId); + } else { + path = + "applications/%s/guilds/%s/commands/%s" + .formatted(applicationId, guildId, commandId); + } + + return new DiscordRequestBuilder() + .delete() + .path(path); + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/EditCommandRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/EditCommandRequest.java new file mode 100644 index 00000000..718c362f --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/EditCommandRequest.java @@ -0,0 +1,56 @@ +package com.javadiscord.jdi.internal.api.application_commands; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import com.javadiscord.jdi.core.api.builders.command.CallbackMessage; +import com.javadiscord.jdi.core.api.builders.command.CallbackResponseType; +import com.javadiscord.jdi.internal.api.DiscordRequest; +import com.javadiscord.jdi.internal.api.DiscordRequestBuilder; + +public record EditCommandRequest( + CallbackResponseType type, + Optional message, + long applicationId, + String interactionToken +) implements DiscordRequest { + + @Override + public DiscordRequestBuilder create() { + Map body = new HashMap<>(); + body.put("type", type.getValue()); + + message.ifPresent(m -> { + body.put("tts", m.isTts()); + body.put("content", m.getContent()); + + if (m.getEmbeds() != null) { + body.put("embeds", m.getEmbeds()); + } + + if (m.getAllowedMentions() != null) { + body.put("allowed_mentions", m.getAllowedMentions()); + } + + body.put("flags", m.getFlags()); + + if (m.getComponents() != null) { + body.put("components", m.getComponents()); + } + + if (m.getAttachments() != null) { + body.put("attachments", m.getAttachments()); + } + + if (m.getPoll() != null) { + body.put("poll", m.getPoll()); + } + }); + + return new DiscordRequestBuilder() + .patch() + .body(body) + .path("/webhooks/%s/%s/messages/@original".formatted(applicationId, interactionToken)); + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/FetchCommandRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/FetchCommandRequest.java new file mode 100644 index 00000000..fc999afa --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/FetchCommandRequest.java @@ -0,0 +1,12 @@ +package com.javadiscord.jdi.internal.api.application_commands; + +import com.javadiscord.jdi.internal.api.DiscordRequest; +import com.javadiscord.jdi.internal.api.DiscordRequestBuilder; + +public record FetchCommandRequest() implements DiscordRequest { + + @Override + public DiscordRequestBuilder create() { + return null; + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/FetchCommandsRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/FetchCommandsRequest.java new file mode 100644 index 00000000..232d11ab --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/FetchCommandsRequest.java @@ -0,0 +1,12 @@ +package com.javadiscord.jdi.internal.api.application_commands; + +import com.javadiscord.jdi.internal.api.DiscordRequest; +import com.javadiscord.jdi.internal.api.DiscordRequestBuilder; + +public record FetchCommandsRequest() implements DiscordRequest { + + @Override + public DiscordRequestBuilder create() { + return null; + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/RespondCommandRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/RespondCommandRequest.java new file mode 100644 index 00000000..0e57d679 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/application_commands/RespondCommandRequest.java @@ -0,0 +1,32 @@ +package com.javadiscord.jdi.internal.api.application_commands; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import com.javadiscord.jdi.core.api.builders.command.CallbackMessage; +import com.javadiscord.jdi.core.api.builders.command.CallbackResponseType; +import com.javadiscord.jdi.internal.api.DiscordRequest; +import com.javadiscord.jdi.internal.api.DiscordRequestBuilder; + +public record RespondCommandRequest( + CallbackResponseType type, + Optional message, + long interactionId, + String interactionToken +) implements DiscordRequest { + + @Override + public DiscordRequestBuilder create() { + + Map body = new HashMap<>(); + body.put("type", type.getValue()); + + message.ifPresent(m -> body.put("data", m)); + + return new DiscordRequestBuilder() + .post() + .body(body) + .path("/interactions/%s/%s/callback".formatted(interactionId, interactionToken)); + } +} diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/channel/CreateMessageRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/channel/CreateMessageRequest.java index db9ac121..35a412b1 100644 --- a/api/src/main/java/com/javadiscord/jdi/internal/api/channel/CreateMessageRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/channel/CreateMessageRequest.java @@ -72,8 +72,8 @@ public DiscordRequestBuilder create() { default -> multiPartBody.filePart(name, path, MediaType.ANY); } - } catch (FileNotFoundException e) { - throw new RuntimeException(e); + } catch (FileNotFoundException ignore) { + /* Ignore */ } } diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/sticker/CreateGuildStickerRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/sticker/CreateGuildStickerRequest.java index cd9cd8a3..70e3f2b3 100644 --- a/api/src/main/java/com/javadiscord/jdi/internal/api/sticker/CreateGuildStickerRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/sticker/CreateGuildStickerRequest.java @@ -34,6 +34,8 @@ public DiscordRequestBuilder create() { case "png" -> body.filePart("file", filePath, MediaType.IMAGE_PNG); case "jpg", "jpeg" -> body.filePart("file", filePath, MediaType.IMAGE_JPEG); case "gif" -> body.filePart("file", filePath, MediaType.IMAGE_GIF); + default -> + throw new IllegalArgumentException("Unsupported extension: " + extension); } return new DiscordRequestBuilder() diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/webhook/EditWebhookMessageRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/webhook/EditWebhookMessageRequest.java index 97b91948..dffbc90d 100644 --- a/api/src/main/java/com/javadiscord/jdi/internal/api/webhook/EditWebhookMessageRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/webhook/EditWebhookMessageRequest.java @@ -47,7 +47,9 @@ public DiscordRequestBuilder create() { for (int i = 0; i < paths.size(); i++) { try { bodyBuilder.filePart("file[%d]".formatted(i), paths.get(i)); - } catch (FileNotFoundException ignored) {} + } catch (FileNotFoundException ignored) { + /* Ignore */ + } } } ); diff --git a/api/src/main/java/com/javadiscord/jdi/internal/api/webhook/ExecuteWebhookRequest.java b/api/src/main/java/com/javadiscord/jdi/internal/api/webhook/ExecuteWebhookRequest.java index 72f541b6..9ac17dd2 100644 --- a/api/src/main/java/com/javadiscord/jdi/internal/api/webhook/ExecuteWebhookRequest.java +++ b/api/src/main/java/com/javadiscord/jdi/internal/api/webhook/ExecuteWebhookRequest.java @@ -70,7 +70,9 @@ public DiscordRequestBuilder create() { for (int i = 0; i < paths.size(); i++) { try { bodyBuilder.filePart("file[%d]".formatted(i), paths.get(i)); - } catch (FileNotFoundException ignored) {} + } catch (FileNotFoundException ignored) { + /* Ignore */ + } } } ); diff --git a/api/src/main/java/com/javadiscord/jdi/internal/utils/CacheUpdater.java b/api/src/main/java/com/javadiscord/jdi/internal/utils/CacheUpdater.java new file mode 100644 index 00000000..6f2ac997 --- /dev/null +++ b/api/src/main/java/com/javadiscord/jdi/internal/utils/CacheUpdater.java @@ -0,0 +1,63 @@ +package com.javadiscord.jdi.internal.utils; + +import java.lang.reflect.Field; +import java.util.List; + +import com.javadiscord.jdi.internal.cache.Cache; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class CacheUpdater { + + private final Cache cache; + + private static final Logger LOGGER = LogManager.getLogger(CacheUpdater.class); + + public CacheUpdater(Cache cache) { + this.cache = cache; + } + + public void updateCache(T result) { + if (result == null) { + return; + } + try { + Field guildIdField = result.getClass().getDeclaredField("guildId"); + Field idField = result.getClass().getDeclaredField("id"); + + long guildId = getLongFromField(guildIdField, result); + long id = getLongFromField(idField, result); + + if (cache.getCacheForGuild(guildId) == null) { + LOGGER.trace( + "Failed to cache result of type {} with guildId of {}", + result.getClass().getName(), guildId + ); + } else { + cache.getCacheForGuild(guildId).add(id, result); + } + + } catch (IllegalAccessException | NoSuchFieldException | NumberFormatException e) { + LOGGER.trace( + "Failed to cache result of type {}, cause: {}", + result.getClass().getName(), e.getMessage() + ); + } + } + + public void updateCache(List resultList) { + resultList.forEach(this::updateCache); + } + + private long getLongFromField( + Field field, + T result + ) throws IllegalAccessException, NumberFormatException { + field.setAccessible(true); + if (field.getType() == String.class) { + return Long.parseLong((String) field.get(result)); + } + return (long) field.get(result); + } +} diff --git a/api/src/test/integration/com/javadiscord/jdi/core/api/EmojiRequestTest.java b/api/src/test/integration/com/javadiscord/jdi/core/api/EmojiRequestTest.java index 854a3488..0a7b6483 100644 --- a/api/src/test/integration/com/javadiscord/jdi/core/api/EmojiRequestTest.java +++ b/api/src/test/integration/com/javadiscord/jdi/core/api/EmojiRequestTest.java @@ -6,6 +6,7 @@ import helpers.LiveDiscordHelper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -20,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.*; +@Disabled class EmojiRequestTest { private static Guild guild; @@ -53,7 +55,7 @@ void testCreateEmoji() throws InterruptedException, URISyntaxException, IOExcept latch.countDown(); }); - asyncResponse.onError(Assertions::fail); + asyncResponse.onError(e -> fail(e.getMessage())); assertTrue(latch.await(30, TimeUnit.SECONDS)); } diff --git a/api/src/test/integration/com/javadiscord/jdi/core/api/UserRequestTest.java b/api/src/test/integration/com/javadiscord/jdi/core/api/UserRequestTest.java index 29ee1e20..426b1005 100644 --- a/api/src/test/integration/com/javadiscord/jdi/core/api/UserRequestTest.java +++ b/api/src/test/integration/com/javadiscord/jdi/core/api/UserRequestTest.java @@ -24,12 +24,8 @@ public static void setup() throws InterruptedException { guild = new LiveDiscordHelper().getGuild(); } - @AfterEach - void delayBetweenTests() throws InterruptedException { - TimeUnit.SECONDS.sleep(30); - } - @Test + @Disabled void testGetCurrentUserGuildMember() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); AsyncResponse asyncResponse = guild.user().getCurrentUserGuildMember(); diff --git a/api/src/test/integration/helpers/LiveDiscordHelper.java b/api/src/test/integration/helpers/LiveDiscordHelper.java index 18b9d281..34ca7d49 100644 --- a/api/src/test/integration/helpers/LiveDiscordHelper.java +++ b/api/src/test/integration/helpers/LiveDiscordHelper.java @@ -17,7 +17,7 @@ public class LiveDiscordHelper { private static final CountDownLatch STARTUP_LATCH = new CountDownLatch(1); private static Guild guild; - private static class TestListener implements EventListener { + public static class TestListener implements EventListener { @Override public void onGuildCreate(Guild guild) { diff --git a/cache/src/main/java/com/javadiscord/jdi/internal/cache/Cache.java b/cache/src/main/java/com/javadiscord/jdi/internal/cache/Cache.java index 4d00bab0..59f496ac 100644 --- a/cache/src/main/java/com/javadiscord/jdi/internal/cache/Cache.java +++ b/cache/src/main/java/com/javadiscord/jdi/internal/cache/Cache.java @@ -20,7 +20,9 @@ public CacheInterface getCacheForGuild(long guildId) { if (!cache.containsKey(guildId)) { return cache.put(guildId, getCacheForType()); } - return cache.get(guildId); + return cache.get(guildId) == null + ? cache.put(guildId, getCacheForType()) + : cache.get(guildId); } public boolean isGuildCached(long guildId) { diff --git a/cache/src/test/unit/com/javadiscord/jdi/internal/cache/CacheTest.java b/cache/src/test/unit/com/javadiscord/jdi/internal/cache/CacheTest.java index b07d4378..e713d948 100644 --- a/cache/src/test/unit/com/javadiscord/jdi/internal/cache/CacheTest.java +++ b/cache/src/test/unit/com/javadiscord/jdi/internal/cache/CacheTest.java @@ -4,7 +4,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import org.junit.jupiter.api.Test; @@ -12,7 +12,7 @@ class CacheTest { @Test void testFetchingGuildFromCache() { - Guild guildMock = mock(Guild.class); + GuildModel guildMock = mock(GuildModel.class); when(guildMock.id()).thenReturn(1L); Cache cache = new Cache(CacheType.FULL); @@ -25,7 +25,7 @@ void testFetchingGuildFromCache() { @Test void testFetchingGuildFromCacheWithNoCache() { - Guild guildMock = mock(Guild.class); + GuildModel guildMock = mock(GuildModel.class); when(guildMock.id()).thenReturn(1L); Cache cache = new Cache(CacheType.NO_CACHE); @@ -37,7 +37,7 @@ void testFetchingGuildFromCacheWithNoCache() { @Test void testFetchingGuildFromCacheWithPartialCache() { - Guild guildMock = mock(Guild.class); + GuildModel guildMock = mock(GuildModel.class); when(guildMock.id()).thenReturn(1L); Cache cache = new Cache(CacheType.PARTIAL); @@ -49,7 +49,7 @@ void testFetchingGuildFromCacheWithPartialCache() { @Test void testFetchingFromFullCache() { - Guild guildMock = mock(Guild.class); + GuildModel guildMock = mock(GuildModel.class); when(guildMock.id()).thenReturn(1L); Cache cache = new Cache(CacheType.FULL); @@ -70,7 +70,7 @@ void testFetchingFromFullCache() { @Test void testFetchingFromFullCacheWhenItemNotPresent() { - Guild guildMock = mock(Guild.class); + GuildModel guildMock = mock(GuildModel.class); when(guildMock.id()).thenReturn(1L); Cache cache = new Cache(CacheType.FULL); @@ -86,7 +86,7 @@ void testFetchingFromFullCacheWhenItemNotPresent() { @Test void testRemovingGuildFromCache() { - Guild guildMock = mock(Guild.class); + GuildModel guildMock = mock(GuildModel.class); when(guildMock.id()).thenReturn(1L); Cache cache = new Cache(CacheType.FULL); @@ -101,7 +101,7 @@ void testRemovingGuildFromCache() { @Test void testGetCacheForType() { - Guild guildMock = mock(Guild.class); + GuildModel guildMock = mock(GuildModel.class); when(guildMock.id()).thenReturn(1L); { diff --git a/core/src/main/java/com/javadiscord/jdi/core/Constants.java b/core/src/main/java/com/javadiscord/jdi/core/Constants.java new file mode 100644 index 00000000..8fc84753 --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/core/Constants.java @@ -0,0 +1,30 @@ +package com.javadiscord.jdi.core; + +public class Constants { + public static final String COMMAND_OPTION_CHOICE_ANNOTATION = + "com.javadiscord.jdi.core.annotations.CommandOptionChoice"; + + public static final String SLASH_COMMAND_ANNOTATION = + "com.javadiscord.jdi.core.annotations.SlashCommand"; + + public static final String LISTENER_LOADER_CLASS = + "com.javadiscord.jdi.internal.processor.loader.ListenerLoader"; + public static final String COMPONENT_LOADER_CLASS = + "com.javadiscord.jdi.internal.processor.loader.ComponentLoader"; + public static final String SLASH_COMMAND_LOADER_CLASS = + "com.javadiscord.jdi.internal.processor.loader.SlashCommandLoader"; + + public static final String LAUNCH_HEADER = """ + + _ ____ ___ + | | _ \\_ _| https://github.com/javadiscord/java-discord-api + _ | | | | | | Open-Source Discord Framework + | |_| | |_| | | GPL-3.0 license + \\___/|____/___| Version 1.0 + """; + + private Constants() { + throw new UnsupportedOperationException("Utility class"); + } + +} diff --git a/core/src/main/java/com/javadiscord/jdi/core/Discord.java b/core/src/main/java/com/javadiscord/jdi/core/Discord.java index 87b3172a..1640ca7e 100644 --- a/core/src/main/java/com/javadiscord/jdi/core/Discord.java +++ b/core/src/main/java/com/javadiscord/jdi/core/Discord.java @@ -1,34 +1,51 @@ package com.javadiscord.jdi.core; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import com.javadiscord.jdi.core.api.builders.command.CommandBuilder; +import com.javadiscord.jdi.core.api.builders.command.CommandOption; +import com.javadiscord.jdi.core.api.builders.command.CommandOptionChoice; +import com.javadiscord.jdi.core.api.builders.command.CommandOptionType; +import com.javadiscord.jdi.core.interaction.InteractionEventHandler; +import com.javadiscord.jdi.core.models.ready.ReadyEvent; +import com.javadiscord.jdi.internal.*; import com.javadiscord.jdi.internal.api.DiscordRequest; import com.javadiscord.jdi.internal.api.DiscordRequestDispatcher; import com.javadiscord.jdi.internal.api.DiscordResponseFuture; +import com.javadiscord.jdi.internal.api.application_commands.CreateCommandRequest; +import com.javadiscord.jdi.internal.api.application_commands.DeleteCommandRequest; import com.javadiscord.jdi.internal.cache.Cache; import com.javadiscord.jdi.internal.cache.CacheType; +import com.javadiscord.jdi.internal.exceptions.GatewayException; +import com.javadiscord.jdi.internal.exceptions.InvalidBotTokenException; import com.javadiscord.jdi.internal.gateway.*; import com.javadiscord.jdi.internal.gateway.identify.IdentifyRequest; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Discord { private static final Logger LOGGER = LogManager.getLogger(Discord.class); private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); private static final String WEBSITE = "https://javadiscord.com/"; private static final String BASE_URL = @@ -43,10 +60,14 @@ public class Discord { private final GatewaySetting gatewaySetting; private final Cache cache; private final List annotatedEventListeners = new ArrayList<>(); + private final Map loadedSlashCommands = new HashMap<>(); private final List eventListeners = new ArrayList<>(); + private final List createInteractionRequests = new ArrayList<>(); + private final List deleteInteractionRequests = new ArrayList<>(); private WebSocketManager webSocketManager; - private Object listenerLoader; + private long applicationId; + private boolean started = false; public Discord(String botToken) { this( @@ -93,6 +114,8 @@ public Discord(String botToken, IdentifyRequest identifyRequest) { } public Discord(String botToken, IdentifyRequest identifyRequest, Cache cache) { + LOGGER.info(Constants.LAUNCH_HEADER); + this.botToken = botToken; this.discordRequestDispatcher = new DiscordRequestDispatcher(botToken); this.gateway = getGatewayURL(botToken); @@ -101,52 +124,180 @@ public Discord(String botToken, IdentifyRequest identifyRequest, Cache cache) { this.identifyRequest = identifyRequest; this.cache = cache; if (annotationLibPresent()) { - LOGGER.info("Annotation lib is present, loading annotations listeners..."); + LOGGER.info("Annotation lib is present"); + loadComponents(); loadAnnotations(); + loadSlashCommands(); + registerLoadedAnnotationsWithDiscord(); + } + } + + private void registerLoadedAnnotationsWithDiscord() { + LOGGER.info("Registering slash commands with Discord"); + loadedSlashCommands.forEach((commandName, slashCommandClassInstance) -> { + try { + ReflectiveSlashCommandClassMethod slashCommandClassMethod = + ReflectiveLoader + .proxy(slashCommandClassInstance, ReflectiveSlashCommandClassMethod.class); + + Method method = slashCommandClassMethod.method(); + + Annotation[] annotations = method.getAnnotations(); + for (Annotation annotation : annotations) { + if ( + annotation.annotationType().getName() + .equals(Constants.SLASH_COMMAND_ANNOTATION) + ) { + CommandBuilder builder = buildCommand(annotation); + createInteractionRequests.add(builder); + } + } + } catch (Exception e) { + LOGGER.error("Error registering slash command with Discord", e); + } + }); + } + + private CommandBuilder buildCommand(Annotation annotation) throws ReflectiveOperationException { + Method nameMethod = annotation.annotationType().getMethod("name"); + String name = (String) nameMethod.invoke(annotation); + + Method descriptionMethod = annotation.annotationType().getMethod("description"); + String description = (String) descriptionMethod.invoke(annotation); + + Method optionsMethod = annotation.annotationType().getMethod("options"); + Object[] options = (Object[]) optionsMethod.invoke(annotation); + + CommandBuilder builder = new CommandBuilder(name, description); + for (Object option : options) { + addCommandOption(builder, option); + } + + return builder; + } + + private void addCommandOption( + CommandBuilder builder, + Object option + ) { + + ReflectiveCommandOption reflectiveCommandOption = + ReflectiveLoader.proxy(option, ReflectiveCommandOption.class); + + String optionName = reflectiveCommandOption.name(); + String optionDescription = reflectiveCommandOption.description(); + String optionTypeValue = reflectiveCommandOption.type().name(); + boolean optionRequired = reflectiveCommandOption.required(); + + List choices = new ArrayList<>(); + Object[] choicesArray = reflectiveCommandOption.choices(); + for (Object choice : choicesArray) { + addCommandOptionChoice(choices, choice); + } + + builder.addOption( + new CommandOption( + optionName, + optionDescription, + CommandOptionType.fromName(optionTypeValue), + optionRequired + ) + .addChoice(choices) + ); + } + + private void addCommandOptionChoice(List choices, Object choice) { + Annotation annotation = (Annotation) choice; + if ( + annotation.annotationType().getName().equals(Constants.COMMAND_OPTION_CHOICE_ANNOTATION) + ) { + ReflectiveCommandOptionChoice commandOptionChoice = + ReflectiveLoader.proxy(annotation, ReflectiveCommandOptionChoice.class); + choices.add( + new CommandOptionChoice(commandOptionChoice.value(), commandOptionChoice.name()) + ); } } private boolean annotationLibPresent() { try { - Class.forName("com.javadiscord.jdi.core.processor.ListenerLoader"); + Class.forName(Constants.LISTENER_LOADER_CLASS); return true; } catch (Exception e) { return false; } } + private void loadComponents() { + LOGGER.info("Loading Components"); + try { + Class clazz = + Class.forName(Constants.COMPONENT_LOADER_CLASS); + ReflectiveComponentLoader componentLoader = null; + for (Constructor constructor : clazz.getConstructors()) { + if (constructor.getParameterCount() == 0) { + componentLoader = + ReflectiveLoader + .proxy(constructor.newInstance(), ReflectiveComponentLoader.class); + } + } + if (componentLoader != null) { + componentLoader.loadComponents(); + } else { + throw new InstantiationException("Unable to create ComponentLoader instance"); + } + } catch (Exception e) { + LOGGER.warn("Component loading failed", e); + } + } + private void loadAnnotations() { + LOGGER.info("Loading EventListeners"); try { - Class clazz = Class.forName("com.javadiscord.jdi.core.processor.ListenerLoader"); + Class clazz = + Class.forName(Constants.LISTENER_LOADER_CLASS); for (Constructor constructor : clazz.getConstructors()) { if (constructor.getParameterCount() == 1) { Parameter parameters = constructor.getParameters()[0]; if (parameters.getType().equals(List.class)) { - listenerLoader = constructor.newInstance(annotatedEventListeners); + constructor.newInstance(annotatedEventListeners); return; } } } - } catch ( - ClassNotFoundException - | InstantiationException - | IllegalAccessException - | InvocationTargetException ignore - ) { - /* Ignore */ + } catch (Exception e) { + LOGGER.warn("Event listener loading failed", e); } } - public void start() { - webSocketManager = - new WebSocketManager( - new GatewaySetting().setApiVersion(10).setEncoding(GatewayEncoding.JSON), - identifyRequest, - cache - ); + private void loadSlashCommands() { + LOGGER.info("Loading SlashCommands"); + try { + Class clazz = + Class.forName(Constants.SLASH_COMMAND_LOADER_CLASS); + for (Constructor constructor : clazz.getConstructors()) { + if (constructor.getParameterCount() == 1) { + Parameter parameters = constructor.getParameters()[0]; + if (parameters.getType().equals(Map.class)) { + eventListeners.add( + new InteractionEventHandler( + constructor.newInstance(loadedSlashCommands), + this + ) + ); + return; + } + } + } + } catch (Exception e) { + LOGGER.error("Failed to load SlashCommands", e); + } + } - WebSocketManagerProxy webSocketManagerProxy = - new WebSocketManagerProxy(webSocketManager); + public void start() { + started = true; + webSocketManager = new WebSocketManager(gatewaySetting, identifyRequest, cache); + WebSocketManagerProxy webSocketManagerProxy = new WebSocketManagerProxy(webSocketManager); ConnectionDetails connectionDetails = new ConnectionDetails(gateway.url(), botToken, gatewaySetting); ConnectionMediator connectionMediator = @@ -154,19 +305,18 @@ public void start() { connectionMediator.addObserver(new GatewayEventListenerAnnotations(this)); connectionMediator.addObserver(new GatewayEventListener(this)); webSocketManagerProxy.start(connectionMediator); - - EXECUTOR.execute(discordRequestDispatcher); } public void stop() { - LOGGER.info("Shutdown initiated"); + started = false; - if (webSocketManager != null) { - webSocketManager.stop(); + if (this.webSocketManager != null) { + this.webSocketManager.stop(); } - discordRequestDispatcher.stop(); + LOGGER.info("Shutdown initiated"); + discordRequestDispatcher.stop(); EXECUTOR.shutdown(); try { @@ -174,13 +324,12 @@ public void stop() { EXECUTOR.shutdownNow(); if (!EXECUTOR.awaitTermination(30, TimeUnit.SECONDS)) { LOGGER.warn( - "Executor failed to shutdown within the specified time limit, some" - + " tasks may still be running" + "Executor failed to shutdown within the specified time limit, some tasks may still be running" ); } } } catch (InterruptedException e) { - LOGGER.error("Termination was interrupted within {} seconds", 30, e); + LOGGER.error("Termination was interrupted", e); Thread.currentThread().interrupt(); } } @@ -210,18 +359,37 @@ private static Gateway getGatewayURL(String authentication) { httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 401) { - throw new RuntimeException("Invalid bot token provided"); + throw new InvalidBotTokenException("Invalid bot token provided"); } if (response.statusCode() != 200) { - throw new RuntimeException("Unexpected error occurred: " + response.body()); + throw new GatewayException("Unexpected error occurred: " + response.body()); } return OBJECT_MAPPER.readValue(response.body(), Gateway.class); } catch (Exception e) { LOGGER.error("Failed to fetch the gateway URL from discord", e); - throw new RuntimeException(e); + throw new GatewayException(e.getMessage()); } } + public void registerSlashCommand(String name, String description, CommandOption... options) { + CommandBuilder builder = new CommandBuilder(name, description); + for (CommandOption option : options) { + builder.addOption(option); + } + builder.applicationId(applicationId); + createInteractionRequests.add(builder); + } + + public void registerSlashCommand(CommandBuilder builder) { + builder.applicationId(applicationId); + createInteractionRequests.add(builder); + } + + public void deleteSlashCommand(long commandId, long guildId, boolean global) { + deleteInteractionRequests + .add(new DeleteCommandRequest(applicationId, guildId, commandId, global)); + } + public DiscordRequestDispatcher getDiscordRequestDispatcher() { return discordRequestDispatcher; } @@ -234,7 +402,72 @@ public List getAnnotatedEventListeners() { return annotatedEventListeners; } + public boolean isStarted() { + return started; + } + public List getEventListeners() { return eventListeners; } + + public long getApplicationId() { + return applicationId; + } + + void handleReadyEvent(ReadyEvent event) { + applicationId = event.application().id(); + EXECUTOR.execute(discordRequestDispatcher); + + for (CommandBuilder builder : createInteractionRequests) { + builder.applicationId(applicationId); + CreateCommandRequest request = builder.build(); + DiscordResponseFuture future = sendRequest(request); + handleCommandRegistrationResponse(request, future); + } + + for (DeleteCommandRequest request : deleteInteractionRequests) { + DiscordResponseFuture future = sendRequest(request); + handleDeleteResponse(request, future); + } + + createInteractionRequests.clear(); + deleteInteractionRequests.clear(); + } + + private void handleDeleteResponse(DeleteCommandRequest request, DiscordResponseFuture future) { + future.onSuccess(res -> { + if (res.status() >= 200 && res.status() < 300) { + LOGGER.info("Deleted slash command {} with discord", request.commandId()); + } else { + LOGGER.error( + "Failed to delete slash command {} with discord\n{}", request.commandId(), + res.body() + ); + } + }); + future.onError( + err -> LOGGER + .error("Failed to delete slash command {} with discord", request.commandId(), err) + ); + } + + private void handleCommandRegistrationResponse( + CreateCommandRequest request, + DiscordResponseFuture future + ) { + future.onSuccess(res -> { + if (res.status() >= 200 && res.status() < 300) { + LOGGER.info("Registered slash command {} with discord", request.name()); + } else { + LOGGER.error( + "Failed to register slash command {} with discord\n{}", request.name(), + res.body() + ); + } + }); + future.onError( + err -> LOGGER + .error("Failed to register slash command {} with discord", request.name(), err) + ); + } } diff --git a/core/src/main/java/com/javadiscord/jdi/core/EventListener.java b/core/src/main/java/com/javadiscord/jdi/core/EventListener.java index 816144ee..96430105 100644 --- a/core/src/main/java/com/javadiscord/jdi/core/EventListener.java +++ b/core/src/main/java/com/javadiscord/jdi/core/EventListener.java @@ -133,7 +133,7 @@ default void onMessageReactionAdd(MessageReaction messageReaction, Guild guild) default void onThreadMembersUpdate(ThreadMember threadMember, Guild guild) {} - default void onGuildIntegrationUpdate(Integration integration, Guild guild) {} + default void onGuildIntegrationUpdate(IntegrationUpdate integration, Guild guild) {} default void onVoiceServerUpdate(VoiceServer voiceServer, Guild guild) {} diff --git a/core/src/main/java/com/javadiscord/jdi/core/GatewayEventListener.java b/core/src/main/java/com/javadiscord/jdi/core/GatewayEventListener.java index 148c7949..34b545b6 100644 --- a/core/src/main/java/com/javadiscord/jdi/core/GatewayEventListener.java +++ b/core/src/main/java/com/javadiscord/jdi/core/GatewayEventListener.java @@ -10,6 +10,7 @@ import com.javadiscord.jdi.core.models.guild.*; import com.javadiscord.jdi.core.models.invite.Invite; import com.javadiscord.jdi.core.models.message.*; +import com.javadiscord.jdi.core.models.ready.ReadyEvent; import com.javadiscord.jdi.core.models.scheduled_event.EventUser; import com.javadiscord.jdi.core.models.scheduled_event.ScheduledEvent; import com.javadiscord.jdi.core.models.stage.Stage; @@ -36,43 +37,58 @@ public GatewayEventListener(Discord discord) { this.discord = discord; } - static Guild getGuild(Discord discord, Object event) { - if (event instanceof com.javadiscord.jdi.core.models.guild.Guild) { - return new Guild( - (com.javadiscord.jdi.core.models.guild.Guild) event, - discord.getCache(), - discord + public static Guild getGuild(Discord discord, Object event) { + if (event instanceof GuildModel guildModel) { + return createGuildFromEvent( + discord, guildModel ); + } else { + return createGuildFromEventObject(discord, event); } + } + + private static Guild createGuildFromEvent( + Discord discord, + GuildModel guildEvent + ) { + return new Guild(guildEvent, discord.getCache(), discord); + } + private static Guild createGuildFromEventObject(Discord discord, Object event) { Cache cache = discord.getCache(); Guild guild = null; try { - Field guildIdField = event.getClass().getDeclaredField("guildId"); - guildIdField.setAccessible(true); - long guildId; - - if (guildIdField.getType() == String.class) { - guildId = Long.parseLong((String) guildIdField.get(event)); - } else { - guildId = (long) guildIdField.get(event); - } - - com.javadiscord.jdi.core.models.guild.Guild model = - (com.javadiscord.jdi.core.models.guild.Guild) cache.getCacheForGuild(guildId) - .get( - guildId, - com.javadiscord.jdi.core.models.guild.Guild.class - ); + long guildId = extractGuildId(event); + GuildModel model = + (GuildModel) cache.getCacheForGuild(guildId) + .get(guildId, GuildModel.class); guild = new Guild(model, cache, discord); } catch (NoSuchFieldException | IllegalAccessException e) { LOGGER.debug("{} did not come with a guildId field", event.getClass().getSimpleName()); } + return guild; } + private static long extractGuildId( + Object event + ) throws NoSuchFieldException, IllegalAccessException { + Field guildIdField = event.getClass().getDeclaredField("guildId"); + guildIdField.setAccessible(true); + + if (guildIdField.getType() == String.class) { + return Long.parseLong((String) guildIdField.get(event)); + } else { + return (long) guildIdField.get(event); + } + } + @Override public void receive(EventType eventType, Object event) { + if (eventType == EventType.READY) { + discord.handleReadyEvent((ReadyEvent) event); + } + Guild guild = getGuild(discord, event); for (EventListener listener : discord.getEventListeners()) { @@ -128,7 +144,7 @@ public void receive(EventType eventType, Object event) { case MESSAGE_REACTION_REMOVE -> listener.onMessageReactionsRemoved((MessageReactionsRemoved) event, guild); case GUILD_INTEGRATIONS_UPDATE -> - listener.onGuildIntegrationUpdate((Integration) event, guild); + listener.onGuildIntegrationUpdate((IntegrationUpdate) event, guild); case AUTO_MODERATION_RULE_CREATE -> listener.onAutoModerationRuleCreate((AutoModerationRule) event, guild); case AUTO_MODERATION_RULE_DELETE -> diff --git a/core/src/main/java/com/javadiscord/jdi/core/GatewayEventListenerAnnotations.java b/core/src/main/java/com/javadiscord/jdi/core/GatewayEventListenerAnnotations.java index 3953f356..4db68b10 100644 --- a/core/src/main/java/com/javadiscord/jdi/core/GatewayEventListenerAnnotations.java +++ b/core/src/main/java/com/javadiscord/jdi/core/GatewayEventListenerAnnotations.java @@ -212,54 +212,77 @@ public GatewayEventListenerAnnotations(Discord discord) { this.discord = discord; } - @SuppressWarnings("unchecked") @Override public void receive(EventType eventType, Object event) { - if (!EVENT_TYPE_ANNOTATIONS.containsKey(eventType)) { + if (!isEventTypeValid(eventType)) { return; } - Class annotationClass; - try { - annotationClass = - (Class) Class.forName(EVENT_TYPE_ANNOTATIONS.get(eventType)); - } catch (ClassNotFoundException e) { + + Class annotationClass = getAnnotationClass(eventType); + if (annotationClass == null) { LOGGER.error("Could not find annotation binding for {}", eventType); return; } + + invokeAnnotatedMethods(annotationClass, event); + } + + private boolean isEventTypeValid(EventType eventType) { + return EVENT_TYPE_ANNOTATIONS.containsKey(eventType); + } + + @SuppressWarnings("unchecked") + private Class getAnnotationClass(EventType eventType) { + try { + return (Class) Class + .forName(EVENT_TYPE_ANNOTATIONS.get(eventType)); + } catch (ClassNotFoundException e) { + return null; + } + } + + private void invokeAnnotatedMethods(Class annotationClass, Object event) { for (Object listener : discord.getAnnotatedEventListeners()) { Method[] methods = listener.getClass().getMethods(); - List paramOrder = new ArrayList<>(); for (Method method : methods) { - if (!method.isAnnotationPresent(annotationClass)) { - continue; - } - Parameter[] parameters = method.getParameters(); - for (Parameter parameter : parameters) { - if (parameter.getParameterizedType() == event.getClass()) { - paramOrder.add(event); - } else if (parameter.getParameterizedType() == Discord.class) { - paramOrder.add(discord); - } else if (parameter.getParameterizedType() == Guild.class) { - Guild guild = GatewayEventListener.getGuild(discord, event); - paramOrder.add(guild); - } - } - try { - if (paramOrder.size() != method.getParameterCount()) { - throw new RuntimeException( - "Bound " - + paramOrder.size() - + " parameters but expected " - + method.getParameterCount() - ); - } - LOGGER.trace("Invoking method {} with params {}", method.getName(), paramOrder); - method.invoke(listener, paramOrder.toArray()); - } catch (Exception e) { - LOGGER.error("Failed to invoke {}", method.getName(), e); - throw new RuntimeException(e); + if (method.isAnnotationPresent(annotationClass)) { + invokeMethod(listener, method, event); } } } } + + private void invokeMethod(Object listener, Method method, Object event) { + List paramOrder = getParamOrder(method, event); + if (paramOrder.size() != method.getParameterCount()) { + LOGGER.error( + "Bound {} parameters but expected {}", paramOrder.size(), method.getParameterCount() + ); + return; + } + try { + LOGGER.trace("Invoking method {} with params {}", method.getName(), paramOrder); + method.invoke(listener, paramOrder.toArray()); + } catch (Exception e) { + LOGGER.error("Failed to invoke {}", method.getName(), e); + throw new RuntimeException(e); + } + } + + private List getParamOrder(Method method, Object event) { + List paramOrder = new ArrayList<>(); + Parameter[] parameters = method.getParameters(); + for (Parameter parameter : parameters) { + if (parameter.getParameterizedType() == event.getClass()) { + paramOrder.add(event); + } else if (parameter.getParameterizedType() == Discord.class) { + paramOrder.add(discord); + } else if (parameter.getParameterizedType() == Guild.class) { + Guild guild = GatewayEventListener.getGuild(discord, event); + paramOrder.add(guild); + } + } + return paramOrder; + } + } diff --git a/core/src/main/java/com/javadiscord/jdi/core/Guild.java b/core/src/main/java/com/javadiscord/jdi/core/Guild.java index 5b684774..ba1d3102 100644 --- a/core/src/main/java/com/javadiscord/jdi/core/Guild.java +++ b/core/src/main/java/com/javadiscord/jdi/core/Guild.java @@ -2,10 +2,11 @@ import com.javadiscord.jdi.core.api.*; import com.javadiscord.jdi.core.api.builders.CreateMessageBuilder; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.internal.cache.Cache; public class Guild { - private final com.javadiscord.jdi.core.models.guild.Guild metadata; + private final GuildModel metadata; private final Cache cache; private final Discord discord; private final ApplicationRequest applicationRequest; @@ -23,8 +24,9 @@ public class Guild { private final StickerRequest stickerRequest; private final UserRequest userRequest; private final VoiceRequest voiceRequest; + private final InteractionRequest interactionRequest; - public Guild(com.javadiscord.jdi.core.models.guild.Guild guild, Cache cache, Discord discord) { + public Guild(GuildModel guild, Cache cache, Discord discord) { this.metadata = guild; this.cache = cache; this.discord = discord; @@ -32,7 +34,7 @@ public Guild(com.javadiscord.jdi.core.models.guild.Guild guild, Cache cache, Dis long guildId = guild.id(); DiscordResponseParser discordResponseParser = - new DiscordResponseParser(discord.getDiscordRequestDispatcher()); + new DiscordResponseParser(discord.getDiscordRequestDispatcher(), cache); this.applicationRequest = new ApplicationRequest(discordResponseParser, guildId); this.applicationRoleConnectionMetaRequest = @@ -51,6 +53,12 @@ public Guild(com.javadiscord.jdi.core.models.guild.Guild guild, Cache cache, Dis this.stickerRequest = new StickerRequest(discordResponseParser, guildId); this.userRequest = new UserRequest(discordResponseParser, guildId); this.voiceRequest = new VoiceRequest(discordResponseParser, guildId); + this.interactionRequest = + new InteractionRequest(discordResponseParser, guildId, discord.getApplicationId()); + } + + public InteractionRequest interaction() { + return interactionRequest; } public ChannelRequest channel() { @@ -65,7 +73,7 @@ public void sendMessage(CreateMessageBuilder builder) { channelRequest.createMessage(builder); } - public com.javadiscord.jdi.core.models.guild.Guild getMetadata() { + public GuildModel getMetadata() { return metadata; } diff --git a/core/src/main/java/com/javadiscord/jdi/core/interaction/InteractionEventHandler.java b/core/src/main/java/com/javadiscord/jdi/core/interaction/InteractionEventHandler.java new file mode 100644 index 00000000..70ba19db --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/core/interaction/InteractionEventHandler.java @@ -0,0 +1,116 @@ +package com.javadiscord.jdi.core.interaction; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; + +import com.javadiscord.jdi.core.Discord; +import com.javadiscord.jdi.core.EventListener; +import com.javadiscord.jdi.core.GatewayEventListener; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.models.guild.Interaction; +import com.javadiscord.jdi.internal.ReflectiveLoader; +import com.javadiscord.jdi.internal.ReflectiveSlashCommandClassMethod; +import com.javadiscord.jdi.internal.ReflectiveSlashCommandLoader; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class InteractionEventHandler implements EventListener { + private static final Logger LOGGER = LogManager.getLogger(InteractionEventHandler.class); + private final Object slashCommandLoader; + private final Discord discord; + + private final Map cachedInstances = new HashMap<>(); + + public InteractionEventHandler(Object slashCommandLoader, Discord discord) { + this.slashCommandLoader = slashCommandLoader; + this.discord = discord; + } + + @Override + public void onInteractionCreate(Interaction interaction, Guild guild) { + String command = interaction.data().name(); + + try { + ReflectiveSlashCommandLoader reflectiveSlashCommandLoader = + ReflectiveLoader.proxy(slashCommandLoader, ReflectiveSlashCommandLoader.class); + + ReflectiveSlashCommandClassMethod reflectiveSlashCommandClassMethod = + ReflectiveLoader.proxy( + reflectiveSlashCommandLoader.getSlashCommandClassMethod(command), + ReflectiveSlashCommandClassMethod.class + ); + + Class handler = reflectiveSlashCommandClassMethod.clazz(); + Method method = reflectiveSlashCommandClassMethod.method(); + + List paramOrder = getOrderOfParameters(method, interaction); + + if (validateParameterCount(method, paramOrder)) { + invokeHandler(handler, method, paramOrder); + } else { + throw new InstantiationException( + "Bound " + paramOrder.size() + " parameters but expected " + + method.getParameterCount() + ); + } + + } catch (Exception e) { + LOGGER.error("Failed to invoke handler for /{}", command, e); + } + } + + private List getOrderOfParameters(Method method, Interaction interaction) { + List paramOrder = new ArrayList<>(); + Parameter[] parameters = method.getParameters(); + + for (Parameter parameter : parameters) { + if (parameter.getParameterizedType() == interaction.getClass()) { + paramOrder.add(interaction); + } else if (parameter.getParameterizedType() == Discord.class) { + paramOrder.add(discord); + } else if (parameter.getParameterizedType() == Guild.class) { + paramOrder.add(GatewayEventListener.getGuild(discord, interaction.guild())); + } else if (parameter.getParameterizedType() == SlashCommandEvent.class) { + paramOrder.add(new SlashCommandEvent(interaction, discord)); + } + } + + return paramOrder; + } + + private boolean validateParameterCount(Method method, List paramOrder) { + return paramOrder.size() == method.getParameterCount(); + } + + private void invokeHandler( + Class handler, + Method method, + List paramOrder + ) throws InstantiationException { + try { + if (cachedInstances.containsKey(handler.getName())) { + method.invoke(cachedInstances.get(handler.getName()), paramOrder.toArray()); + } else { + Object handlerInstance = handler.getDeclaredConstructor().newInstance(); + cachedInstances.put(handler.getName(), handlerInstance); + injectComponents(handlerInstance); + method.invoke(handlerInstance, paramOrder.toArray()); + } + } catch ( + InvocationTargetException | IllegalAccessException | NoSuchMethodException + | InstantiationException e + ) { + throw new InstantiationException(e.getLocalizedMessage()); + } + } + + private void injectComponents(Object object) { + ReflectiveSlashCommandLoader reflectiveSlashCommandLoader = + ReflectiveLoader.proxy(slashCommandLoader, ReflectiveSlashCommandLoader.class); + + reflectiveSlashCommandLoader.injectComponents(object); + } +} diff --git a/core/src/main/java/com/javadiscord/jdi/core/interaction/SlashCommandEvent.java b/core/src/main/java/com/javadiscord/jdi/core/interaction/SlashCommandEvent.java new file mode 100644 index 00000000..a654298a --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/core/interaction/SlashCommandEvent.java @@ -0,0 +1,159 @@ +package com.javadiscord.jdi.core.interaction; + +import java.util.List; +import java.util.Optional; + +import com.javadiscord.jdi.core.Discord; +import com.javadiscord.jdi.core.GatewayEventListener; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.api.AsyncResponse; +import com.javadiscord.jdi.core.api.builders.command.CallbackMessage; +import com.javadiscord.jdi.core.api.builders.command.CallbackMessageBuilder; +import com.javadiscord.jdi.core.api.builders.command.CallbackResponseType; +import com.javadiscord.jdi.core.models.application.ApplicationCommandOption; +import com.javadiscord.jdi.core.models.channel.Channel; +import com.javadiscord.jdi.core.models.guild.Interaction; +import com.javadiscord.jdi.core.models.guild.InteractionData; +import com.javadiscord.jdi.core.models.guild.InteractionType; +import com.javadiscord.jdi.core.models.guild.ResolvedData; +import com.javadiscord.jdi.core.models.message.embed.Embed; +import com.javadiscord.jdi.core.models.user.User; +import com.javadiscord.jdi.internal.api.DiscordResponseFuture; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SlashCommandEvent { + private static final Logger LOGGER = LogManager.getLogger(SlashCommandEvent.class); + private final Interaction interaction; + private final Discord discord; + private boolean deferred; + + public SlashCommandEvent(Interaction interaction, Discord discord) { + this.interaction = interaction; + this.discord = discord; + this.deferred = false; + } + + public void deferReply() { + CallbackMessageBuilder builder = + new CallbackMessageBuilder( + CallbackResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, + interaction.id(), + interaction.token() + ); + + DiscordResponseFuture future = discord.sendRequest(builder.build()); + + future.onError(err -> LOGGER.error("Failed to defer response", err)); + deferred = true; + } + + public void deferReplyWithoutSpinner() { + CallbackMessageBuilder builder = + new CallbackMessageBuilder( + CallbackResponseType.DEFERRED_UPDATE_MESSAGE, + interaction.id(), + interaction.token() + ); + + DiscordResponseFuture future = discord.sendRequest(builder.build()); + + future.onError(err -> LOGGER.error("Failed to defer response", err)); + deferred = true; + } + + public AsyncResponse reply(CallbackMessageBuilder builder) { + return sendReply(builder); + } + + public AsyncResponse reply(Embed... embed) { + return sendReply( + new CallbackMessageBuilder( + CallbackResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + interaction.id(), + interaction.token() + ) + .message(new CallbackMessage().setEmbeds(List.of(embed))) + ); + } + + public AsyncResponse reply(String message) { + return sendReply( + new CallbackMessageBuilder( + CallbackResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + interaction.id(), + interaction.token() + ) + .message(new CallbackMessage().setContent(message)) + ); + } + + private AsyncResponse sendReply(CallbackMessageBuilder builder) { + AsyncResponse asyncResponse = new AsyncResponse<>(); + DiscordResponseFuture future; + + if (deferred) { + builder.applicationId(discord.getApplicationId()); + future = discord.sendRequest(builder.buildEdit()); + } else { + future = discord.sendRequest(builder.build()); + } + + future.onSuccess(res -> { + if (res.status() >= 200 && res.status() < 300) { + asyncResponse.setResult(res.body()); + } else { + asyncResponse.setException( + new Exception("Received " + res.status() + "\n" + res.body()) + ); + } + }); + future.onError(asyncResponse::setException); + + return asyncResponse; + } + + public Channel channel() { + return interaction.channel(); + } + + public Guild guild() { + return GatewayEventListener.getGuild(discord, interaction.guild()); + } + + public User user() { + return interaction.member().user(); + } + + public InteractionType interactionType() { + return interaction.type(); + } + + public InteractionData interactionData() { + return interaction.data(); + } + + public ResolvedData resolvedData() { + return interactionData().resolved(); + } + + public Optional option(String name) { + InteractionData interactionData = interaction.data(); + ApplicationCommandOption[] options = interactionData.options(); + for (ApplicationCommandOption option : options) { + if (option.name().equals(name)) { + return Optional.of(option); + } + } + return Optional.empty(); + } + + public ApplicationCommandOption[] options() { + return interaction.data().options(); + } + + public Interaction interaction() { + return interaction; + } +} diff --git a/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveCommandOption.java b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveCommandOption.java new file mode 100644 index 00000000..5023e098 --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveCommandOption.java @@ -0,0 +1,13 @@ +package com.javadiscord.jdi.internal; + +public interface ReflectiveCommandOption { + String name(); + + String description(); + + Enum type(); + + Object[] choices(); + + boolean required(); +} diff --git a/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveCommandOptionChoice.java b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveCommandOptionChoice.java new file mode 100644 index 00000000..505c8f07 --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveCommandOptionChoice.java @@ -0,0 +1,7 @@ +package com.javadiscord.jdi.internal; + +public interface ReflectiveCommandOptionChoice { + String name(); + + String value(); +} diff --git a/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveComponentLoader.java b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveComponentLoader.java new file mode 100644 index 00000000..858f04f1 --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveComponentLoader.java @@ -0,0 +1,5 @@ +package com.javadiscord.jdi.internal; + +public interface ReflectiveComponentLoader { + void loadComponents(); +} diff --git a/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveLoader.java b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveLoader.java new file mode 100644 index 00000000..d2ffcb54 --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveLoader.java @@ -0,0 +1,26 @@ +package com.javadiscord.jdi.internal; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; + +public class ReflectiveLoader { + private ReflectiveLoader() { + throw new UnsupportedOperationException("Utility class"); + } + + public static T proxy(Object object, Class interfaceClass) { + InvocationHandler handler = + (proxy, method, methodArgs) -> object.getClass() + .getMethod(method.getName(), method.getParameterTypes()) + .invoke(object, methodArgs); + + @SuppressWarnings("unchecked") T proxyInstance = + (T) Proxy.newProxyInstance( + interfaceClass.getClassLoader(), + new Class[] {interfaceClass}, + handler + ); + + return proxyInstance; + } +} diff --git a/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveSlashCommandClassMethod.java b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveSlashCommandClassMethod.java new file mode 100644 index 00000000..6cb82e96 --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveSlashCommandClassMethod.java @@ -0,0 +1,9 @@ +package com.javadiscord.jdi.internal; + +import java.lang.reflect.Method; + +public interface ReflectiveSlashCommandClassMethod { + Class clazz(); + + Method method(); +} diff --git a/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveSlashCommandLoader.java b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveSlashCommandLoader.java new file mode 100644 index 00000000..d88434ab --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/internal/ReflectiveSlashCommandLoader.java @@ -0,0 +1,7 @@ +package com.javadiscord.jdi.internal; + +public interface ReflectiveSlashCommandLoader { + Object getSlashCommandClassMethod(String name); + + void injectComponents(Object object); +} diff --git a/core/src/main/java/com/javadiscord/jdi/internal/exceptions/GatewayException.java b/core/src/main/java/com/javadiscord/jdi/internal/exceptions/GatewayException.java new file mode 100644 index 00000000..057dc420 --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/internal/exceptions/GatewayException.java @@ -0,0 +1,12 @@ +package com.javadiscord.jdi.internal.exceptions; + +public class GatewayException extends RuntimeException { + + public GatewayException() { + super(); + } + + public GatewayException(String message) { + super(message); + } +} diff --git a/core/src/main/java/com/javadiscord/jdi/internal/exceptions/InvalidBotTokenException.java b/core/src/main/java/com/javadiscord/jdi/internal/exceptions/InvalidBotTokenException.java new file mode 100644 index 00000000..b2ca5956 --- /dev/null +++ b/core/src/main/java/com/javadiscord/jdi/internal/exceptions/InvalidBotTokenException.java @@ -0,0 +1,12 @@ +package com.javadiscord.jdi.internal.exceptions; + +public class InvalidBotTokenException extends RuntimeException { + + public InvalidBotTokenException() { + super(); + } + + public InvalidBotTokenException(String message) { + super(message); + } +} diff --git a/gateway/src/main/resources/log4j2.xml b/core/src/main/resources/log4j2.xml similarity index 100% rename from gateway/src/main/resources/log4j2.xml rename to core/src/main/resources/log4j2.xml diff --git a/example/echo-bot/src/main/java/com/javadiscord/jdi/example/ExampleSlashCommand.java b/example/echo-bot/src/main/java/com/javadiscord/jdi/example/ExampleSlashCommand.java new file mode 100644 index 00000000..785feafb --- /dev/null +++ b/example/echo-bot/src/main/java/com/javadiscord/jdi/example/ExampleSlashCommand.java @@ -0,0 +1,139 @@ +package com.javadiscord.jdi.example; + +import java.awt.*; +import java.util.Optional; + +import com.javadiscord.jdi.core.CommandOptionType; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.annotations.CommandOption; +import com.javadiscord.jdi.core.annotations.CommandOptionChoice; +import com.javadiscord.jdi.core.annotations.SlashCommand; +import com.javadiscord.jdi.core.interaction.SlashCommandEvent; +import com.javadiscord.jdi.core.models.application.ApplicationCommandOption; +import com.javadiscord.jdi.core.models.message.embed.Embed; + +public class ExampleSlashCommand { + + @SlashCommand( + name = "quiz", description = "A fun Java quiz", options = { + @CommandOption( + name = "q1", description = "What is an Integer?", type = CommandOptionType.STRING, choices = { + @CommandOptionChoice( + name = "option1", value = "An object that represents a number" + ), + @CommandOptionChoice(name = "option2", value = "A class used to store objects"), + @CommandOptionChoice(name = "option3", value = "A primitive data type") + } + ), + @CommandOption( + name = "q2", description = "In which package is the List interface defined?", type = CommandOptionType.STRING, choices = { + @CommandOptionChoice(name = "option1", value = "java.util"), + @CommandOptionChoice(name = "option2", value = "java.lang"), + @CommandOptionChoice(name = "option3", value = "java.io") + } + ), + @CommandOption( + name = "q3", description = "What does JVM stand for?", type = CommandOptionType.STRING, choices = { + @CommandOptionChoice(name = "option1", value = "Java Virtual Machine"), + @CommandOptionChoice(name = "option2", value = "Java Verified Module"), + @CommandOptionChoice(name = "option3", value = "Java Variable Method") + } + ), + @CommandOption( + name = "q4", description = "Is a String a primitive data type?", type = CommandOptionType.STRING, choices = { + @CommandOptionChoice(name = "option1", value = "Yes"), + @CommandOptionChoice(name = "option2", value = "No") + } + ), + @CommandOption( + name = "q5", description = "Which of the following is not a Java keyword?", type = CommandOptionType.STRING, choices = { + @CommandOptionChoice(name = "option1", value = "static"), + @CommandOptionChoice(name = "option2", value = "void"), + @CommandOptionChoice(name = "option3", value = "main"), + @CommandOptionChoice(name = "option4", value = "private") + } + ) + } + ) + public void handle(SlashCommandEvent event, Guild guild) { + event.deferReply(); + + Optional q1 = event.option("q1"); + Optional q2 = event.option("q2"); + Optional q3 = event.option("q3"); + Optional q4 = event.option("q4"); + Optional q5 = event.option("q5"); + + StringBuilder feedback = new StringBuilder(); + + q1.ifPresent(answer -> { + if (answer.valueAsString().equals("option1")) { + feedback.append("Q1: Correct!\n"); + } else { + feedback.append("Q1: Incorrect\n"); + } + }); + + q2.ifPresent(answer -> { + if (answer.valueAsString().equals("option1")) { + feedback.append("Q2: Correct!\n"); + } else { + feedback.append("Q2: Incorrect\n"); + } + }); + + q3.ifPresent(answer -> { + if (answer.valueAsString().equals("option1")) { + feedback.append("Q3: Correct!\n"); + } else { + feedback.append("Q3: Incorrect\n"); + } + }); + + q4.ifPresent(answer -> { + if (answer.valueAsString().equals("option2")) { + feedback.append("Q4: Correct!\n"); + } else { + feedback.append("Q4: Incorrect\n"); + } + }); + + q5.ifPresent(answer -> { + if (answer.valueAsString().equals("option3")) { + feedback.append("Q5: Correct!\n"); + } else { + feedback.append("Q5: Incorrect\n"); + } + }); + + int score = score(feedback.toString()); + + feedback.append("Your score: ").append(score).append("/5"); + + if (score == 5) { + feedback.append("\nCongratulations! You you all the questions right!\n"); + } + + Embed embed = + new Embed.Builder() + .color(Color.CYAN) + .description(feedback.toString()) + .build(); + + event.reply(embed) + .onSuccess(System.out::println) + .onError(System.err::println); + } + + private static int score(String str) { + String word = "Correct"; + int index = 0; + int count = 0; + while ((index = str.indexOf(word, index)) != -1) { + count++; + index += word.length(); + } + return count; + } + +} diff --git a/example/lj-discord-bot/build.gradle b/example/lj-discord-bot/build.gradle new file mode 100644 index 00000000..99e06c01 --- /dev/null +++ b/example/lj-discord-bot/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +application { + mainClass = 'com.javadiscord.bot.Main' +} + +dependencies { + implementation 'com.github.docker-java:docker-java:3.3.6' + implementation 'com.theokanning.openai-gpt3-java:service:0.18.2' + implementation 'com.rometools:rome:2.1.0' +} + +shadowJar { + archiveBaseName.set('lj-discord-bot') + archiveClassifier.set('') + archiveVersion.set('') +} \ No newline at end of file diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/Main.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/Main.java new file mode 100644 index 00000000..7d314111 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/Main.java @@ -0,0 +1,46 @@ +package com.javadiscord.bot; + +import com.javadiscord.bot.utils.chatgpt.ChatGPT; +import com.javadiscord.bot.utils.docker.DockerCommandRunner; +import com.javadiscord.bot.utils.docker.DockerSessions; +import com.javadiscord.bot.utils.jshell.JShellService; +import com.javadiscord.jdi.core.Discord; +import com.javadiscord.jdi.core.annotations.Component; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.core.DockerClientBuilder; + +public class Main { + public static void main(String[] args) { + Discord discord = new Discord(System.getenv("BOT_TOKEN")); + discord.start(); + } + + @Component + public static ChatGPT chatGpt() { + return new ChatGPT(); + } + + @Component + public static JShellService jShellService() { + return new JShellService(); + } + + private static final DockerClient DOCKER_CLIENT = + DockerClientBuilder.getInstance("tcp://localhost:2375").build(); + + @Component + public static DockerClient dockerClient() { + return DOCKER_CLIENT; + } + + @Component + public static DockerCommandRunner dockerCommandRunner() { + return new DockerCommandRunner(DOCKER_CLIENT); + } + + @Component + public static DockerSessions dockerSessions() { + return new DockerSessions(DOCKER_CLIENT); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/ChatGPTCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/ChatGPTCommand.java new file mode 100644 index 00000000..6132f566 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/ChatGPTCommand.java @@ -0,0 +1,63 @@ +package com.javadiscord.bot.commands.slash; + +import java.awt.*; + +import com.javadiscord.bot.utils.chatgpt.ChatGPT; +import com.javadiscord.jdi.core.CommandOptionType; +import com.javadiscord.jdi.core.annotations.CommandOption; +import com.javadiscord.jdi.core.annotations.Inject; +import com.javadiscord.jdi.core.annotations.SlashCommand; +import com.javadiscord.jdi.core.interaction.SlashCommandEvent; +import com.javadiscord.jdi.core.models.message.embed.Embed; +import com.javadiscord.jdi.core.models.message.embed.EmbedAuthor; + +public class ChatGPTCommand { + + @Inject + private ChatGPT chatGPT; + + @SlashCommand( + name = "chatgpt", description = "Ask ChatGPT a question", options = { + @CommandOption( + name = "message", description = "What would you like to ask?", type = CommandOptionType.STRING + ) + } + ) + public void handle(SlashCommandEvent event) { + event.deferReply(); + Thread.ofVirtual().start(() -> handleCommand(event)); + } + + private void handleCommand(SlashCommandEvent event) { + event.option("message").ifPresent(msg -> { + StringBuilder answer = new StringBuilder(); + answer.append(event.user().asMention()); + answer.append(" asked:\n"); + answer.append(msg.valueAsString()); + answer.append("\n"); + answer.append("───────────────\n"); + + chatGPT.ask(msg.valueAsString()) + .ifPresentOrElse( + strings -> { + for (String string : strings) { + answer.append(string).append("\n"); + } + }, + () -> sendChatGptUnavailableMessage(event) + ); + + Embed embed = + new Embed.Builder().color(Color.CYAN) + .description(answer.toString()) + .author( + new EmbedAuthor("", "https://chat.openai.com/favicon-32x32.png", null, null) + ).build(); + event.reply(embed); + }); + } + + private void sendChatGptUnavailableMessage(SlashCommandEvent event) { + event.reply("ChatGPT is currently unavailable."); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/JShellCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/JShellCommand.java new file mode 100644 index 00000000..3fe7e4c6 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/JShellCommand.java @@ -0,0 +1,134 @@ +package com.javadiscord.bot.commands.slash; + +import java.awt.*; + +import com.javadiscord.bot.utils.jshell.JShellResponse; +import com.javadiscord.bot.utils.jshell.JShellService; +import com.javadiscord.bot.utils.jshell.JShellSnippet; +import com.javadiscord.jdi.core.CommandOptionType; +import com.javadiscord.jdi.core.annotations.CommandOption; +import com.javadiscord.jdi.core.annotations.Inject; +import com.javadiscord.jdi.core.annotations.SlashCommand; +import com.javadiscord.jdi.core.interaction.SlashCommandEvent; +import com.javadiscord.jdi.core.models.message.embed.Embed; +import com.javadiscord.jdi.core.models.message.embed.EmbedAuthor; +import com.javadiscord.jdi.core.models.user.User; + +public class JShellCommand { + + @Inject + private JShellService jShellService; + + @SlashCommand( + name = "jshell", description = "Run Java code using JShell", options = { + @CommandOption( + name = "code", description = "The code you would like to execute", type = CommandOptionType.STRING + ) + } + ) + public void handle(SlashCommandEvent event) { + event.deferReply(); + Thread.ofVirtual().start(() -> handleJShell(event)); + } + + private void handleJShell(SlashCommandEvent event) { + User user = event.user(); + long start = System.currentTimeMillis(); + + event.option("code").ifPresent(msg -> { + JShellResponse response = jShellService.sendRequest(msg.valueAsString()); + if (response == null) { + handleNullResponse(event, user); + return; + } + + if (response.error() != null && !response.error().isEmpty()) { + handleErrorResponse(event, user, msg.valueAsString(), response); + return; + } + + handleSuccessResponse(event, user, response, start); + }); + } + + private void handleNullResponse(SlashCommandEvent event, User user) { + String reply = "Failed to execute the provided code, was it bad?"; + Embed embed = + new Embed.Builder() + .author(new EmbedAuthor(user.asMention(), user.avatar(), null, null)) + .description(reply) + .color(Color.ORANGE) + .build(); + event.reply(embed); + } + + private void handleErrorResponse( + SlashCommandEvent event, + User user, + String code, + JShellResponse response + ) { + String reply = + String.format( + """ + An error occurred while executing command: + + ```java + %s + ``` + + %s + """, code, response.error() + ); + Embed embed = + new Embed.Builder() + .author(new EmbedAuthor(user.asMention(), user.avatar(), null, null)) + .description(reply) + .color(Color.RED) + .build(); + event.reply(embed); + } + + private void handleSuccessResponse( + SlashCommandEvent event, + User user, + JShellResponse response, + long start + ) { + StringBuilder sb = new StringBuilder(); + sb.append("## Snippets\n"); + for (JShellSnippet snippet : response.events()) { + sb.append("`").append(snippet.statement()).append("`\n\n"); + sb.append("**Status**: ").append(snippet.status()).append("\n"); + + if (snippet.value() != null && !snippet.value().isEmpty()) { + sb.append("**Output**\n"); + sb.append("```java\n").append(snippet.value()).append("```\n"); + } + } + + appendOutputStreams(sb, response); + + Embed.Builder embed = + new Embed.Builder() + .author(new EmbedAuthor(user.asMention(), null, null, null)) + .description(sb.length() > 4000 ? sb.substring(0, 4000) : sb.toString()) + .color(Color.GREEN) + .footer("Time taken: " + (System.currentTimeMillis() - start) + "ms"); + + event.reply(embed.build()); + } + + private void appendOutputStreams(StringBuilder sb, JShellResponse response) { + if (!response.outputStream().isEmpty()) { + sb.append("## Console Output\n"); + sb.append("```java\n").append(response.outputStream()).append("```\n"); + } + + if (response.errorStream() != null && !response.errorStream().isEmpty()) { + sb.append("## Error Output\n"); + sb.append("```java\n").append(response.errorStream()).append("```\n"); + } + } + +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/LinuxCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/LinuxCommand.java new file mode 100644 index 00000000..31ee7595 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/LinuxCommand.java @@ -0,0 +1,167 @@ +package com.javadiscord.bot.commands.slash; + +import java.awt.*; +import java.io.OutputStream; +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import com.javadiscord.bot.utils.docker.*; +import com.javadiscord.jdi.core.CommandOptionType; +import com.javadiscord.jdi.core.annotations.CommandOption; +import com.javadiscord.jdi.core.annotations.Inject; +import com.javadiscord.jdi.core.annotations.SlashCommand; +import com.javadiscord.jdi.core.interaction.SlashCommandEvent; +import com.javadiscord.jdi.core.models.application.ApplicationCommandOption; +import com.javadiscord.jdi.core.models.message.embed.Embed; +import com.javadiscord.jdi.core.models.message.embed.EmbedAuthor; +import com.javadiscord.jdi.core.models.user.User; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerResponse; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class LinuxCommand { + private static final Logger LOGGER = LogManager.getLogger(LinuxCommand.class); + private static final ScheduledExecutorService EXECUTOR_SERVICE = + Executors.newSingleThreadScheduledExecutor(); + + @Inject + private DockerClient dockerClient; + + @Inject + private DockerSessions dockerSessions; + + @Inject + private DockerCommandRunner commandRunner; + + public LinuxCommand() { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> dockerSessions + .getSessions() + .forEach(dockerSessions::stopContainer) + ) + ); + + EXECUTOR_SERVICE.scheduleAtFixedRate( + new ContainerCleanupTask(dockerSessions), 0, 5, TimeUnit.MINUTES + ); + } + + @SlashCommand( + name = "linux", description = "Run commands in your very own Linux session", options = { + @CommandOption( + name = "code", description = "The command you would like to run", type = CommandOptionType.STRING + ) + } + ) + public void handle(SlashCommandEvent event) { + event.deferReply(); + + Optional codeOption = event.option("code"); + User user = event.user(); + + codeOption.ifPresent(option -> { + LOGGER.info("Running command {}", option.valueAsString()); + + Thread.ofVirtual().start(() -> handleLinuxCommand(event, option.valueAsString(), user)); + }); + } + + private void handleLinuxCommand(SlashCommandEvent event, String command, User member) { + String memberId = String.valueOf(member.id()); + Session session = getSessionForUser(memberId); + try (OutputStream output = commandRunner.sendCommand(session, command)) { + String reply = + """ + Ran command: + ``` + $ %s + ``` + + ```java + %s + ``` + + Session expires in %s + """ + .formatted(command, output, getSessionExpiry(session)); + + Embed embed = + new Embed.Builder() + .author(new EmbedAuthor(member.displayName(), null, null, null)) + .description(shortenOutput(reply)) + .color(Color.RED) + .build(); + + event.reply(embed); + } catch (Exception e) { + LOGGER.error(e); + event.reply("An error occurred: " + e.getMessage()); + } + } + + private String getSessionExpiry(Session session) { + Instant expiry = session.getStartTime().plusSeconds(TimeUnit.MINUTES.toSeconds(5)); + long epochSeconds = expiry.getEpochSecond(); + return ""; + } + + private String shortenOutput(String input) { + String concatMessage = "\n**Rest of the output as been removed as it was too long**\n"; + if (input.length() > 4096) { + input = input.substring(0, 4096 - concatMessage.length()) + concatMessage; + } + StringBuilder sb = new StringBuilder(); + String[] parts = input.split("\n"); + if (parts.length > 50) { + for (int i = 50; i > 0; i--) { + sb.append(parts[i]).append("\n"); + } + sb.append(concatMessage); + } else { + sb.append(input); + } + return sb.toString(); + } + + private Session getSessionForUser(String name) { + if (!dockerSessions.hasSession(name)) { + + LOGGER.info("Creating new session for {}", name); + + DockerContainerCreator containerCreator = new DockerContainerCreator(dockerClient); + + CreateContainerResponse createContainerResponse = + containerCreator.createContainerStarted( + "session-" + ThreadLocalRandom.current().nextInt(), + "ubuntu:latest", + mb(256), + mb(256), + 512, + 100000, + cpuQuota(100000, 0.5) + ); + + return dockerSessions.createSession(name, createContainerResponse.getId()); + } + + LOGGER.info("Found existing session for {}", name); + + return dockerSessions.getSessionForUser(name); + } + + public static long mb(long megabytes) { + return megabytes * 1024 * 1024; + } + + public static long cpuQuota(int cpuPeriod, double percentage) { + return (long) (cpuPeriod * (percentage / 10.0)); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/PingSlashCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/PingSlashCommand.java new file mode 100644 index 00000000..3983ad08 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/slash/PingSlashCommand.java @@ -0,0 +1,14 @@ +package com.javadiscord.bot.commands.slash; + +import com.javadiscord.jdi.core.annotations.SlashCommand; +import com.javadiscord.jdi.core.interaction.SlashCommandEvent; + +public class PingSlashCommand { + + @SlashCommand( + name = "ping", description = "Pong!" + ) + public void ping(SlashCommandEvent event) { + event.reply("Pong!"); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/TextCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/TextCommand.java new file mode 100644 index 00000000..4de04381 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/TextCommand.java @@ -0,0 +1,8 @@ +package com.javadiscord.bot.commands.text; + +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.models.message.Message; + +public interface TextCommand { + void handle(Guild guild, Message message, String input); +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/TextCommandRepository.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/TextCommandRepository.java new file mode 100644 index 00000000..ff313fbc --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/TextCommandRepository.java @@ -0,0 +1,25 @@ +package com.javadiscord.bot.commands.text; + +import java.util.HashMap; +import java.util.Map; + +import com.javadiscord.bot.commands.text.impl.*; + +public class TextCommandRepository { + private static final Map COMMANDS = new HashMap<>(); + static { + COMMANDS.put("clear", new ClearChannelCommand()); + COMMANDS.put("mute", new MuteCommand()); + COMMANDS.put("unmute", new UnmuteCommand()); + COMMANDS.put("say", new SayCommand()); + COMMANDS.put("embed", new SayEmbedCommand()); + } + + private TextCommandRepository() { + throw new UnsupportedOperationException("Utility class"); + } + + public static TextCommand get(String key) { + return COMMANDS.get(key); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/ClearChannelCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/ClearChannelCommand.java new file mode 100644 index 00000000..3b34dab9 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/ClearChannelCommand.java @@ -0,0 +1,39 @@ +package com.javadiscord.bot.commands.text.impl; + +import java.util.List; + +import com.javadiscord.bot.commands.text.TextCommand; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.api.builders.FetchChannelMessagesBuilder; +import com.javadiscord.jdi.core.models.message.Message; + +public class ClearChannelCommand implements TextCommand { + @Override + public void handle(Guild guild, Message message, String input) { + int limit; + try { + limit = Integer.parseInt(input); + if (limit <= 1) { + limit = 2; + } + if (limit > 100) { + limit = 100; + } + } catch (Exception e) { + limit = 10; + } + + guild.channel() + .fetchChannelMessages(new FetchChannelMessagesBuilder(message.channelId(), limit)) + .onSuccess(messages -> { + + List ids = + messages.stream() + .map(Message::id) + .toList(); + + guild.channel().bulkDeleteMessages(message.channelId(), ids); + }); + + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/MuteCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/MuteCommand.java new file mode 100644 index 00000000..e3ee603c --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/MuteCommand.java @@ -0,0 +1,21 @@ +package com.javadiscord.bot.commands.text.impl; + +import com.javadiscord.bot.commands.text.TextCommand; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.models.guild.Role; +import com.javadiscord.jdi.core.models.message.Message; + +public class MuteCommand implements TextCommand { + + @Override + public void handle(Guild guild, Message message, String input) { + guild.guild().guildRoles().onSuccess(roles -> { + for (Role role : roles) { + if (role.name().equals("Muted")) { + guild.guild().addGuildMemberRole(message.author().id(), role.id()); + break; + } + } + }); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/SayCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/SayCommand.java new file mode 100644 index 00000000..c9907556 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/SayCommand.java @@ -0,0 +1,14 @@ +package com.javadiscord.bot.commands.text.impl; + +import com.javadiscord.bot.commands.text.TextCommand; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.api.builders.CreateMessageBuilder; +import com.javadiscord.jdi.core.models.message.Message; + +public class SayCommand implements TextCommand { + + @Override + public void handle(Guild guild, Message message, String input) { + guild.channel().createMessage(new CreateMessageBuilder(message.channelId()).content(input)); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/SayEmbedCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/SayEmbedCommand.java new file mode 100644 index 00000000..a16a13c5 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/SayEmbedCommand.java @@ -0,0 +1,18 @@ +package com.javadiscord.bot.commands.text.impl; + +import com.javadiscord.bot.commands.text.TextCommand; +import com.javadiscord.bot.listeners.TextCommandListener; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.api.builders.CreateMessageBuilder; +import com.javadiscord.jdi.core.models.message.Message; + +public class SayEmbedCommand implements TextCommand { + + @Override + public void handle(Guild guild, Message message, String input) { + guild.channel().createMessage( + new CreateMessageBuilder(message.channelId()) + .embeds(TextCommandListener.create("", input, "")) + ); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/UnmuteCommand.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/UnmuteCommand.java new file mode 100644 index 00000000..a7601910 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/commands/text/impl/UnmuteCommand.java @@ -0,0 +1,21 @@ +package com.javadiscord.bot.commands.text.impl; + +import com.javadiscord.bot.commands.text.TextCommand; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.models.guild.Role; +import com.javadiscord.jdi.core.models.message.Message; + +public class UnmuteCommand implements TextCommand { + + @Override + public void handle(Guild guild, Message message, String input) { + guild.guild().guildRoles().onSuccess(roles -> { + for (Role role : roles) { + if (role.name().equals("Muted")) { + guild.guild().removeGuildMemberRole(message.author().id(), role.id()); + break; + } + } + }); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/QuestionListener.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/QuestionListener.java new file mode 100644 index 00000000..fe8b4469 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/QuestionListener.java @@ -0,0 +1,93 @@ +package com.javadiscord.bot.listeners; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +import com.javadiscord.bot.utils.chatgpt.ChatGPT; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.annotations.EventListener; +import com.javadiscord.jdi.core.annotations.Inject; +import com.javadiscord.jdi.core.annotations.MessageCreate; +import com.javadiscord.jdi.core.annotations.ThreadCreate; +import com.javadiscord.jdi.core.models.message.Message; +import com.javadiscord.jdi.core.models.message.embed.Embed; +import com.javadiscord.jdi.core.models.message.embed.EmbedAuthor; +import com.javadiscord.jdi.internal.gateway.handlers.events.codec.models.channel.Thread; + +@EventListener +public class QuestionListener { + private final List channelsThatNeedChatGpt = new ArrayList<>(); + + @Inject + private ChatGPT chatGPT; + + @ThreadCreate + public void onQuestionCreate(Thread thread, Guild guild) { + if (thread.newlyCreated() && thread.parentId() == 1245064991275618511L) { + + guild.channel().sendEmbed( + thread.id(), new Embed.Builder() + .description( + """ + # Important + Please make sure your question has enough details for a helper to understand the problem. + + * If you are asking for help with code, please use a code block. + * If you are asking for help with an error, please include the full error message. + * Screenshots may also be useful. Please do not post screenshots of code, however. + """ + ) + .image( + "https://media.tenor.com/LoNa2zOMxoAAAAAC/its-very-important-it-matters.gif" + ) + .build() + ); + + guild.channel().sendEmbed( + thread.id(), new Embed.Builder() + .description( + """ + Once your question has been answered, please close this thread by doing `/close`. + """ + ).build() + ); + + channelsThatNeedChatGpt.add(thread.id()); + } + } + + @MessageCreate + public void sendChatGptAnswer(Message message, Guild guild) { + if (channelsThatNeedChatGpt.contains(message.id())) { + channelsThatNeedChatGpt.remove(message.id()); + + StringBuilder answer = new StringBuilder(); + answer.append("## Here is an attempted answer by ChatGPT\n\n"); + + chatGPT.ask(message.content()) + .ifPresentOrElse( + strings -> { + for (String string : strings) { + answer.append(string).append("\n"); + } + }, + () -> guild.channel().sendMessage( + message.channelId(), + "ChatGPT is currently unavailable." + ) + ); + + Embed embed = + new Embed.Builder() + .author( + new EmbedAuthor("", null, "https://chat.openai.com/favicon-32x32.png", null) + ) + .color(Color.CYAN) + .description(answer.toString()) + .build(); + + guild.channel().sendEmbed(message.channelId(), embed); + } + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/RolePlayMessageListener.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/RolePlayMessageListener.java new file mode 100644 index 00000000..49632f8e --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/RolePlayMessageListener.java @@ -0,0 +1,75 @@ +package com.javadiscord.bot.listeners; + +import java.awt.*; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import com.javadiscord.bot.utils.Tenor; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.annotations.EventListener; +import com.javadiscord.jdi.core.annotations.MessageCreate; +import com.javadiscord.jdi.core.api.builders.CreateMessageBuilder; +import com.javadiscord.jdi.core.models.message.Message; +import com.javadiscord.jdi.core.models.message.embed.Embed; +import com.javadiscord.jdi.core.models.user.User; + +import com.fasterxml.jackson.databind.JsonNode; + +@EventListener +public class RolePlayMessageListener { + + @MessageCreate + public void handleRolePlay(Message message, Guild guild) { + if (containsRolePlayAction(message.content())) { + String content = message.content(); + String[] split = content.split("-"); + String action = split[1].trim(); + + List mentions = message.mentions(); + + if (!mentions.isEmpty()) { + String from = message.author().asMention(); + StringBuilder names = new StringBuilder(); + mentions.forEach( + m -> { + names.append(m.asMention()); + names.append(" "); + } + ); + + if (names.toString().trim().equals("**")) { + return; + } + + String searchTerm = action.replace(" ", "%20") + "ing%20anime"; + JsonNode json = Tenor.search(searchTerm, 50); + + if (json != null && json.has("results")) { + JsonNode results = json.get("results"); + JsonNode result = + results.get(ThreadLocalRandom.current().nextInt(results.size())); + JsonNode media = result.get("media").get(0); + JsonNode gif = media.get("gif"); + String url = gif.get("url").asText(); + + Embed embed = + new Embed.Builder() + .description("**" + from + "** " + action + " **" + names + "**") + .image(url) + .color(Color.RED) + .build(); + + guild.channel().createMessage( + new CreateMessageBuilder(message.channelId()) + .embeds(embed) + ); + } + } + + } + } + + private static boolean containsRolePlayAction(String message) { + return message.matches(".*-.*-.*") && message.startsWith("-"); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SlashCommandListener.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SlashCommandListener.java new file mode 100644 index 00000000..399f11bd --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SlashCommandListener.java @@ -0,0 +1,27 @@ +package com.javadiscord.bot.listeners; + +import com.javadiscord.jdi.core.annotations.EventListener; +import com.javadiscord.jdi.core.annotations.InteractionCreate; +import com.javadiscord.jdi.core.models.guild.Interaction; +import com.javadiscord.jdi.core.models.user.Member; +import com.javadiscord.jdi.core.models.user.User; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@EventListener +public class SlashCommandListener { + private static final Logger LOGGER = LogManager.getLogger(SlashCommandListener.class); + + @InteractionCreate + public void slashCommandLogger(Interaction interaction) { + Member member = interaction.member(); + User user = member.user(); + + LOGGER.info( + "{} used /{} in {}", user.displayName(), interaction.data().name(), + interaction.channel().name() + ); + } + +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SpamListener.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SpamListener.java new file mode 100644 index 00000000..3d052466 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SpamListener.java @@ -0,0 +1,37 @@ +package com.javadiscord.bot.listeners; + +import com.javadiscord.bot.utils.CurseWords; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.annotations.EventListener; +import com.javadiscord.jdi.core.annotations.MessageCreate; +import com.javadiscord.jdi.core.api.builders.CreateMessageBuilder; +import com.javadiscord.jdi.core.models.message.Message; + +@EventListener +public class SpamListener { + + @MessageCreate + public void onMessage(Message message, Guild guild) { + if (message.fromUser() && CurseWords.containsCurseWord(message.content())) { + guild.channel().deleteMessage(message.channelId(), message.id()); + guild.user() + .createDM(message.id()) + .onSuccess( + channel -> guild.channel() + .createMessage( + new CreateMessageBuilder(channel.id()).content( + """ + Your message has been removed for containing words blacklisted by this server! + Please avoid sending such messages in the future. Thank you. + + The message you sent: + > %s + """ + .formatted(message.content()) + ) + ) + ); + } + } + +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SuggestionListener.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SuggestionListener.java new file mode 100644 index 00000000..682441c1 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/SuggestionListener.java @@ -0,0 +1,36 @@ +package com.javadiscord.bot.listeners; + +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.annotations.EventListener; +import com.javadiscord.jdi.core.annotations.MessageCreate; +import com.javadiscord.jdi.core.api.builders.StartThreadFromMessageBuilder; +import com.javadiscord.jdi.core.models.message.Message; + +@EventListener +public class SuggestionListener { + + @MessageCreate + public void onMessage(Message message, Guild guild) { + if (message.fromBot()) { + return; + } + + if (message.channelId() != 1244690778505216154L) { + return; + } + + String title = + message.content().length() > 60 + ? message.content().substring(0, 60) + : message.content(); + + guild.channel().startThreadFromMessage( + new StartThreadFromMessageBuilder( + message.channelId(), + message.id(), + title + ) + ); + } + +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/TextCommandListener.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/TextCommandListener.java new file mode 100644 index 00000000..0354057c --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/listeners/TextCommandListener.java @@ -0,0 +1,45 @@ +package com.javadiscord.bot.listeners; + +import java.awt.*; + +import com.javadiscord.bot.commands.text.TextCommand; +import com.javadiscord.bot.commands.text.TextCommandRepository; +import com.javadiscord.jdi.core.Guild; +import com.javadiscord.jdi.core.annotations.EventListener; +import com.javadiscord.jdi.core.annotations.MessageCreate; +import com.javadiscord.jdi.core.models.message.Message; +import com.javadiscord.jdi.core.models.message.embed.Embed; +import com.javadiscord.jdi.core.models.message.embed.EmbedAuthor; + +@EventListener +public class TextCommandListener { + + @MessageCreate + public void handleTextCommand(Message message, Guild guild) { + if (!message.author().bot()) { + String msg = message.content(); + if (msg.startsWith("!")) { + String cmd = msg.split(" ")[0].replace("!", "").trim(); + String input = msg.replace(String.format("!%s", cmd), "").trim(); + TextCommand command = TextCommandRepository.get(cmd); + if (command != null) { + command.handle(guild, message, input); + guild.channel().deleteMessage(message.channelId(), message.id()); + } else { + System.err.println("Command not found."); + } + } + } + } + + public static Embed create(String title, String caption, String imageURL) { + Embed.Builder eb = new Embed.Builder(); + if (!imageURL.isEmpty()) { + eb.image(imageURL); + } + eb.color(Color.RED); + eb.author(new EmbedAuthor(title, null, null, null)); + eb.description(caption); + return eb.build(); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/CurseWords.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/CurseWords.java new file mode 100644 index 00000000..27c7c0de --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/CurseWords.java @@ -0,0 +1,1245 @@ +package com.javadiscord.bot.utils; + +import java.util.Arrays; +import java.util.List; + +public class CurseWords { + private CurseWords() { + throw new UnsupportedOperationException("Utility class"); + } + + public static boolean containsCurseWord(String text) { + for (String s : CURSE_WORDS) { + if (text.toLowerCase().contains(s.toLowerCase())) { + return true; + } + } + return false; + } + + private static final List CURSE_WORDS = + Arrays.asList( + "fuck", + "shit", + "asshole", + "bitch", + "bastard", + "damn", + "hell", + "dick", + "pussy", + "cunt", + "ass", + "cock", + "motherfucker", + "bullshit", + "son of a bitch", + "allah", + "anal", + "anus", + "aroused", + "arse", + "arsehole", + "ass", + "assassinate", + "assassination", + "babe", + "babies", + "balllicker", + "ballsack", + "banging", + "baptist", + "barelylegal", + "bastard ", + "beastality", + "beastial", + "beastiality", + "beatyourmeat", + "bestial", + "bestiality", + "bi", + "biatch", + "bicurious", + "bigass", + "bigbastard", + "bigbutt", + "bigger", + "bisexual", + "bi-sexual", + "bitch", + "blow", + "blowjob", + "bollick", + "bollock", + "bondage", + "boner", + "bong", + "boob", + "boong", + "boonga", + "boonie", + "booty", + "bootycall", + "bountybar", + "bra", + "brea5t", + "breast", + "breastjob", + "breastlover", + "breastman", + "brothel", + "bugger", + "buggered", + "buggery", + "bullcrap", + "bulldike", + "bulldyke", + "bullshit", + "bumblefuck", + "bumfuck", + "bunga", + "bunghole", + "buried", + "burn", + "butchbabes", + "butchdike", + "butchdyke", + "buttbang", + "butt-bang", + "buttface", + "buttfuck", + "butt-fuck", + "buttfucker", + "butt-fucker", + "buttfuckers", + "butt-fuckers", + "butthead", + "buttman", + "buttmunch", + "buttmuncher", + "buttpirate", + "buttplug", + "buttstain", + "byatch", + "cameltoe", + "cancer", + "carpetmuncher", + "carruth", + "catholic", + "catholics", + "cemetery", + "cherrypopper", + "chinaman", + "chinamen", + "chinese", + "chink", + "chinky", + "choad", + "chode", + "cigarette", + "cigs", + "clamdigger", + "clamdiver", + "clit", + "clitoris", + "clogwog", + "cocaine", + "cock", + "cockblock", + "cockblocker", + "cockcowboy", + "cockfight", + "cockhead", + "cockknob", + "cocklicker", + "cocklover", + "cocknob", + "cockqueen", + "cockrider", + "cocksman", + "cocksmith", + "cocksmoker", + "cocksucer", + "cocksuck ", + "cocksucked ", + "cocksucker", + "cocksucking", + "cocktail", + "cocktease", + "cocky", + "cohee", + "coitus", + "color", + "colored", + "coloured", + "commie", + "communist", + "condom", + "conservative", + "conspiracy", + "coolie", + "cooly", + "coon", + "coondog", + "copulate", + "cornhole", + "corruption", + "cra5h", + "crabs", + "crack", + "crackpipe", + "crackwhore", + "crack-whore", + "crap", + "crapola", + "crapper", + "crappy", + "crash", + "creamy", + "crime", + "crimes", + "criminal", + "criminals", + "crotch", + "crotchjockey", + "crotchmonkey", + "crotchrot", + "cum", + "cumbubble", + "cumfest", + "cumjockey", + "cumm", + "cummer", + "cumming", + "cumquat", + "cumqueen", + "cumshot", + "cunilingus", + "cunillingus", + "cunn", + "cunnilingus", + "cunntt", + "cunt", + "cunteyed", + "cuntfuck", + "cuntfucker", + "cuntlick ", + "cuntlicker ", + "cuntlicking ", + "cuntsucker", + "cybersex", + "damnation", + "nigga", + "dead", + "deapthroat", + "death", + "deepthroat", + "defecate", + "dego", + "demon", + "deposit", + "desire", + "destroy", + "deth", + "devil", + "devilworshipper", + "dick", + "dickbrain", + "dickforbrains", + "dickhead", + "dickless", + "dicklick", + "dicklicker", + "dickman", + "dickwad", + "dickweed", + "diddle", + "dike", + "dildo", + "dingleberry", + "dink", + "dipshit", + "dipstick", + "dirty", + "disease", + "diseases", + "disturbed", + "dive", + "dix", + "dixiedike", + "dixiedyke", + "doggiestyle", + "doggystyle", + "dripdick", + "drug", + "dumbbitch", + "dumbfuck", + "dyefly", + "dyke", + "easyslut", + "eatballs", + "eatme", + "eatpussy", + "ecstacy", + "ejaculate", + "ejaculated", + "ejaculating ", + "ejaculation", + "erect", + "erection", + "escort", + "ethiopian", + "ethnic", + "european", + "explosion", + "facefucker", + "faeces", + "fag", + "fagging", + "faggot", + "fagot", + "failed", + "failure", + "fairies", + "fairy", + "faith", + "fannyfucker", + "fart", + "farted ", + "farting ", + "farty ", + "fastfuck", + "fatah", + "fatass", + "fatfuck", + "fatfucker", + "fatso", + "fckcum", + "feces", + "felatio ", + "felch", + "felcher", + "felching", + "fellatio", + "feltch", + "feltcher", + "feltching", + "fetish", + "fingerfood", + "fingerfuck ", + "fingerfucked ", + "fingerfucker ", + "fingerfuckers", + "fingerfucking ", + "fister", + "fistfuck", + "fistfucked ", + "fistfucker ", + "fistfucking ", + "fisting", + "flange", + "flasher", + "flatulence", + "floo", + "flydie", + "flydye", + "fok", + "fondle", + "footaction", + "footfuck", + "footfucker", + "footlicker", + "footstar", + "fore", + "foreskin", + "forni", + "fornicate", + "foursome", + "fourtwenty", + "fraud", + "freakfuck", + "freakyfucker", + "freefuck", + "fu", + "fubar", + "fuc", + "fucck", + "fuck", + "fucka", + "fuckable", + "fuckbag", + "fuckbuddy", + "fucked", + "fuckedup", + "fucker", + "fuckers", + "fuckface", + "fuckfest", + "fuckfreak", + "fuckfriend", + "fuckhead", + "fuckher", + "fuckin", + "fuckina", + "fucking", + "fuckingbitch", + "fuckinnuts", + "fuckinright", + "fuckit", + "fuckknob", + "fuckme ", + "fuckmehard", + "fuckmonkey", + "fuckoff", + "fuckpig", + "fucks", + "fucktard", + "fuckwhore", + "fuckyou", + "fudgepacker", + "fugly", + "fuk", + "fuks", + "funeral", + "funfuck", + "fungus", + "fuuck", + "gangbang", + "gangbanged ", + "gangbanger", + "gangsta", + "gatorbait", + "gay", + "gaymuthafuckinwhore", + "gaysex ", + "geez", + "geezer", + "geni", + "genital", + "german", + "getiton", + "gin", + "ginzo", + "gipp", + "girls", + "givehead", + "glazeddonut", + "gob", + "god", + "godammit", + "goddamit", + "goddammit", + "goddamn", + "goddamned", + "goddamnes", + "goddamnit", + "goddamnmuthafucker", + "goldenshower", + "gonorrehea", + "gonzagas", + "gook", + "gotohell", + "goy", + "goyim", + "greaseball", + "gringo", + "groe", + "gross", + "grostulation", + "gubba", + "gummer", + "gun", + "gyp", + "gypo", + "gypp", + "gyppie", + "gyppo", + "gyppy", + "hamas", + "handjob", + "hapa", + "harder", + "hardon", + "harem", + "headfuck", + "headlights", + "hebe", + "heeb", + "hell", + "henhouse", + "heroin", + "herpes", + "heterosexual", + "hijack", + "hijacker", + "hijacking", + "hillbillies", + "hindoo", + "hiscock", + "hitler", + "hitlerism", + "hitlerist", + "hiv", + "ho", + "hobo", + "hodgie", + "hoes", + "hole", + "holestuffer", + "homicide", + "homo", + "homobangers", + "homosexual", + "honger", + "honk", + "honkers", + "honkey", + "honky", + "hook", + "hooker", + "hookers", + "hooters", + "hore", + "hork", + "horn", + "horney", + "horniest", + "horny", + "horseshit", + "hosejob", + "hoser", + "hostage", + "hotdamn", + "hotpussy", + "hottotrot", + "hummer", + "husky", + "hussy", + "hustler", + "hymen", + "hymie", + "iblowu", + "idiot", + "ikey", + "illegal", + "incest", + "insest", + "intercourse", + "interracial", + "intheass", + "inthebuff", + "israel", + "israeli", + "israel's", + "italiano", + "itch", + "jackass", + "jackoff", + "jackshit", + "jacktheripper", + "jade", + "jap", + "japanese", + "japcrap", + "jebus", + "jeez", + "jerkoff", + "jesus", + "jesuschrist", + "jew", + "jewish", + "jiga", + "jigaboo", + "jigg", + "jigga", + "jiggabo", + "jigger ", + "jiggy", + "jihad", + "jijjiboo", + "jimfish", + "jism", + "jiz ", + "jizim", + "jizjuice", + "jizm ", + "jizz", + "jizzim", + "jizzum", + "joint", + "juggalo", + "jugs", + "junglebunny", + "kaffer", + "kaffir", + "kaffre", + "kafir", + "kanake", + "kid", + "kigger", + "kike", + "kill", + "killed", + "killer", + "killing", + "kills", + "kink", + "kinky", + "kissass", + "kkk", + "knife", + "knockers", + "kock", + "kondum", + "koon", + "kotex", + "krap", + "krappy", + "kraut", + "kum", + "kumbubble", + "kumbullbe", + "kummer", + "kumming", + "kumquat", + "kums", + "kunilingus", + "kunnilingus", + "kunt", + "ky", + "kyke", + "lactate", + "laid", + "lapdance", + "latin", + "lesbain", + "lesbayn", + "lesbian", + "lesbin", + "lesbo", + "lez", + "lezbe", + "lezbefriends", + "lezbo", + "lezz", + "lezzo", + "liberal", + "libido", + "licker", + "lickme", + "lies", + "limey", + "limpdick", + "limy", + "lingerie", + "liquor", + "livesex", + "loadedgun", + "lolita", + "looser", + "loser", + "lotion", + "lovebone", + "lovegoo", + "lovegun", + "lovejuice", + "lovemuscle", + "lovepistol", + "loverocket", + "lowlife", + "lsd", + "lubejob", + "lucifer", + "luckycammeltoe", + "lugan", + "lynch", + "macaca", + "mad", + "mafia", + "magicwand", + "mams", + "manhater", + "manpaste", + "marijuana", + "mastabate", + "mastabater", + "masterbate", + "masterblaster", + "mastrabator", + "masturbate", + "masturbating", + "mattressprincess", + "meatbeatter", + "meatrack", + "meth", + "mexican", + "mgger", + "mggor", + "mickeyfinn", + "mideast", + "milf", + "minority", + "mockey", + "mockie", + "mocky", + "mofo", + "moky", + "moles", + "molest", + "molestation", + "molester", + "molestor", + "moneyshot", + "mooncricket", + "mormon", + "moron", + "moslem", + "mosshead", + "mothafuck", + "mothafucka", + "mothafuckaz", + "mothafucked ", + "mothafucker", + "mothafuckin", + "mothafucking ", + "mothafuckings", + "motherfuck", + "motherfucked", + "motherfucker", + "motherfuckin", + "motherfucking", + "motherfuckings", + "motherlovebone", + "muff", + "muffdive", + "muffdiver", + "muffindiver", + "mufflikcer", + "mulatto", + "muncher", + "munt", + "murder", + "murderer", + "muslim", + "naked", + "narcotic", + "nasty", + "nastybitch", + "nastyho", + "nastyslut", + "nastywhore", + "nazi", + "necro", + "negro", + "negroes", + "negroid", + "negro's", + "nig", + "niger", + "nigerian", + "nigerians", + "nigg", + "nigga", + "niggah", + "niggaracci", + "niggard", + "niggarded", + "niggarding", + "niggardliness", + "niggardliness's", + "niggardly", + "niggards", + "niggard's", + "niggaz", + "nigger", + "niggerhead", + "niggerhole", + "niggers", + "nigger's", + "niggle", + "niggled", + "niggles", + "niggling", + "nigglings", + "niggor", + "niggur", + "niglet", + "nignog", + "nigr", + "nigra", + "nigre", + "nip", + "nipple", + "nipplering", + "nittit", + "nlgger", + "nlggor", + "nofuckingway", + "nook", + "nookey", + "nookie", + "noonan", + "nooner", + "nude", + "nudger", + "nuke", + "nutfucker", + "nymph", + "ontherag", + "oral", + "orga", + "orgasim ", + "orgasm", + "orgies", + "orgy", + "osama", + "paki", + "palesimian", + "palestinian", + "pansies", + "pansy", + "panti", + "panties", + "payo", + "pearlnecklace", + "peck", + "pecker", + "peckerwood", + "pee", + "peehole", + "pee-pee", + "peepshow", + "peepshpw", + "pendy", + "penetration", + "peni5", + "penile", + "penis", + "penises", + "penthouse", + "period", + "perv", + "phonesex", + "phuk", + "phuked", + "phuking", + "phukked", + "phukking", + "phungky", + "phuq", + "pi55", + "picaninny", + "piccaninny", + "pickaninny", + "piker", + "pikey", + "piky", + "pimp", + "pimped", + "pimper", + "pimpjuic", + "pimpjuice", + "pimpsimp", + "pindick", + "piss", + "pissed", + "pisser", + "pisses ", + "pisshead", + "pissin ", + "pissing", + "pissoff ", + "pistol", + "pixie", + "pixy", + "playboy", + "playgirl", + "pocha", + "pocho", + "pocketpool", + "pohm", + "polack", + "pom", + "pommie", + "pommy", + "poo", + "poon", + "poontang", + "poop", + "pooper", + "pooperscooper", + "pooping", + "poorwhitetrash", + "popimp", + "porchmonkey", + "porn", + "pornflick", + "pornking", + "porno", + "pornography", + "pornprincess", + "pot", + "poverty", + "premature", + "pric", + "prick", + "prickhead", + "primetime", + "propaganda", + "pros", + "prostitute", + "protestant", + "pu55i", + "pu55y", + "pube", + "pubic", + "pubiclice", + "pud", + "pudboy", + "pudd", + "puddboy", + "puke", + "puntang", + "purinapricness", + "puss", + "pussie", + "pussies", + "pussy", + "pussycat", + "pussyeater", + "pussyfucker", + "pussylicker", + "pussylips", + "pussylover", + "pussypounder", + "pusy", + "quashie", + "queef", + "queer", + "quickie", + "quim", + "ra8s", + "rabbi", + "racial", + "racist", + "radical", + "radicals", + "raghead", + "randy", + "rape", + "raped", + "raper", + "rapist", + "rearend", + "rearentry", + "rectum", + "redlight", + "redneck", + "reefer", + "reestie", + "refugee", + "reject", + "remains", + "rentafuck", + "republican", + "rere", + "retard", + "retarded", + "ribbed", + "rigger", + "rimjob", + "rimming", + "roach", + "robber", + "roundeye", + "rump", + "russki", + "russkie", + "sadis", + "sadom", + "samckdaddy", + "sandm", + "sandnigger", + "satan", + "scag", + "scallywag", + "scat", + "schlong", + "screw", + "screwyou", + "scrotum", + "scum", + "semen", + "seppo", + "servant", + "sex", + "sexed", + "sexfarm", + "sexhound", + "sexhouse", + "sexing", + "sexkitten", + "sexpot", + "sexslave", + "sextogo", + "sextoy", + "sextoys", + "sexual", + "sexually", + "sexwhore", + "sexy", + "sexymoma", + "sexy-slim", + "shag", + "shaggin", + "shagging", + "shat", + "shav", + "shawtypimp", + "sheeney", + "shhit", + "shinola", + "shit", + "shitcan", + "shitdick", + "shite", + "shiteater", + "shited", + "shitface", + "shitfaced", + "shitfit", + "shitforbrains", + "shitfuck", + "shitfucker", + "shitfull", + "shithapens", + "shithappens", + "shithead", + "shithouse", + "shiting", + "shitlist", + "shitola", + "shitoutofluck", + "shits", + "shitstain", + "shitted", + "shitter", + "shitting", + "shitty ", + "shoot", + "shooting", + "shortfuck", + "showtime", + "sick", + "sissy", + "sixsixsix", + "sixtynine", + "sixtyniner", + "skank", + "skankbitch", + "skankfuck", + "skankwhore", + "skanky", + "skankybitch", + "skankywhore", + "skinflute", + "skum", + "skumbag", + "slant", + "slanteye", + "slapper", + "slaughter", + "slav", + "slave", + "slavedriver", + "sleezebag", + "sleezeball", + "slideitin", + "slime", + "slimeball", + "slimebucket", + "slopehead", + "slopey", + "slopy", + "slut", + "sluts", + "slutt", + "slutting", + "slutty", + "slutwear", + "slutwhore", + "smack", + "smackthemonkey", + "smut", + "snatch", + "snatchpatch", + "snigger", + "sniggered", + "sniggering", + "sniggers", + "snigger's", + "sniper", + "snot", + "snowback", + "snownigger", + "sob", + "sodom", + "sodomise", + "sodomite", + "sodomize", + "sodomy", + "sonofabitch", + "sonofbitch", + "sooty", + "sos", + "soviet", + "spaghettibender", + "spaghettinigger", + "spank", + "spankthemonkey", + "sperm", + "spermacide", + "spermbag", + "spermhearder", + "spermherder", + "spic", + "spick", + "spig", + "spigotty", + "spik", + "spit", + "spitter", + "splittail", + "spooge", + "spreadeagle", + "spunk", + "spunky", + "squaw", + "stagg", + "stiffy", + "strapon", + "stringer", + "stripclub", + "dick", + "suicide", + "swallow", + "swastika", + "syphilis", + "taboo", + "tampon", + "tang", + "tantra", + "tarbaby", + "tard", + "teat", + "terror", + "terrorist", + "teste", + "testicle", + "testicles", + "thicklips", + "thirdeye", + "thirdleg", + "threesome", + "threeway", + "timbernigger", + "tinkle", + "tit", + "titbitnipply", + "titfuck", + "titfucker", + "titfuckin", + "titjob", + "titlicker", + "titlover", + "tits", + "tittie", + "titties", + "titty", + "tnt", + "toilet", + "tongethruster", + "tonguethrust", + "tonguetramp", + "tortur", + "torture", + "tosser", + "towelhead", + "trailertrash", + "tramp", + "trannie", + "tranny", + "transexual", + "transsexual", + "transvestite", + "triplex", + "trisexual", + "trojan", + "trots", + "tuckahoe", + "tunneloflove", + "turd", + "turnon", + "twat", + "twink", + "twinkie", + "twobitwhore", + "uck", + "uk", + "unfuckable", + "upskirt", + "uptheass", + "upthebutt", + "urinary", + "urinate", + "urine", + "usama", + "uterus", + "vagina", + "vaginal", + "vatican", + "vibr", + "vibrater", + "vibrator", + "vietcong", + "violence", + "virgin", + "vulva", + "wank", + "wanker", + "wanking", + "waysted", + "weapon", + "weenie", + "weewee", + "welcher", + "welfare", + "wetb", + "wetback", + "wetspot", + "whacker", + "whash", + "whigger", + "whitenigger", + "whitetrash", + "whitey", + "whiz", + "whop", + "whore", + "wigger", + "willie", + "williewanker", + "willy", + "wn", + "wog", + "wop", + "wuss", + "wuzzie", + "xtc", + "xxx", + "yankee", + "yellowman", + "zigabo", + "zipperhead" + ); +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/Executor.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/Executor.java new file mode 100644 index 00000000..da8b8b2c --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/Executor.java @@ -0,0 +1,26 @@ +package com.javadiscord.bot.utils; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class Executor { + private static final ScheduledExecutorService EXECUTOR_SERVICE = + Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); + + private Executor() { + throw new UnsupportedOperationException("Utility class"); + } + + public static void execute(Runnable runnable) { + EXECUTOR_SERVICE.submit(runnable); + } + + public static void run(Runnable runnable, int period, TimeUnit timeUnit) { + EXECUTOR_SERVICE.scheduleAtFixedRate(runnable, 0, period, timeUnit); + } + + public static void run(Runnable runnable, int delay, int period, TimeUnit timeUnit) { + EXECUTOR_SERVICE.scheduleAtFixedRate(runnable, delay, period, timeUnit); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/Tenor.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/Tenor.java new file mode 100644 index 00000000..c5b035b5 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/Tenor.java @@ -0,0 +1,55 @@ +package com.javadiscord.bot.utils; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Tenor { + private static final Logger LOGGER = LogManager.getLogger(Tenor.class); + private static final String API_KEY = System.getenv("TENOR_API_KEY"); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + + private Tenor() { + throw new UnsupportedOperationException("Utility class"); + } + + public static JsonNode search(String searchTerm, int limit) { + final String url = + String.format( + "https://api.tenor.com/v1/search?q=%1$s&key=%2$s&limit=%3$s", + searchTerm, API_KEY, limit + ); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .build(); + + try { + HttpResponse response = + HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + return OBJECT_MAPPER.readTree(response.body()); + } else { + LOGGER.trace("HTTP Code: {} from {}", response.statusCode(), url); + } + + } catch (IOException | InterruptedException e) { + LOGGER.error("Error making a request to Tenor", e); + Thread.currentThread().interrupt(); + } + + return null; + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/chatgpt/ChatGPT.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/chatgpt/ChatGPT.java new file mode 100644 index 00000000..ba801334 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/chatgpt/ChatGPT.java @@ -0,0 +1,94 @@ +package com.javadiscord.bot.utils.chatgpt; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; +import com.theokanning.openai.service.OpenAiService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ChatGPT { + private static final Logger LOGGER = LogManager.getLogger(ChatGPT.class); + private static final String API_KEY = System.getenv("CHATGPT_API_KEY"); + private static final Duration TIMEOUT = Duration.ofMinutes(3); + private static final String AI_MODEL = "gpt-3.5-turbo"; + private static final int MAX_TOKENS = 2000; + private static final double FREQUENCY_PENALTY = 0.5; + private static final double TEMPERATURE = 0.8; + private static final int MAX_NUMBER_OF_RESPONSES = 1; + + private final OpenAiService openAiService; + + public ChatGPT() { + openAiService = new OpenAiService(API_KEY, TIMEOUT); + + ChatMessage setupMessage = + new ChatMessage( + ChatMessageRole.SYSTEM.value(), + """ + Please answer questions in 2000 characters or less. Remember to count spaces in the + character limit. The context is Java Programming:\s""" + ); + + ChatCompletionRequest systemSetupRequest = + ChatCompletionRequest.builder() + .model(AI_MODEL) + .messages(List.of(setupMessage)) + .frequencyPenalty(FREQUENCY_PENALTY) + .temperature(TEMPERATURE) + .maxTokens(50) + .n(MAX_NUMBER_OF_RESPONSES) + .build(); + + openAiService.createChatCompletion(systemSetupRequest); + } + + public Optional ask(String question) { + try { + ChatMessage chatMessage = + new ChatMessage(ChatMessageRole.USER.value(), Objects.requireNonNull(question)); + + ChatCompletionRequest chatCompletionRequest = + ChatCompletionRequest.builder() + .model(AI_MODEL) + .messages(List.of(chatMessage)) + .frequencyPenalty(FREQUENCY_PENALTY) + .temperature(TEMPERATURE) + .maxTokens(MAX_TOKENS) + .n(MAX_NUMBER_OF_RESPONSES) + .build(); + + String response = + openAiService + .createChatCompletion(chatCompletionRequest) + .getChoices() + .getFirst() + .getMessage() + .getContent(); + + return Optional.of(ChatGPTResponseParser.parse(response)); + } catch (OpenAiHttpException openAiHttpException) { + LOGGER.warn( + String.format( + "There was an error using the OpenAI API: %s Code: %s Type: %s Status" + + " Code: %s", + openAiHttpException.getMessage(), + openAiHttpException.code, + openAiHttpException.type, + openAiHttpException.statusCode + ) + ); + } catch (RuntimeException e) { + LOGGER.warn( + "There was an error using the OpenAI API: {}", e.getMessage() + ); + } + return Optional.empty(); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/chatgpt/ChatGPTResponseParser.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/chatgpt/ChatGPTResponseParser.java new file mode 100644 index 00000000..165d2d66 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/chatgpt/ChatGPTResponseParser.java @@ -0,0 +1,61 @@ +package com.javadiscord.bot.utils.chatgpt; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ChatGPTResponseParser { + private static final Logger LOGGER = LogManager.getLogger(ChatGPTResponseParser.class); + private static final int RESPONSE_LENGTH_LIMIT = 2000; + + private ChatGPTResponseParser() {} + + public static String[] parse(String response) { + String[] partedResponse = new String[] {response}; + if (response.length() > RESPONSE_LENGTH_LIMIT) { + LOGGER.debug("Response to parse:\n{}", response); + partedResponse = partitionAiResponse(response); + } + return partedResponse; + } + + private static String[] partitionAiResponse(String response) { + List responseChunks = new ArrayList<>(); + String[] splitResponseOnMarks = response.split("```"); + for (int i = 0; i < splitResponseOnMarks.length; i++) { + String split = splitResponseOnMarks[i]; + List chunks = new ArrayList<>(); + chunks.add(split); + + while (!chunks.stream().allMatch(s -> s.length() < RESPONSE_LENGTH_LIMIT)) { + for (int j = 0; j < chunks.size(); j++) { + String chunk = chunks.get(j); + if (chunk.length() > RESPONSE_LENGTH_LIMIT) { + int midpointNewline = chunk.lastIndexOf("\n", chunk.length() / 2); + chunks.set(j, chunk.substring(0, midpointNewline)); + chunks.add(j + 1, chunk.substring(midpointNewline)); + } + } + } + + if (i % 2 != 0) { + String lang = split.substring(0, split.indexOf(System.lineSeparator())); + chunks = + chunks.stream() + .map(s -> ("```" + lang).concat(s).concat("```")) + .map(s -> s.replaceFirst("```" + lang + lang, "```" + lang)) + .toList(); + } + + responseChunks.addAll(filterEmptyStrings(chunks)); + } + + return responseChunks.toArray(new String[0]); + } + + private static List filterEmptyStrings(List chunks) { + return chunks.stream().filter(string -> !string.isEmpty()).toList(); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/ContainerCleanupTask.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/ContainerCleanupTask.java new file mode 100644 index 00000000..748c4235 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/ContainerCleanupTask.java @@ -0,0 +1,30 @@ +package com.javadiscord.bot.utils.docker; + +import java.time.Instant; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +public class ContainerCleanupTask implements Runnable { + private static final long CONTAINER_DURATION = TimeUnit.MINUTES.toSeconds(5); + + private final DockerSessions dockerSessions; + + public ContainerCleanupTask(DockerSessions dockerSessions) { + this.dockerSessions = dockerSessions; + } + + @Override + public void run() { + Instant now = Instant.now(); + Iterator it = dockerSessions.getSessions().iterator(); + while (it.hasNext()) { + Session session = it.next(); + Instant sessionStart = session.getStartTime(); + boolean sessionExpired = sessionStart.plusSeconds(CONTAINER_DURATION).isAfter(now); + if (sessionExpired) { + dockerSessions.stopContainer(session); + it.remove(); + } + } + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerCommandRunner.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerCommandRunner.java new file mode 100644 index 00000000..7bac950a --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerCommandRunner.java @@ -0,0 +1,37 @@ +package com.javadiscord.bot.utils.docker; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.ExecCreateCmdResponse; +import com.github.dockerjava.core.command.ExecStartResultCallback; + +public class DockerCommandRunner { + private final DockerClient dockerClient; + + public DockerCommandRunner(DockerClient dockerClient) { + this.dockerClient = dockerClient; + } + + public OutputStream sendCommand(Session session, String command) throws InterruptedException { + session.updateHistory(command); + + OutputStream output = new ByteArrayOutputStream(); + + ExecCreateCmdResponse execCreateCmdResponse = + dockerClient + .execCreateCmd(session.getContainerId()) + .withAttachStdout(true) + .withAttachStderr(true) + .withCmd("bash", "-c", command.trim()) + .exec(); + + dockerClient + .execStartCmd(execCreateCmdResponse.getId()) + .exec(new ExecStartResultCallback(output, output)) + .awaitCompletion(); + + return output; + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerContainerCreator.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerContainerCreator.java new file mode 100644 index 00000000..3bfe9b29 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerContainerCreator.java @@ -0,0 +1,65 @@ +package com.javadiscord.bot.utils.docker; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.model.HostConfig; + +public class DockerContainerCreator { + private final DockerClient dockerClient; + + public DockerContainerCreator(DockerClient dockerClient) { + this.dockerClient = dockerClient; + } + + public CreateContainerResponse createContainerStarted( + String name, + String image, + long memoryLimit, + long memorySwapLimit, + int cpuShares, + long cpuPeriod, + long cpuQuota + ) { + + CreateContainerResponse createContainerResponse = + createContainer( + name, image, memoryLimit, memorySwapLimit, cpuShares, cpuPeriod, cpuQuota + ); + + startContainer(createContainerResponse.getId()); + + return createContainerResponse; + } + + public CreateContainerResponse createContainer( + String name, + String image, + long memoryLimit, + long memorySwapLimit, + int cpuShares, + long cpuPeriod, + long cpuQuota + ) { + + HostConfig hostConfig = + HostConfig.newHostConfig() + .withAutoRemove(true) + .withInit(true) + .withMemory(memoryLimit) + .withMemorySwap(memorySwapLimit) + .withCpuShares(cpuShares) + .withCpuPeriod(cpuPeriod) + .withCpuQuota(cpuQuota); + + return dockerClient + .createContainerCmd(image) + .withHostConfig(hostConfig) + .withStdinOpen(true) + .withName(name) + .exec(); + } + + public void startContainer(String containerId) { + dockerClient.startContainerCmd(containerId).exec(); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerSessions.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerSessions.java new file mode 100644 index 00000000..f61b593e --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/DockerSessions.java @@ -0,0 +1,56 @@ +package com.javadiscord.bot.utils.docker; + +import java.util.ArrayList; +import java.util.List; + +import com.github.dockerjava.api.DockerClient; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class DockerSessions { + private static final Logger LOGGER = LogManager.getLogger(); + private final DockerClient dockerClient; + private final List sessions = new ArrayList<>(); + + public DockerSessions(DockerClient dockerClient) { + this.dockerClient = dockerClient; + } + + public Session createSession(String userId, String containerId) { + Session session = new Session(userId, containerId); + sessions.add(session); + LOGGER.info("Created new session for {}", userId); + return session; + } + + public void removeSession(Session session) { + sessions.remove(session); + dockerClient.stopContainerCmd(session.getContainerId()).exec(); + } + + public void stopContainer(Session session) { + dockerClient.stopContainerCmd(session.getContainerId()).exec(); + } + + public List getSessions() { + return sessions; + } + + public boolean hasSession(String userId) { + for (Session session : sessions) { + if (session.getSessionId().equalsIgnoreCase(userId)) { + return true; + } + } + return false; + } + + public Session getSessionForUser(String userId) { + for (Session session : sessions) { + if (session.getSessionId().equals(userId)) { + return session; + } + } + throw new RuntimeException("No session found for user"); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/Session.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/Session.java new file mode 100644 index 00000000..8dd9fc23 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/docker/Session.java @@ -0,0 +1,39 @@ +package com.javadiscord.bot.utils.docker; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +public class Session { + private final Instant startTime; + private final String sessionId; + private final String containerId; + + private final List commandHistory = new ArrayList<>(); + + public Session(String sessionId, String containerId) { + this.startTime = Instant.now(); + this.sessionId = sessionId; + this.containerId = containerId; + } + + public void updateHistory(String command) { + commandHistory.add(command); + } + + public List getCommandHistory() { + return commandHistory; + } + + public String getSessionId() { + return sessionId; + } + + public String getContainerId() { + return containerId; + } + + public Instant getStartTime() { + return startTime; + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellResponse.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellResponse.java new file mode 100644 index 00000000..64b36eeb --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellResponse.java @@ -0,0 +1,14 @@ +package com.javadiscord.bot.utils.jshell; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record JShellResponse( + @JsonProperty("errorStream") String errorStream, + @JsonProperty("outputStream") String outputStream, + @JsonProperty("events") List events, + @JsonProperty("error") String error +) {} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellService.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellService.java new file mode 100644 index 00000000..dc1d0474 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellService.java @@ -0,0 +1,64 @@ +package com.javadiscord.bot.utils.jshell; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class JShellService { + private static final Logger LOGGER = LogManager.getLogger(JShellService.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String API_URL = System.getenv("JSHELL_API_URL"); + + private final Map> history = new HashMap<>(); + + public JShellResponse sendRequest(String code) { + record Request(String code) {} + try (HttpClient client = HttpClient.newHttpClient()) { + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(API_URL)) + .setHeader("Content-Type", "application/json") + .POST( + HttpRequest.BodyPublishers.ofString( + OBJECT_MAPPER.writeValueAsString(new Request(code)) + ) + ) + .build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString()); + return OBJECT_MAPPER.readValue(response.body(), JShellResponse.class); + } catch (JsonProcessingException e) { + LOGGER.error("Failed to parse data received from JShell API", e); + } catch (IOException | InterruptedException e) { + LOGGER.error("Failed to send request to JShell API", e); + Thread.currentThread().interrupt(); + } + return null; + } + + public void updateHistory(long userId, String snippet) { + if (history.containsKey(userId)) { + history.get(userId).add(snippet); + } else { + history.put(userId, new ArrayList<>()); + } + } + + public List getHistory(long userId) { + if (history.containsKey(userId)) { + return history.get(userId); + } + return new ArrayList<>(); + } +} diff --git a/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellSnippet.java b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellSnippet.java new file mode 100644 index 00000000..f6551ce1 --- /dev/null +++ b/example/lj-discord-bot/src/main/java/com/javadiscord/bot/utils/jshell/JShellSnippet.java @@ -0,0 +1,11 @@ +package com.javadiscord.bot.utils.jshell; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record JShellSnippet( + @JsonProperty("statement") String statement, + @JsonProperty("value") String value, + @JsonProperty("status") String status +) {} diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/Gateway.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/Gateway.java index b1baad35..75493c22 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/Gateway.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/Gateway.java @@ -5,7 +5,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record Gateway( - String url, - int shards, + @JsonProperty("url") String url, + @JsonProperty("shards") int shards, @JsonProperty("session_start_limit") SessionStartLimit sessionStartLimit ) {} diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/SessionStartLimit.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/SessionStartLimit.java index 58f3459e..8b06d492 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/SessionStartLimit.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/SessionStartLimit.java @@ -5,8 +5,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record SessionStartLimit( - int total, - int remaining, + @JsonProperty("total") int total, + @JsonProperty("remaining") int remaining, @JsonProperty("reset_after") long resetAfter, @JsonProperty("max_concurrency") int maxConcurrency ) {} diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketHandler.java index 4afe17c9..7d59580b 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketHandler.java @@ -13,6 +13,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.WebSocket; @@ -21,25 +23,24 @@ public class WebSocketHandler implements Handler { private static final Logger LOGGER = LogManager.getLogger(WebSocketHandler.class); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); private static final Map OPERATION_HANDLER = new HashMap<>(); private final ConnectionMediator connectionMediator; - private final WebSocketRetryHandler retryHandler; private final Cache cache; + private final HeartbeatService heartbeatService; public WebSocketHandler( ConnectionMediator connectionMediator, - WebSocketRetryHandler retryHandler, - Cache cache + Cache cache, HeartbeatService heartbeatService ) { this.connectionMediator = connectionMediator; - this.retryHandler = retryHandler; this.cache = cache; + this.heartbeatService = heartbeatService; registerHandlers(); } private void registerHandlers() { - HeartbeatService heartbeatService = new HeartbeatService(connectionMediator); OPERATION_HANDLER.put(GatewayOpcode.HELLO, new HelloOperationHandler(heartbeatService)); OPERATION_HANDLER.put( GatewayOpcode.HEARTBEAT_ACK, new HeartbeatAckOperationHandler(heartbeatService) diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManager.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManager.java index 92a61c2c..1dd31228 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManager.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManager.java @@ -1,6 +1,7 @@ package com.javadiscord.jdi.internal.gateway; import com.javadiscord.jdi.internal.cache.Cache; +import com.javadiscord.jdi.internal.gateway.handlers.heartbeat.HeartbeatService; import com.javadiscord.jdi.internal.gateway.identify.IdentifyRequest; import com.fasterxml.jackson.core.JsonProcessingException; @@ -23,6 +24,7 @@ public class WebSocketManager { private final Cache cache; private WebSocket webSocket; private WebSocketClient webSocketClient; + private HeartbeatService heartbeatService; private boolean retryAllowed; public WebSocketManager( @@ -36,6 +38,8 @@ public WebSocketManager( } public void start(ConnectionMediator connectionMediator) { + heartbeatService = new HeartbeatService(connectionMediator); + String gatewayURL = connectionMediator.getConnectionDetails().getGatewayURL(); WebSocketConnectOptions webSocketConnectOptions = @@ -61,7 +65,7 @@ public void start(ConnectionMediator connectionMediator) { this.webSocket = webSocket; WebSocketHandler webSocketHandler = - new WebSocketHandler(connectionMediator, retryHandler, cache); + new WebSocketHandler(connectionMediator, cache, heartbeatService); webSocketHandler.handle(webSocket); @@ -101,6 +105,9 @@ public void stop() { if (webSocket != null && !webSocket.isClosed()) { webSocket.close(); } + if (heartbeatService != null) { + heartbeatService.stop(); + } webSocketClient.close() .onSuccess(res -> LOGGER.info("Web socket client has been shutdown")) .onFailure(err -> LOGGER.error("Failed to shutdown web socket client", err)); diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManagerProxy.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManagerProxy.java index 7e0b0791..5a37fa61 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManagerProxy.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/WebSocketManagerProxy.java @@ -14,7 +14,6 @@ public void start(ConnectionMediator connectionMediator) { } public void restart(ConnectionMediator connectionMediator) { - webSocketManager.restart(connectionMediator); } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/EventCodecHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/EventCodecHandler.java index b3ae2647..514635c1 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/EventCodecHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/EventCodecHandler.java @@ -169,6 +169,8 @@ EventType.MESSAGE_REACTION_REMOVE_ALL, new MessageReactionsRemovedHandler() EVENT_DECODERS.put(EventType.INTERACTION_CREATE, new InteractionCreateDecoder()); EVENT_HANDLERS.put(EventType.INTERACTION_CREATE, new InteractionCreateHandler()); + EVENT_DECODERS.put(EventType.GUILD_INTEGRATIONS_UPDATE, new IntegrationUpdateDecoder()); + EVENT_HANDLERS.put(EventType.GUILD_INTEGRATIONS_UPDATE, new IntegrationUpdateHandler()); AutoModerationDecoder autoModerationDecoder = new AutoModerationDecoder(); EVENT_DECODERS.put(EventType.AUTO_MODERATION_RULE_CREATE, autoModerationDecoder); @@ -223,9 +225,6 @@ EventType.GUILD_SCHEDULED_EVENT_USER_REMOVE, new ScheduledEventUserRemoveHandler EVENT_DECODERS.put(EventType.GUILD_STICKERS_UPDATE, new StickerUpdateDecoder()); EVENT_HANDLERS.put(EventType.GUILD_STICKERS_UPDATE, new StickerUpdateHandler()); - EVENT_DECODERS.put(EventType.GUILD_INTEGRATIONS_UPDATE, new IntegrationUpdateDecoder()); - EVENT_HANDLERS.put(EventType.GUILD_INTEGRATIONS_UPDATE, new IntegrationUpdateHandler()); - EVENT_DECODERS.put(EventType.GUILD_MEMBERS_CHUNK, new MemberChunkDecoder()); EVENT_HANDLERS.put(EventType.GUILD_MEMBERS_CHUNK, new MemberChunkHandler()); } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/AutoModerationDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/AutoModerationDecoder.java index 5977f84d..61c84285 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/AutoModerationDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/AutoModerationDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class AutoModerationDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public AutoModerationRule decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ChannelDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ChannelDecoder.java index a34b838f..746fb808 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ChannelDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ChannelDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class ChannelDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Channel decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ChannelPinUpdateDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ChannelPinUpdateDecoder.java index 9580bee2..f42fb6a2 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ChannelPinUpdateDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ChannelPinUpdateDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class ChannelPinUpdateDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public MessagePin decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/EntitlementDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/EntitlementDecoder.java index 364bc3fd..95d8cd8b 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/EntitlementDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/EntitlementDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class EntitlementDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Entitlement decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/EventUserDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/EventUserDecoder.java index e96c72cb..7101fc00 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/EventUserDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/EventUserDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class EventUserDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public EventUser decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildBanDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildBanDecoder.java index 70f53c23..2c2afc8d 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildBanDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildBanDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class GuildBanDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public GuildBan decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildDecoder.java index e38bc128..15325a42 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildDecoder.java @@ -1,19 +1,22 @@ package com.javadiscord.jdi.internal.gateway.handlers.events.codec.decoders; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.internal.gateway.GatewayEvent; import com.javadiscord.jdi.internal.gateway.handlers.events.codec.EventDecoder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -public class GuildDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); +public class GuildDecoder implements EventDecoder { + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override - public Guild decode(GatewayEvent gatewayEvent) { + public GuildModel decode(GatewayEvent gatewayEvent) { try { - return OBJECT_MAPPER.readValue(gatewayEvent.data().toString(), Guild.class); + return OBJECT_MAPPER.readValue(gatewayEvent.data().toString(), GuildModel.class); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildInviteDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildInviteDecoder.java index 8de1624b..39852683 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildInviteDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildInviteDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class GuildInviteDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Invite decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildMemberDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildMemberDecoder.java index 414d4dec..a9a8f0bb 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildMemberDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildMemberDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class GuildMemberDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Member decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildRoleDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildRoleDecoder.java index 54d6e98f..8508b153 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildRoleDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/GuildRoleDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class GuildRoleDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Role decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/IntegrationUpdateDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/IntegrationUpdateDecoder.java index a6c290de..37abf3a1 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/IntegrationUpdateDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/IntegrationUpdateDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class IntegrationUpdateDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public IntegrationUpdate decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/InteractionCreateDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/InteractionCreateDecoder.java index 9f9d137d..201cf285 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/InteractionCreateDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/InteractionCreateDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class InteractionCreateDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Interaction decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MemberChunkDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MemberChunkDecoder.java index 5bac2ad2..9b962ca4 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MemberChunkDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MemberChunkDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class MemberChunkDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public MemberChunk decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageBulkDeleteDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageBulkDeleteDecoder.java index cbb3305f..09d08fbf 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageBulkDeleteDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageBulkDeleteDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class MessageBulkDeleteDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public MessageBulkDelete decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageDecoder.java index 70562234..c24b8d40 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class MessageDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Message decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageReactionDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageReactionDecoder.java index f222cfba..3ea9870c 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageReactionDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageReactionDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class MessageReactionDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public MessageReaction decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageReactionsRemovedDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageReactionsRemovedDecoder.java index 224567f9..f90e8a6c 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageReactionsRemovedDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/MessageReactionsRemovedDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class MessageReactionsRemovedDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public MessageReactionsRemoved decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ReadyEventDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ReadyEventDecoder.java index 3523b042..ff78e4bf 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ReadyEventDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ReadyEventDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class ReadyEventDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public ReadyEvent decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ScheduledEventDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ScheduledEventDecoder.java index 93d81284..1fa1c0a1 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ScheduledEventDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ScheduledEventDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class ScheduledEventDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public ScheduledEvent decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/StageDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/StageDecoder.java index aa1a02a1..569c69c3 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/StageDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/StageDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class StageDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Stage decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/StickerUpdateDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/StickerUpdateDecoder.java index d663f2c6..39e303d9 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/StickerUpdateDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/StickerUpdateDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class StickerUpdateDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public StickerUpdate decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadDecoder.java index 8a533030..34ddb514 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class ThreadDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public Thread decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadListSyncDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadListSyncDecoder.java index 3276a7c0..3f2c89f5 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadListSyncDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadListSyncDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class ThreadListSyncDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public ThreadSync decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadMemberDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadMemberDecoder.java index c7913a68..8e46110b 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadMemberDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadMemberDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class ThreadMemberDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public ThreadMember decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadMemberUpdateDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadMemberUpdateDecoder.java index a822d8fe..6219c813 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadMemberUpdateDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/ThreadMemberUpdateDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class ThreadMemberUpdateDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public ThreadMemberUpdate decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/TypingStartDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/TypingStartDecoder.java index 2b8b51cb..b89c242c 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/TypingStartDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/TypingStartDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class TypingStartDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public TypingStart decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/UserDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/UserDecoder.java index 281b1d06..6a186388 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/UserDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/UserDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class UserDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public User decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/VoiceServerDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/VoiceServerDecoder.java index da353049..6b40acde 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/VoiceServerDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/VoiceServerDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class VoiceServerDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public VoiceServer decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/VoiceStateDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/VoiceStateDecoder.java index 5523876d..c2b676ca 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/VoiceStateDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/VoiceStateDecoder.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class VoiceStateDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override public VoiceState decode(GatewayEvent gatewayEvent) { diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/WebhookDecoder.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/WebhookDecoder.java index d09a2f26..245f482f 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/WebhookDecoder.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/decoders/WebhookDecoder.java @@ -1,18 +1,22 @@ package com.javadiscord.jdi.internal.gateway.handlers.events.codec.decoders; +import com.javadiscord.jdi.core.models.webhook.Webhook; import com.javadiscord.jdi.internal.gateway.GatewayEvent; import com.javadiscord.jdi.internal.gateway.handlers.events.codec.EventDecoder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -public class WebhookDecoder implements EventDecoder { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); +public class WebhookDecoder implements EventDecoder { + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); @Override - public WebhookDecoder decode(GatewayEvent gatewayEvent) { + public Webhook decode(GatewayEvent gatewayEvent) { try { - return OBJECT_MAPPER.readValue(gatewayEvent.data().toString(), WebhookDecoder.class); + return OBJECT_MAPPER.readValue(gatewayEvent.data().toString(), Webhook.class); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildCreateHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildCreateHandler.java index 71199e0c..6bff5d3e 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildCreateHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildCreateHandler.java @@ -1,14 +1,14 @@ package com.javadiscord.jdi.internal.gateway.handlers.events.codec.handlers.guild; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.internal.cache.Cache; import com.javadiscord.jdi.internal.gateway.ConnectionMediator; import com.javadiscord.jdi.internal.gateway.handlers.events.codec.EventHandler; -public class GuildCreateHandler implements EventHandler { +public class GuildCreateHandler implements EventHandler { @Override - public void handle(Guild event, ConnectionMediator connectionMediator, Cache cache) { + public void handle(GuildModel event, ConnectionMediator connectionMediator, Cache cache) { cache.createCache(event.id()); cache.getCacheForGuild(event.id()).add(event.id(), event); } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildDeleteHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildDeleteHandler.java index f16c2bad..d81c2e6f 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildDeleteHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildDeleteHandler.java @@ -1,13 +1,13 @@ package com.javadiscord.jdi.internal.gateway.handlers.events.codec.handlers.guild; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.internal.cache.Cache; import com.javadiscord.jdi.internal.gateway.ConnectionMediator; import com.javadiscord.jdi.internal.gateway.handlers.events.codec.EventHandler; -public class GuildDeleteHandler implements EventHandler { +public class GuildDeleteHandler implements EventHandler { @Override - public void handle(Guild event, ConnectionMediator connectionMediator, Cache cache) { + public void handle(GuildModel event, ConnectionMediator connectionMediator, Cache cache) { cache.removeGuild(event.id()); } } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildUpdateEventHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildUpdateEventHandler.java index 1e426080..49e7d2ad 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildUpdateEventHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/GuildUpdateEventHandler.java @@ -1,13 +1,13 @@ package com.javadiscord.jdi.internal.gateway.handlers.events.codec.handlers.guild; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.internal.cache.Cache; import com.javadiscord.jdi.internal.gateway.ConnectionMediator; import com.javadiscord.jdi.internal.gateway.handlers.events.codec.EventHandler; -public class GuildUpdateEventHandler implements EventHandler { +public class GuildUpdateEventHandler implements EventHandler { @Override - public void handle(Guild event, ConnectionMediator connectionMediator, Cache cache) { + public void handle(GuildModel event, ConnectionMediator connectionMediator, Cache cache) { if (!cache.isGuildCached(event.id())) { cache.createCache(event.id()); } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/message/MessageUpdateHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/message/MessageUpdateHandler.java index 2a21b038..50b41680 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/message/MessageUpdateHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/guild/message/MessageUpdateHandler.java @@ -6,8 +6,11 @@ import com.javadiscord.jdi.internal.gateway.handlers.events.codec.EventHandler; public class MessageUpdateHandler implements EventHandler { + @Override public void handle(Message event, ConnectionMediator connectionMediator, Cache cache) { - cache.getCacheForGuild(event.guildId()).update(event.id(), event); + if (event.author() != null && !event.author().bot()) { + cache.getCacheForGuild(event.guildId()).update(event.id(), event); + } } } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/interaction/InteractionCreateHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/interaction/InteractionCreateHandler.java index 049a12fe..d9a8aa1f 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/interaction/InteractionCreateHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/handlers/interaction/InteractionCreateHandler.java @@ -6,8 +6,12 @@ import com.javadiscord.jdi.internal.gateway.handlers.events.codec.EventHandler; public class InteractionCreateHandler implements EventHandler { + @Override public void handle(Interaction event, ConnectionMediator connectionMediator, Cache cache) { - cache.getCacheForGuild(event.guildId()).add(event.id(), event); + /* + * Empty because we do not need to do anything with interaction events at the + * gateway level + */ } } diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/models/channel/Thread.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/models/channel/Thread.java index e69c8fe2..5d8e0fa1 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/models/channel/Thread.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/events/codec/models/channel/Thread.java @@ -11,8 +11,8 @@ public record Thread( @JsonProperty("total_message_sent") int totalMessageSent, @JsonProperty("thread_metadata") ThreadMetadata threadMetadata, @JsonProperty("rate_limit_per_user") int rateLimitPerUser, - @JsonProperty("parent_id") String parentId, - @JsonProperty("owner_id") String ownerId, + @JsonProperty("parent_id") long parentId, + @JsonProperty("owner_id") long ownerId, @JsonProperty("newly_created") boolean newlyCreated, @JsonProperty("name") String name, @JsonProperty("message_count") int messageCount, diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HeartbeatService.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HeartbeatService.java index a8dcb19f..98c08a25 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HeartbeatService.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HeartbeatService.java @@ -55,6 +55,10 @@ private void checkHeartbeatAckReceived(WebSocket webSocket) { } } + public void stop() { + EXECUTOR_SERVICE.shutdown(); + } + public void sendHeartbeat(WebSocket webSocket) { webSocket.write( Buffer.buffer() diff --git a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HelloOperationHandler.java b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HelloOperationHandler.java index a087d4ac..53c9e081 100644 --- a/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HelloOperationHandler.java +++ b/gateway/src/main/java/com/javadiscord/jdi/internal/gateway/handlers/heartbeat/HelloOperationHandler.java @@ -9,12 +9,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class HelloOperationHandler implements GatewayOperationHandler { private static final Logger LOGGER = LogManager.getLogger(HelloOperationHandler.class); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().addModule(new JavaTimeModule()).build(); private final HeartbeatService heartbeatService; public HelloOperationHandler(HeartbeatService heartbeatService) { diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/application/Application.java b/models/src/main/java/com/javadiscord/jdi/core/models/application/Application.java index a9b6f543..b0f99fe6 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/application/Application.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/application/Application.java @@ -2,7 +2,7 @@ import java.util.List; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.core.models.user.User; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -30,7 +30,7 @@ public record Application( @JsonProperty("owner") User owner, @JsonProperty("verify_key") String verifyKey, @JsonProperty("guild_id") String guildId, - @JsonProperty("guild") Guild guild, + @JsonProperty("guild") GuildModel guild, @JsonProperty("primary_sku_id") String primarySkuId, @JsonProperty("slug") String slug, @JsonProperty("cover_image") String coverImage, diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/application/ApplicationCommandOption.java b/models/src/main/java/com/javadiscord/jdi/core/models/application/ApplicationCommandOption.java index 45a6556c..7dc94862 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/application/ApplicationCommandOption.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/application/ApplicationCommandOption.java @@ -12,4 +12,26 @@ public record ApplicationCommandOption( @JsonProperty("value") Object value, @JsonProperty("options") List options, @JsonProperty("focused") boolean focused -) {} +) { + + public String valueAsString() { + return String.valueOf(value); + } + + public int valueAsInt() { + return (int) value; + } + + public double valueAsDouble() { + return (double) value; + } + + public boolean valueAsBoolean() { + return (boolean) value; + } + + @Override + public String toString() { + return String.valueOf(value == null ? "" : value); + } +} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/channel/ThreadMember.java b/models/src/main/java/com/javadiscord/jdi/core/models/channel/ThreadMember.java index e6ac211d..e25c59c4 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/channel/ThreadMember.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/channel/ThreadMember.java @@ -1,10 +1,7 @@ package com.javadiscord.jdi.core.models.channel; -import java.time.OffsetDateTime; - import com.javadiscord.jdi.core.models.guild.GuildMember; -import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -12,9 +9,9 @@ public record ThreadMember( @JsonProperty("id") long threadId, @JsonProperty("user_id") long userId, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssX") @JsonProperty( + @JsonProperty( "join_timestamp" - ) OffsetDateTime joinTime, + ) String joinTime, @JsonProperty("flags") int flags, @JsonProperty("member") GuildMember guildMember ) {} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/channel/ThreadMetadata.java b/models/src/main/java/com/javadiscord/jdi/core/models/channel/ThreadMetadata.java index 85a6b815..b1423ee4 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/channel/ThreadMetadata.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/channel/ThreadMetadata.java @@ -1,5 +1,6 @@ package com.javadiscord.jdi.core.models.channel; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -7,8 +8,12 @@ public record ThreadMetadata( @JsonProperty("archived") boolean archived, @JsonProperty("auto_archive_duration") int autoArchiveDuration, - @JsonProperty("archive_timestamp") long archiveTimestamp, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssX") @JsonProperty( + "archive_timestamp" + ) String archiveTimestamp, @JsonProperty("locked") boolean locked, @JsonProperty("invitable") boolean invitable, - @JsonProperty("create_timestamp") String createTimestamp + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssX") @JsonProperty( + "create_timestamp" + ) String createTimestamp ) {} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/guild/Guild.java b/models/src/main/java/com/javadiscord/jdi/core/models/guild/GuildModel.java similarity index 99% rename from models/src/main/java/com/javadiscord/jdi/core/models/guild/Guild.java rename to models/src/main/java/com/javadiscord/jdi/core/models/guild/GuildModel.java index 135e2527..1a7c8f9f 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/guild/Guild.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/guild/GuildModel.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) -public record Guild( +public record GuildModel( @JsonProperty("id") long id, @JsonProperty("name") String name, @JsonProperty("icon") String icon, diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/guild/Interaction.java b/models/src/main/java/com/javadiscord/jdi/core/models/guild/Interaction.java index 41792ae7..00b4950d 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/guild/Interaction.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/guild/Interaction.java @@ -18,7 +18,7 @@ public record Interaction( @JsonProperty("application_id") long applicationId, @JsonProperty("type") InteractionType type, @JsonProperty("data") InteractionData data, - @JsonProperty("guild") Guild guild, + @JsonProperty("guild") GuildModel guild, @JsonProperty("guild_id") long guildId, @JsonProperty("channel") Channel channel, @JsonProperty("channel_id") long channelId, diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/guild/InteractionData.java b/models/src/main/java/com/javadiscord/jdi/core/models/guild/InteractionData.java index 1d9861af..ce2ca5fb 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/guild/InteractionData.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/guild/InteractionData.java @@ -12,6 +12,6 @@ public record InteractionData( @JsonProperty("type") int type, @JsonProperty("resolved") ResolvedData resolved, @JsonProperty("options") ApplicationCommandOption[] options, - @JsonProperty("guild_id") Long guildId, - @JsonProperty("target_id") Long targetId + @JsonProperty("guild_id") long guildId, + @JsonProperty("target_id") long targetId ) {} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/guild_template/GuildTemplate.java b/models/src/main/java/com/javadiscord/jdi/core/models/guild_template/GuildTemplate.java index f0399fe3..f49a7e68 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/guild_template/GuildTemplate.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/guild_template/GuildTemplate.java @@ -2,7 +2,7 @@ import java.time.OffsetDateTime; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.core.models.user.User; import com.fasterxml.jackson.annotation.JsonFormat; @@ -24,6 +24,6 @@ public record GuildTemplate( "updated_at" ) OffsetDateTime updatedAt, @JsonProperty("source_guild_id") long sourceGuildId, - @JsonProperty("serialized_source_guild") Guild sourceGuild, + @JsonProperty("serialized_source_guild") GuildModel sourceGuild, @JsonProperty("is_dirty") boolean isDirty ) {} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/message/Message.java b/models/src/main/java/com/javadiscord/jdi/core/models/message/Message.java index 7debd554..509ff608 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/message/Message.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/message/Message.java @@ -49,4 +49,13 @@ public record Message( @JsonProperty("position") int position, @JsonProperty("role_subscription_data") RoleSubscriptionData roleSubscriptionData, @JsonProperty("resolved") ResolvedData resolved -) {} +) { + + public boolean fromBot() { + return author.bot(); + } + + public boolean fromUser() { + return !author.bot(); + } +} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/message/StickerUpdate.java b/models/src/main/java/com/javadiscord/jdi/core/models/message/StickerUpdate.java index e9be206b..de6074b0 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/message/StickerUpdate.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/message/StickerUpdate.java @@ -7,6 +7,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record StickerUpdate( - @JsonProperty("guildId") long guildId, + @JsonProperty("guild_id") long guildId, @JsonProperty("stickers") List stickers ) {} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/message/embed/Embed.java b/models/src/main/java/com/javadiscord/jdi/core/models/message/embed/Embed.java index d05c607b..fdf4c3f3 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/message/embed/Embed.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/message/embed/Embed.java @@ -1,5 +1,7 @@ package com.javadiscord.jdi.core.models.message.embed; +import java.awt.*; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -24,4 +26,123 @@ public record Embed( @JsonProperty("provider") EmbedProvider provider, @JsonProperty("author") EmbedAuthor author, @JsonProperty("fields") List fields -) {} +) { + public static class Builder { + private String title; + private String type; + private String description; + private String url; + private Date timestamp; + private Integer color; + private EmbedFooter footer; + private EmbedImage image; + private EmbedThumbnail thumbnail; + private EmbedVideo video; + private EmbedProvider provider; + private EmbedAuthor author; + private List fields = new ArrayList<>(); + + public Builder title(String title) { + this.title = title; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder url(String url) { + this.url = url; + return this; + } + + public Builder timestamp(Date timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder color(Color color) { + this.color = color.getRGB() & 0xFFFFFF; + return this; + } + + public Builder footer(EmbedFooter footer) { + this.footer = footer; + return this; + } + + public Builder footer(String content) { + this.footer = new EmbedFooter(content, null, null); + return this; + } + + public Builder image(String imageURL) { + this.image = new EmbedImage(imageURL, null, null, null); + return this; + } + + public Builder image(EmbedImage image) { + this.image = image; + return this; + } + + public Builder thumbnail(String imageURL) { + this.thumbnail = new EmbedThumbnail(imageURL, null, null, null); + return this; + } + + public Builder thumbnail(EmbedThumbnail thumbnail) { + this.thumbnail = thumbnail; + return this; + } + + public Builder video(EmbedVideo video) { + this.video = video; + return this; + } + + public Builder provider(EmbedProvider provider) { + this.provider = provider; + return this; + } + + public Builder author(EmbedAuthor author) { + this.author = author; + return this; + } + + public Builder fields(List fields) { + this.fields = fields; + return this; + } + + public Builder addField(EmbedField field) { + this.fields.add(field); + return this; + } + + public Embed build() { + return new Embed( + title, + type, + description, + url, + timestamp, + color, + footer, + image, + thumbnail, + video, + provider, + author, + fields + ); + } + } +} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/ready/Application.java b/models/src/main/java/com/javadiscord/jdi/core/models/ready/Application.java index bff9b7c9..0595aeb2 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/ready/Application.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/ready/Application.java @@ -1,3 +1,3 @@ package com.javadiscord.jdi.core.models.ready; -public record Application(String id, int flags) {} +public record Application(long id, int flags) {} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/ready/ReadyEvent.java b/models/src/main/java/com/javadiscord/jdi/core/models/ready/ReadyEvent.java index ee8194a6..34988d3a 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/ready/ReadyEvent.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/ready/ReadyEvent.java @@ -2,7 +2,7 @@ import java.util.List; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.core.models.user.User; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -20,7 +20,7 @@ public record ReadyEvent( String[] relationships, String[] private_channels, String[] presences, - List guilds, + List guilds, @JsonProperty("guild_join_requests") String[] guildJoinRequests, @JsonProperty("geo_ordered_rtc_regions") String[] geoOrderedRtcRegions, Auth auth, diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/user/User.java b/models/src/main/java/com/javadiscord/jdi/core/models/user/User.java index 0880b653..9e3f90e5 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/user/User.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/user/User.java @@ -22,4 +22,10 @@ public record User( @JsonProperty("premium_type") PremiumType premiumType, @JsonProperty("public_flags") int publicFlags, @JsonProperty("avtar_decoration") String avatarDecoration -) {} +) { + + public String asMention() { + return "<@" + id + ">"; + } + +} diff --git a/models/src/main/java/com/javadiscord/jdi/core/models/webhook/Webhook.java b/models/src/main/java/com/javadiscord/jdi/core/models/webhook/Webhook.java index a6fb092a..2593f30d 100644 --- a/models/src/main/java/com/javadiscord/jdi/core/models/webhook/Webhook.java +++ b/models/src/main/java/com/javadiscord/jdi/core/models/webhook/Webhook.java @@ -1,7 +1,7 @@ package com.javadiscord.jdi.core.models.webhook; import com.javadiscord.jdi.core.models.channel.Channel; -import com.javadiscord.jdi.core.models.guild.Guild; +import com.javadiscord.jdi.core.models.guild.GuildModel; import com.javadiscord.jdi.core.models.user.User; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -18,7 +18,7 @@ public record Webhook( @JsonProperty("avatar") String avatar, @JsonProperty("token") String token, @JsonProperty("application_id") long applicationId, - @JsonProperty("source_guild") Guild sourceGuild, + @JsonProperty("source_guild") GuildModel sourceGuild, @JsonProperty("source_channel") Channel sourceChannel, @JsonProperty("url") String url ) {} diff --git a/models/src/test/unit/com/javadiscord/jdi/internal/channel/ThreadGuildMemberTest.java b/models/src/test/unit/com/javadiscord/jdi/internal/channel/ThreadGuildModelMemberTest.java similarity index 94% rename from models/src/test/unit/com/javadiscord/jdi/internal/channel/ThreadGuildMemberTest.java rename to models/src/test/unit/com/javadiscord/jdi/internal/channel/ThreadGuildModelMemberTest.java index 3d76ff9d..e4e46d3b 100644 --- a/models/src/test/unit/com/javadiscord/jdi/internal/channel/ThreadGuildMemberTest.java +++ b/models/src/test/unit/com/javadiscord/jdi/internal/channel/ThreadGuildModelMemberTest.java @@ -13,7 +13,7 @@ import java.time.OffsetDateTime; -class ThreadGuildMemberTest { +class ThreadGuildModelMemberTest { private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().addModule(new JavaTimeModule()).build(); @@ -32,7 +32,7 @@ void testDecodingThreadMember() { ThreadMember threadMember = OBJECT_MAPPER.readValue(input, ThreadMember.class); assertEquals(1, threadMember.threadId()); assertEquals(10, threadMember.userId()); - assertEquals(OffsetDateTime.parse("2024-04-25T21:37:44Z"), threadMember.joinTime()); + assertEquals(OffsetDateTime.parse("2024-04-25T21:37:44Z").toString(), threadMember.joinTime()); assertEquals(0, threadMember.flags()); } catch (JsonProcessingException e) { fail(e.getMessage()); diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 00000000..4620052a --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,27 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" +#Specify inspection profile for code analysis +profile: + name: qodana.starter +#Enable inspections +#include: +# - name: +#Disable inspections +#exclude: +# - name: +# paths: +# - +projectJDK: 21 #(Applied in CI/CD pipeline) +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-jvm:latest +include: + - name: CommentedOutCode + - name: Deprecation diff --git a/settings.gradle b/settings.gradle index 46019308..c058073f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,5 @@ include 'annotations' include 'cache' include 'example:echo-bot' findProject(':example:echo-bot')?.name = 'echo-bot' - +include 'example:lj-discord-bot' +findProject(':example:lj-discord-bot')?.name = 'lj-discord-bot'