Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] New decoding & encoding system #287

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,6 @@
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<!-- TODO: REMOVE -->
<repository>
<id>dv8tion</id>
<name>m2-dv8tion</name>
<url>https://m2.dv8tion.net/releases</url>
</repository>
</repositories>

<dependencies>
Expand Down Expand Up @@ -146,5 +140,11 @@
<artifactId>xsalsa20poly1305</artifactId>
<version>v0.10.1 </version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
7 changes: 7 additions & 0 deletions src/main/java/com/seailz/discordjar/DiscordJar.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.seailz.discordjar.command.listeners.slash.SlashCommandListener;
import com.seailz.discordjar.command.listeners.slash.SlashSubCommand;
import com.seailz.discordjar.command.listeners.slash.SubCommandListener;
import com.seailz.discordjar.decoding.DiscordObjectParser;
import com.seailz.discordjar.events.DiscordListener;
import com.seailz.discordjar.events.EventDispatcher;
import com.seailz.discordjar.gateway.Gateway;
Expand Down Expand Up @@ -90,6 +91,7 @@ public class DiscordJar {
* Stores the logger
*/
private final Logger logger;
private final DiscordObjectParser parser;
/**
* Intents the bot will use when connecting to the gateway
*/
Expand Down Expand Up @@ -224,6 +226,7 @@ public DiscordJar(String token, EnumSet<Intent> intents, APIVersion version, boo
new URLS(release, version);
logger = Logger.getLogger("DISCORD.JAR");
this.commandDispatcher = new CommandDispatcher();
this.parser = new DiscordObjectParser(this);
this.queuedRequests = new ArrayList<>();
this.buckets = new ArrayList<>();
this.voiceStates = new HashMap<>();
Expand Down Expand Up @@ -1571,4 +1574,8 @@ public Long getAverageGatewayPing() {
public APIVersion getApiVersion() {
return apiVersion;
}

public DiscordObjectParser getParser() {
return parser;
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/seailz/discordjar/decoding/DiscordObject.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.seailz.discordjar.decoding;

import com.seailz.discordjar.decoding.annotations.DiscordJarParameter;
import com.seailz.discordjar.decoding.annotations.DiscordObjectConstructor;
import com.seailz.discordjar.decoding.annotations.DiscordObjectParameter;

/**
* Marks an object that can be decoded by {@link DiscordObjectParser}.
* <p>Each one of your fields must be marked with {@link DiscordObjectParameter DiscordObjectParameter},
* and can be marked with {@link DiscordJarParameter DiscordJarParameter} in order to have
* its value set default to the existing DiscordJar instance.
* <p>
* You must have a constructor that matches your fields, <b>even down to the order</b>. That constructor
* must be marked with {@link DiscordObjectConstructor DiscordObjectConstructor}.
*/
public interface DiscordObject {
}
193 changes: 193 additions & 0 deletions src/main/java/com/seailz/discordjar/decoding/DiscordObjectParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package com.seailz.discordjar.decoding;

import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.decoding.annotations.DiscordJarParameter;
import com.seailz.discordjar.decoding.annotations.DiscordObjectConstructor;
import com.seailz.discordjar.decoding.annotations.DiscordObjectCustomAssignationsMethod;
import com.seailz.discordjar.decoding.annotations.DiscordObjectParameter;
import com.seailz.discordjar.decoding.data.DiscordObjectInformation;
import com.seailz.discordjar.decoding.data.DiscordObjectParameterInformation;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;

/**
* Parses objects from JSON into Java Objects ({@link DiscordObject}). Designed using reflection
* to improve the developer experience of working on discord.jar and decrease
* the amount of repeated code.
* <p>
* Each DiscordJar instance uses its own parser as if a parameter is marked as {@link DiscordObjectParameter},
* the parser needs to provide a DiscordJar instance.
*/
public class DiscordObjectParser {

@Getter
private DiscordJar discordJar;

public DiscordObjectParser(DiscordJar discordJar) {
this.discordJar = discordJar;
}

/**
* Since reflection isn't instant, there's a cache to prevent un-needed delays.
*/
private HashMap<Class<? extends DiscordObject>, DiscordObjectInformation> objectInformationCache = new HashMap<>();

public <T extends DiscordObject> T decompileObject(JSONObject data, Class<T> type) {
long start = System.currentTimeMillis();
DiscordObjectInformation objInfo = objectInformationCache.get(type);

if (objInfo == null) {
objInfo = discoverObject(type);
}
System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to discover object info");

List<Object> parameterValues = new ArrayList<>();
start = System.currentTimeMillis();
HashMap<String, Object> customAssignations = invokeCustomAssignationsMethod(objInfo, data);
System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to discover custom assignations");

start = System.currentTimeMillis();
for (DiscordObjectParameterInformation param : objInfo.parameterList()) {
if (customAssignations != null && customAssignations.containsKey(param.determineKey())) {
parameterValues.add(customAssignations.get(param.determineKey()));
continue;
}
parameterValues.add(param.determineValue(data, this));
}
System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to discover parameter values");

try {
start = System.currentTimeMillis();
if (objInfo.constructor() == null) {
Logger.getLogger("discord.jar")
.severe("[discord.jar - object decoding] Unable to find valid constructor for " +
type.getName() + " - returning null, please contact discord.jar's developers.");
return null;
}

System.out.println(Arrays.toString(parameterValues.toArray()));
System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to check constructor");

start = System.currentTimeMillis();
T obj = (T) objInfo.constructor()
.newInstance(parameterValues.toArray());
System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to create new instance of object");
return obj;
} catch (Exception e) {
Logger.getLogger("discord.jar")
.severe("[discord.jar - object decoding] Unable to decode object for " + type.getName() + ", printing stacktrace," +
" returning null. Please contact discord.jar's developers.");
e.printStackTrace();
}

return null;
}

private DiscordObjectInformation discoverObject(Class<? extends DiscordObject> clazz) {
long start = System.currentTimeMillis();
List<DiscordObjectParameterInformation> parameterList = new ArrayList<>();

for (Field field : clazz.getDeclaredFields()) {
// If an annotation isn't present, we don't really care.
if (!field.isAnnotationPresent(DiscordObjectParameter.class)) continue;
DiscordObjectParameter param = field.getAnnotation(DiscordObjectParameter.class);

DiscordObjectParameterInformation info = new DiscordObjectParameterInformation(
field.getName(),
field,
field.getType(),
field.isAnnotationPresent(DiscordJarParameter.class),
param
);

parameterList.add(info);
}

System.out.println("Time to discover object: " + (System.currentTimeMillis() - start) + "ms");

DiscordObjectInformation info = new DiscordObjectInformation(parameterList, discoverCustomAssignationsMethod(clazz), discoverConstructor(clazz));
objectInformationCache.remove(clazz);
objectInformationCache.put(clazz, info);

return info;
}

/**
* Finds a constructor marked with {@link DiscordObjectConstructor} within a class.
* Ideally this shouldn't return null, but don't rule it out.
*/
@Nullable
private Constructor<DiscordObject> discoverConstructor(Class<? extends DiscordObject> clazz) {
for (Constructor<?> constructor : clazz.getConstructors()) {
if (!constructor.isAnnotationPresent(DiscordObjectConstructor.class)) continue;
return (Constructor<DiscordObject>) constructor;
}
return null;
}

/**
* discord.jar allows objects to come up with their own method for decompiling certain parameters.
* <br>To do this, an object would define a static method that takes a {@link JSONObject}, returns a
* {@link HashMap HashMap<String, Object>} of JSON keys to values and is annotated with
* {@link DiscordObjectCustomAssignationsMethod}.
*/
private Method discoverCustomAssignationsMethod(Class<? extends DiscordObject> clazz) {
for (Method method : clazz.getMethods()) {
if (!method.isAnnotationPresent(DiscordObjectCustomAssignationsMethod.class)) continue;

if (!Modifier.isStatic(method.getModifiers())) {
Logger.getLogger("discord.jar").warning("[discord.jar - object decoding] Defined custom assignations method" +
" for " + clazz.getName() + " was not static. Ignoring custom assignations - please contact the discord.jar" +
" developers.");
return null;
}
if (!method.getReturnType().equals(HashMap.class)) {
Logger.getLogger("discord.jar").warning("[discord.jar - object decoding] Defined custom assignations method" +
" for " + clazz.getName() + " doesn't return a HashMap. Ignoring custom assignations - please contact the discord.jar" +
" developers.");
return null;
}
if (!Arrays.equals(method.getParameterTypes(), new Class[]{JSONObject.class})) {
Logger.getLogger("discord.jar").warning("[discord.jar - object decoding] Defined custom assignations method" +
" for " + clazz.getName() + " doesn't take a JSONObject as input. Ignoring custom assignations - please contact the discord.jar" +
" developers.");
return null;
}

return method;
}
return null;
}

private HashMap<String, Object> invokeCustomAssignationsMethod(DiscordObjectInformation info, JSONObject data) {
if (info.customAssignations() == null) return null;
Object obj = null;

Check warning on line 172 in src/main/java/com/seailz/discordjar/decoding/DiscordObjectParser.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused assignment

Variable `obj` initializer `null` is redundant
try {
obj = info.customAssignations().invoke(null, data);
} catch (Exception e) {
Logger.getLogger("discord.jar").warning("[discord.jar - object decoding] Error while finding custom " +
"assignations in the process of decoding an object - please contact the discord.jar developers with " +
"the stacktrace below.");
e.printStackTrace();
return null;
}

try {
return (HashMap<String, Object>) obj;
} catch (Exception e) {
Logger.getLogger("discord.jar").warning("[discord.jar - object decoding] Error while finding custom " +
"assignations in the process of decoding an object - couldn't cast to HashMap<String, Object> - please " +
"contact the discord.jar developers with the stacktrace below.");
e.printStackTrace();
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.seailz.discordjar.decoding.annotations;

import com.seailz.discordjar.decoding.annotations.DiscordObjectParameter;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* If used on a parameter, this won't be treated like a normal object parameter and instead will always
* have its value set to the current DiscordJar instance.
* <br><br>
* <b>This must be used in conjunction with {@link DiscordObjectParameter} and cannot be used on it's own.</b>
* {@link DiscordObjectParameter}
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DiscordJarParameter {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.seailz.discordjar.decoding.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks a constructor for a Discord object
*/
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
public @interface DiscordObjectConstructor {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.seailz.discordjar.decoding.annotations;

import com.seailz.discordjar.decoding.DiscordObjectParser;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotate a method with this to define custom decompilation methods for certain values while using
* {@link DiscordObjectParser DiscordObjectParser}.
* <p>The method must be static and should take in a {@link org.json.JSONObject} and return a {@link java.util.HashMap HashMap<String, Object>} of JSON
* keys to the values.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DiscordObjectCustomAssignationsMethod {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.seailz.discordjar.decoding.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DiscordObjectParameter {

boolean nullable() default true;

/**
* Is there a possibility for this parameter to be excluded from an object?
*/
boolean excludable() default true;

/**
* Allows you to override the default JSON key of a parameter.
* <br>If this is not set, a key is assumed by setting the parameter name to full lowercase.
*/
String overrideKey() default "";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.seailz.discordjar.decoding.data;

import com.seailz.discordjar.decoding.DiscordObject;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;

/**
* Contains information about a {@link DiscordObject}. This class is generally used for caching purposes,
* as reflection can take some time to complete and doing that every time you want to use a Discord object would
* be too resource intensive.
*/
public record DiscordObjectInformation(List<DiscordObjectParameterInformation> parameterList, Method customAssignations, Constructor<DiscordObject> constructor) {

}
Loading
Loading