diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml
deleted file mode 100644
index 806d41d1..00000000
--- a/.github/workflows/codesee-arch-diagram.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-# This workflow was added by CodeSee. Learn more at https://codesee.io/
-# This is v2.0 of this workflow file
-on:
- push:
- branches:
- - main
- pull_request_target:
- types: [opened, synchronize, reopened]
-
-name: CodeSee
-
-permissions: read-all
-
-jobs:
- codesee:
- runs-on: ubuntu-latest
- continue-on-error: true
- name: Analyze the repo with CodeSee
- steps:
- - uses: Codesee-io/codesee-action@v2
- with:
- codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}
- codesee-url: https://app.codesee.io
diff --git a/README.md b/README.md
index 1887a70a..08351105 100644
--- a/README.md
+++ b/README.md
@@ -40,15 +40,19 @@ account [here](https://discord.com/developers/applications).
To initialize a bot that uses the gateway (is able to receive events), you can use the following code:
```java
-new DiscordJar("token");
+
+DiscordJar djar = new DiscordJarBuilder("token").build();
```
You can specify intents to use with the gateway by using the following code:
```java
-new DiscordJar("token", EnumSet.of(Intent.GUILDS, Intent.GUILD_MESSAGES));
+DiscordJar djar = new DiscordJarBuilder("token")
+ .addIntents(Intent.GUILDS, Intent.GUILD_MESSAGES).build();
```
+Doing so will override the default intents.
+
Note: You can use the `Intent.ALL` constant to specify all intents. This does not include privileged intents.
### Creating an HTTP-Only bot
@@ -57,11 +61,12 @@ To make your bot an
-
-
-
-
diff --git a/pom.xml b/pom.xml
index 028884c1..cb81c9c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -94,7 +94,7 @@
org.springframework.boot
spring-boot-starter-websocket
- 3.0.5
+ 3.0.6
@@ -118,12 +118,12 @@
org.jsoup
jsoup
- 1.15.4
+ 1.16.1
com.squareup.okhttp3
okhttp
- 5.0.0-alpha.11
+ 4.11.0
diff --git a/src/main/java/com/seailz/discordjar/DiscordJar.java b/src/main/java/com/seailz/discordjar/DiscordJar.java
index e911d341..84a405b6 100644
--- a/src/main/java/com/seailz/discordjar/DiscordJar.java
+++ b/src/main/java/com/seailz/discordjar/DiscordJar.java
@@ -53,7 +53,9 @@
import java.util.logging.Logger;
/**
- * The main class of the discord.jar wrapper for the Discord API.
+ * The main class of the discord.jar wrapper for the Discord API. It is HIGHLY recommended that you use
+ * {@link DiscordJarBuilder} for creating new instances of this class as the other constructors are deprecated
+ * and will be set to protected/removed in the future.
*
* @author Seailz
* @since 1.0
@@ -113,23 +115,50 @@ public class DiscordJar {
* List of rate-limit buckets
*/
private List buckets;
+ /**
+ * The current status of the bot, or null if not set.
+ */
+ private Status status;
+ public int gatewayConnections = 0;
+ public List gatewayFactories = new ArrayList<>();
+
+ /**
+ * @deprecated Use {@link DiscordJarBuilder} instead.
+ */
+ @Deprecated(forRemoval = true)
public DiscordJar(String token, EnumSet intents, APIVersion version) throws ExecutionException, InterruptedException {
this(token, intents, version, false, null, false);
}
+ /**
+ * @deprecated Use {@link DiscordJarBuilder} instead.
+ */
+ @Deprecated(forRemoval = true)
public DiscordJar(String token, EnumSet intents, APIVersion version, boolean debug) throws ExecutionException, InterruptedException {
this(token, intents, version, false, null, debug);
}
+ /**
+ * @deprecated Use {@link DiscordJarBuilder} instead.
+ */
+ @Deprecated(forRemoval = true)
public DiscordJar(String token, APIVersion version) throws ExecutionException, InterruptedException {
this(token, EnumSet.of(Intent.ALL), version, false, null, false);
}
+ /**
+ * @deprecated Use {@link DiscordJarBuilder} instead.
+ */
+ @Deprecated(forRemoval = true)
public DiscordJar(String token, APIVersion version, boolean httpOnly, HTTPOnlyInfo httpOnlyInfo) throws ExecutionException, InterruptedException {
this(token, EnumSet.noneOf(Intent.class), version, httpOnly, httpOnlyInfo, false);
}
+ /**
+ * @deprecated Use {@link DiscordJarBuilder} instead.
+ */
+ @Deprecated(forRemoval = true)
public DiscordJar(String token, boolean httpOnly, HTTPOnlyInfo httpOnlyInfo) throws ExecutionException, InterruptedException {
this(token, EnumSet.noneOf(Intent.class), APIVersion.getLatest(), httpOnly, httpOnlyInfo, false);
}
@@ -153,7 +182,10 @@ public DiscordJar(String token, boolean httpOnly, HTTPOnlyInfo httpOnlyInfo) thr
* @param debug Should the bot be in debug mode?
* @throws ExecutionException If an error occurs while connecting to the gateway
* @throws InterruptedException If an error occurs while connecting to the gateway
+ *
+ * @deprecated Use {@link DiscordJarBuilder} instead. This constructor will be set to protected in the future.
*/
+ @Deprecated
public DiscordJar(String token, EnumSet intents, APIVersion version, boolean httpOnly, HTTPOnlyInfo httpOnlyInfo, boolean debug) throws ExecutionException, InterruptedException {
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
new RequestQueueHandler(this);
@@ -165,11 +197,7 @@ public DiscordJar(String token, EnumSet intents, APIVersion version, boo
this.queuedRequests = new ArrayList<>();
this.buckets = new ArrayList<>();
if (!httpOnly) {
- try {
- this.gatewayFactory = new GatewayFactory(this, debug);
- } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
- throw new RuntimeException(e);
- }
+ this.gatewayFactory = new GatewayFactory(this, debug);
}
this.debug = debug;
this.guildCache = new Cache<>(this, Guild.class,
@@ -219,21 +247,33 @@ public DiscordJar(String token, EnumSet intents, APIVersion version, boo
throw new RuntimeException(e);
}
- if (gatewayFactory == null || !gatewayFactory.getSession().isOpen()) {
+ if (gatewayFactory == null || (gatewayFactory.getSession() != null && !gatewayFactory.getSession().isOpen())) {
restartGateway();
}
}
}).start();
}
+ /**
+ * @deprecated Use {@link DiscordJarBuilder} instead.
+ */
+ @Deprecated(forRemoval = true)
public DiscordJar(String token) throws ExecutionException, InterruptedException {
this(token, EnumSet.of(Intent.ALL), APIVersion.getLatest());
}
+ /**
+ * @deprecated Use {@link DiscordJarBuilder} instead.
+ */
+ @Deprecated(forRemoval = true)
public DiscordJar(String token, boolean debug) throws ExecutionException, InterruptedException {
this(token, EnumSet.of(Intent.ALL), APIVersion.getLatest(), debug);
}
+ /**
+ * @deprecated Use {@link DiscordJarBuilder} instead.
+ */
+ @Deprecated(forRemoval = true)
public DiscordJar(String token, EnumSet intents) throws ExecutionException, InterruptedException {
this(token, intents, APIVersion.getLatest());
}
@@ -262,15 +302,13 @@ public GatewayFactory getGateway() {
* Kills the gateway connection and destroys the {@link GatewayFactory} instance.
* This method will also initiate garbage collection to avoid memory leaks. This probably shouldn't be used unless in {@link #restartGateway()}.
*/
- public Status killGateway() {
- Status status = gatewayFactory == null ? null : gatewayFactory.getStatus();
+ public void killGateway() {
try {
if (gatewayFactory != null) gatewayFactory.killConnection();
} catch (IOException ignored) {}
gatewayFactory = null;
// init garbage collection to avoid memory leaks
System.gc();
- return status;
}
/**
@@ -282,13 +320,13 @@ public Status killGateway() {
* @see #killGateway()
*/
public void restartGateway() {
- Status stat = killGateway();
+ killGateway();
try {
gatewayFactory = new GatewayFactory(this, debug);
- } catch (ExecutionException | InterruptedException | DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ gatewayFactories.add(gatewayFactory);
+ } catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
- if (stat != null) setStatus(stat);
}
protected void initiateShutdownHooks() {
@@ -308,6 +346,10 @@ protected void initiateShutdownHooks() {
}));
}
+ public void setGatewayFactory(GatewayFactory gatewayFactory) {
+ this.gatewayFactory = gatewayFactory;
+ }
+
public List getBuckets() {
return buckets;
}
@@ -353,6 +395,11 @@ public void setStatus(@NotNull Status status) {
json.put("op", 3);
gatewayFactory.queueMessage(json);
gatewayFactory.setStatus(status);
+ this.status = status;
+ }
+
+ public Status getStatus() {
+ return status;
}
/**
@@ -682,19 +729,25 @@ public void registerCommands(CommandListener... listeners) throws DiscordRequest
Permission[] defaultMemberPermissions = (ann instanceof SlashCommandInfo) ? ((SlashCommandInfo) ann).defaultMemberPermissions() : ((ContextCommandInfo) ann).defaultMemberPermissions();
boolean canUseInDms = (ann instanceof SlashCommandInfo) ? ((SlashCommandInfo) ann).canUseInDms() : ((ContextCommandInfo) ann).canUseInDms();
boolean nsfw = (ann instanceof SlashCommandInfo) ? ((SlashCommandInfo) ann).nsfw() : ((ContextCommandInfo) ann).nsfw();
- registerCommand(
- new Command(
- name,
- listener.getType(),
- description,
- (listener instanceof SlashCommandListener) ? ((SlashCommandListener) listener).getOptions() : new ArrayList<>(),
- nameLocales,
- descriptionLocales,
- defaultMemberPermissions,
- canUseInDms,
- nsfw
- )
- );
+ new Thread(() -> {
+ try {
+ registerCommand(
+ new Command(
+ name,
+ listener.getType(),
+ description,
+ (listener instanceof SlashCommandListener) ? ((SlashCommandListener) listener).getOptions() : new ArrayList<>(),
+ nameLocales,
+ descriptionLocales,
+ defaultMemberPermissions,
+ canUseInDms,
+ nsfw
+ )
+ );
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ throw new RuntimeException(e);
+ }
+ }).start();
commandDispatcher.registerCommand(name, listener);
if (!(listener instanceof SlashCommandListener slashCommandListener)) continue ;
diff --git a/src/main/java/com/seailz/discordjar/DiscordJarBuilder.java b/src/main/java/com/seailz/discordjar/DiscordJarBuilder.java
new file mode 100644
index 00000000..b16252d5
--- /dev/null
+++ b/src/main/java/com/seailz/discordjar/DiscordJarBuilder.java
@@ -0,0 +1,94 @@
+package com.seailz.discordjar;
+
+import com.seailz.discordjar.model.application.Intent;
+import com.seailz.discordjar.utils.HTTPOnlyInfo;
+import com.seailz.discordjar.utils.version.APIVersion;
+
+import java.util.EnumSet;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Factory class for creating a {@link DiscordJar} instance.
+ *
+ * @author Seailz
+ * @since 1.0.0
+ */
+public class DiscordJarBuilder {
+
+ private final String token;
+ private EnumSet intents;
+ private APIVersion apiVersion = APIVersion.getLatest();
+ private boolean httpOnly;
+ private HTTPOnlyInfo httpOnlyInfo;
+ private boolean debug;
+
+ public DiscordJarBuilder(String token) {
+ this.token = token;
+ }
+
+ public DiscordJarBuilder setIntents(EnumSet intents) {
+ this.intents = intents;
+ return this;
+ }
+
+ /**
+ * Resets back to default intents.
+ */
+ public DiscordJarBuilder defaultIntents() {
+ if (this.intents == null) this.intents = EnumSet.noneOf(Intent.class);
+ this.intents.clear();
+ this.intents.add(Intent.ALL);
+ return this;
+ }
+
+ public DiscordJarBuilder addIntents(Intent... intents) {
+ for (Intent intent : intents) {
+ addIntent(intent);
+ }
+ return this;
+ }
+
+ public DiscordJarBuilder addIntent(Intent intent) {
+ if (this.intents == null) this.intents = EnumSet.noneOf(Intent.class);
+ this.intents.remove(Intent.ALL);
+ this.intents.add(intent);
+ return this;
+ }
+
+ public DiscordJarBuilder removeIntent(Intent intent) {
+ if (this.intents == null) return this;
+ this.intents.remove(intent);
+ return this;
+ }
+
+ public DiscordJarBuilder setAPIVersion(APIVersion apiVersion) {
+ this.apiVersion = apiVersion;
+ return this;
+ }
+
+ public DiscordJarBuilder setHTTPOnly(boolean httpOnly) {
+ this.httpOnly = httpOnly;
+ return this;
+ }
+
+ public DiscordJarBuilder setHTTPOnlyInfo(HTTPOnlyInfo httpOnlyInfo) {
+ this.httpOnlyInfo = httpOnlyInfo;
+ return this;
+ }
+
+ public DiscordJarBuilder setDebug(boolean debug) {
+ this.debug = debug;
+ return this;
+ }
+
+ @SuppressWarnings("deprecation") // Deprecation warning is suppressed here because the intended use of that constructor is here.
+ public DiscordJar build() throws ExecutionException, InterruptedException {
+ if (intents == null) defaultIntents();
+ if (httpOnly && httpOnlyInfo == null) throw new IllegalStateException("HTTPOnly is enabled but no HTTPOnlyInfo was provided.");
+ return new DiscordJar(token, intents, apiVersion, httpOnly, httpOnlyInfo, debug);
+ }
+
+
+
+
+}
diff --git a/src/main/java/com/seailz/discordjar/action/guild/channel/CreateGuildChannelAction.java b/src/main/java/com/seailz/discordjar/action/guild/channel/CreateGuildChannelAction.java
index 606f1dd7..ed455522 100644
--- a/src/main/java/com/seailz/discordjar/action/guild/channel/CreateGuildChannelAction.java
+++ b/src/main/java/com/seailz/discordjar/action/guild/channel/CreateGuildChannelAction.java
@@ -9,6 +9,7 @@
import com.seailz.discordjar.utils.URLS;
import com.seailz.discordjar.utils.rest.DiscordRequest;
import com.seailz.discordjar.utils.rest.DiscordResponse;
+import com.seailz.discordjar.utils.rest.Response;
import org.json.JSONObject;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -24,6 +25,7 @@ public class CreateGuildChannelAction {
private int position;
private List permissionOverwrites;
private Category category;
+ private String categoryId;
private final Guild guild;
private final DiscordJar discordJar;
@@ -34,20 +36,32 @@ public CreateGuildChannelAction(String name, ChannelType type, Guild guild, Disc
this.discordJar = discordJar;
}
- public void setTopic(String topic) {
+ public CreateGuildChannelAction setTopic(String topic) {
this.topic = topic;
+ return this;
}
- public void setPosition(int position) {
+ public CreateGuildChannelAction setPosition(int position) {
this.position = position;
+ return this;
}
- public void setPermissionOverwrites(List permissionOverwrites) {
+ public CreateGuildChannelAction setPermissionOverwrites(List permissionOverwrites) {
this.permissionOverwrites = permissionOverwrites;
+ return this;
}
- public void setCategory(Category category) {
+ public CreateGuildChannelAction setCategory(Category category) {
this.category = category;
+ return this;
+ }
+
+ /**
+ * This is generally not recommended to use. If you set this, it will take priority over {@link #setCategory(Category)}.
+ */
+ public CreateGuildChannelAction setCategoryWithId(String id) {
+ this.categoryId = id;
+ return this;
}
public String getName() {
@@ -78,11 +92,15 @@ public Guild getGuild() {
return guild;
}
- public CompletableFuture run() {
- CompletableFuture future = new CompletableFuture<>();
- future.completeAsync(() -> {
+ public Response run() {
+ Response res = new Response<>();
+
+ new Thread(() -> {
+ String categoryId = null;
+ if (this.categoryId != null) categoryId = this.categoryId;
+ else if (this.category != null) categoryId = this.category.id();
try {
- return GuildChannel.decompile(
+ GuildChannel chan = GuildChannel.decompile(
new DiscordRequest(
new JSONObject()
.put("name", name)
@@ -90,7 +108,7 @@ public CompletableFuture run() {
.put("topic", topic != null ? topic : JSONObject.NULL)
.put("position", position)
.put("permission_overwrites", permissionOverwrites)
- .put("parent_id", category != null ? category.id() : JSONObject.NULL),
+ .put("parent_id", categoryId != null ? categoryId : JSONObject.NULL),
new HashMap<>(),
URLS.POST.GUILDS.CHANNELS.CREATE.replace("{guild.id}", guild.id()),
discordJar,
@@ -99,12 +117,16 @@ public CompletableFuture run() {
).invoke().body(),
discordJar
);
+ res.complete(chan);
} catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
- future.completeExceptionally(e);
+ res.completeError(new Response.Error(
+ e.getCode(),
+ e.getMessage(),
+ e.getBody()
+ ));
}
- return null;
- });
- return future;
+ }).start();
+ return res;
}
}
diff --git a/src/main/java/com/seailz/discordjar/action/interaction/EditInteractionMessageAction.java b/src/main/java/com/seailz/discordjar/action/interaction/EditInteractionMessageAction.java
index 13e7e17b..fb5e927d 100644
--- a/src/main/java/com/seailz/discordjar/action/interaction/EditInteractionMessageAction.java
+++ b/src/main/java/com/seailz/discordjar/action/interaction/EditInteractionMessageAction.java
@@ -7,6 +7,7 @@
import com.seailz.discordjar.model.message.Message;
import com.seailz.discordjar.utils.URLS;
import com.seailz.discordjar.utils.rest.DiscordRequest;
+import com.seailz.discordjar.utils.rest.Response;
import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -124,9 +125,9 @@ public EditInteractionMessageAction addComponent(DisplayComponent component) {
return this;
}
- public CompletableFuture run() {
- CompletableFuture future = new CompletableFuture<>();
- future.completeAsync(() -> {
+ public Response run() {
+ Response future = new Response<>();
+ new Thread(() -> {
JSONObject obj = new JSONObject();
if (content != null) obj.put("content", content);
if (embeds != null) {
@@ -175,12 +176,11 @@ public CompletableFuture run() {
);
try {
- return Message.decompile(request.invoke().body(), discordJar);
+ future.complete(Message.decompile(request.invoke().body(), discordJar));
} catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
- future.completeExceptionally(e);
- return null;
+ future.completeError(new Response.Error(e.getCode(), e.getMessage(), e.getBody()));
}
- });
+ }).start();
return future;
}
diff --git a/src/main/java/com/seailz/discordjar/action/interaction/InteractionCallbackAction.java b/src/main/java/com/seailz/discordjar/action/interaction/InteractionCallbackAction.java
index b79d98f6..97c0f02a 100644
--- a/src/main/java/com/seailz/discordjar/action/interaction/InteractionCallbackAction.java
+++ b/src/main/java/com/seailz/discordjar/action/interaction/InteractionCallbackAction.java
@@ -6,6 +6,7 @@
import com.seailz.discordjar.model.interaction.reply.InteractionReply;
import com.seailz.discordjar.utils.URLS;
import com.seailz.discordjar.utils.rest.DiscordRequest;
+import com.seailz.discordjar.utils.rest.Response;
import org.json.JSONObject;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -43,7 +44,7 @@ public InteractionReply getReply() {
return reply;
}
- public InteractionHandler run() throws DiscordRequest.UnhandledDiscordAPIErrorException {
+ public Response run() {
JSONObject json = new JSONObject();
json.put("type", this.type.getCode());
json.put("data", this.reply.compile());
@@ -53,8 +54,14 @@ public InteractionHandler run() throws DiscordRequest.UnhandledDiscordAPIErrorEx
URLS.POST.INTERACTIONS.CALLBACK.replace("{interaction.id}", this.id)
.replace("{interaction.token}", this.token), discordJar, URLS.POST.INTERACTIONS.CALLBACK,
RequestMethod.POST);
- request.invoke();
- return InteractionHandler.from(token, id, discordJar);
+ Response response = new Response<>();
+ try {
+ request.invoke();
+ response.complete(InteractionHandler.from(token, id, discordJar));
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ response.completeError(new Response.Error(e.getCode(), e.getMessage(), e.getBody()));
+ }
+ return response;
}
diff --git a/src/main/java/com/seailz/discordjar/action/interaction/followup/InteractionFollowupAction.java b/src/main/java/com/seailz/discordjar/action/interaction/followup/InteractionFollowupAction.java
index ebdb263d..e2a6fcf0 100644
--- a/src/main/java/com/seailz/discordjar/action/interaction/followup/InteractionFollowupAction.java
+++ b/src/main/java/com/seailz/discordjar/action/interaction/followup/InteractionFollowupAction.java
@@ -9,6 +9,7 @@
import com.seailz.discordjar.model.message.Attachment;
import com.seailz.discordjar.utils.URLS;
import com.seailz.discordjar.utils.rest.DiscordRequest;
+import com.seailz.discordjar.utils.rest.Response;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.HashMap;
@@ -142,17 +143,23 @@ public InteractionMessageResponse getReply() {
return reply;
}
- public InteractionHandler run() throws DiscordRequest.UnhandledDiscordAPIErrorException {
- new DiscordRequest(
- getReply().compile(),
- new HashMap<>(),
- URLS.POST.INTERACTIONS.FOLLOWUP
- .replaceAll("application.id", discordJar.getSelfInfo().id())
- .replaceAll("interaction.token", token),
- discordJar,
- URLS.POST.INTERACTIONS.FOLLOWUP,
- RequestMethod.POST
- ).invoke();
- return InteractionHandler.from(token, id, discordJar);
+ public Response run() {
+ Response response = new Response<>();
+ try {
+ new DiscordRequest(
+ getReply().compile(),
+ new HashMap<>(),
+ URLS.POST.INTERACTIONS.FOLLOWUP
+ .replaceAll("application.id", discordJar.getSelfInfo().id())
+ .replaceAll("interaction.token", token),
+ discordJar,
+ URLS.POST.INTERACTIONS.FOLLOWUP,
+ RequestMethod.POST
+ ).invoke();
+ response.complete(InteractionHandler.from(token, id, discordJar));
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ response.completeError(new Response.Error(e.getCode(), e.getMessage(), e.getBody()));
+ }
+ return response;
}
}
diff --git a/src/main/java/com/seailz/discordjar/action/message/MessageCreateAction.java b/src/main/java/com/seailz/discordjar/action/message/MessageCreateAction.java
index c7f6e4e4..510ddfd1 100644
--- a/src/main/java/com/seailz/discordjar/action/message/MessageCreateAction.java
+++ b/src/main/java/com/seailz/discordjar/action/message/MessageCreateAction.java
@@ -18,10 +18,7 @@
import org.springframework.web.bind.annotation.RequestMethod;
import java.io.File;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
+import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
@@ -50,6 +47,8 @@ public class MessageCreateAction {
private final DiscordJar discordJar;
private boolean silent = false;
private AllowedMentions allowedMentions;
+ private byte[] waveform;
+ private float duration = -1;
public MessageCreateAction(@Nullable String text, @NotNull String channelId, @NotNull DiscordJar discordJar) {
this.text = text;
@@ -128,6 +127,22 @@ public MessageCreateAction setText(@Nullable String text) {
return this;
}
+ /**
+ * For voice messages
+ */
+ public MessageCreateAction setWaveform(byte[] waveform) {
+ this.waveform = waveform;
+ return this;
+ }
+
+ /**
+ * For voice messages
+ */
+ public MessageCreateAction setDuration(float dur) {
+ this.duration = dur;
+ return this;
+ }
+
public MessageCreateAction setAllowedMentions(AllowedMentions allowedMentions) {
this.allowedMentions = allowedMentions;
return this;
@@ -256,6 +271,15 @@ public CompletableFuture run() {
if (this.nonce != null) payload.put("nonce", this.nonce);
if (this.tts) payload.put("tts", true);
if (this.messageReference != null) payload.put("message_reference", this.messageReference.compile());
+ if (this.waveform != null) {
+ // Encode base64
+ String encoded = Base64.getEncoder().encodeToString(this.waveform);
+ payload.put("waveform", encoded);
+ }
+
+ if (this.duration != -1) {
+ payload.put("duration", this.duration);
+ }
JSONArray components = new JSONArray();
if (this.components != null && !this.components.isEmpty()) {
diff --git a/src/main/java/com/seailz/discordjar/action/message/MessageEditAction.java b/src/main/java/com/seailz/discordjar/action/message/MessageEditAction.java
index bd8816b6..f2029443 100644
--- a/src/main/java/com/seailz/discordjar/action/message/MessageEditAction.java
+++ b/src/main/java/com/seailz/discordjar/action/message/MessageEditAction.java
@@ -43,10 +43,11 @@ public class MessageEditAction {
private final DiscordJar discordJar;
private final String messageId;
- public MessageEditAction(@NotNull String channelId, @NotNull DiscordJar discordJar, String messageId) {
+ public MessageEditAction(@NotNull String channelId, @NotNull DiscordJar discordJar, String messageId, boolean isVoiceMessage) {
this.channelId = channelId;
this.discordJar = discordJar;
this.messageId = messageId;
+ if (isVoiceMessage) throw new IllegalArgumentException("Cannot edit a voice message");
}
public MessageEditAction(ArrayList components, @NotNull String channelId, @NotNull DiscordJar discordJar, String messageId) {
@@ -182,7 +183,7 @@ public MessageEditAction addFiles(List files) {
public CompletableFuture run() {
CompletableFuture future = new CompletableFuture<>();
future.completeAsync(() -> {
- String url = URLS.PATCH.CHANNEL.MESSAGE.EDIT.replace("{channel.id}", channelId);
+ String url = URLS.PATCH.CHANNEL.MESSAGE.EDIT.replace("{channel.id}", channelId).replace("{message.id}", messageId);
JSONObject payload = new JSONObject();
if (this.text != null) payload.put("content", this.text);
diff --git a/src/main/java/com/seailz/discordjar/cache/Cache.java b/src/main/java/com/seailz/discordjar/cache/Cache.java
index 335c2cb0..2895cefd 100644
--- a/src/main/java/com/seailz/discordjar/cache/Cache.java
+++ b/src/main/java/com/seailz/discordjar/cache/Cache.java
@@ -1,6 +1,7 @@
package com.seailz.discordjar.cache;
import com.seailz.discordjar.DiscordJar;
+import com.seailz.discordjar.model.guild.Member;
import com.seailz.discordjar.utils.rest.DiscordRequest;
import com.seailz.discordjar.utils.rest.DiscordResponse;
import org.jetbrains.annotations.NotNull;
@@ -29,11 +30,13 @@ public class Cache {
private final DiscordJar discordJar;
private final Class clazz;
private final DiscordRequest discordRequest;
+ private final boolean isMember;
public Cache(DiscordJar discordJar, Class clazz, DiscordRequest request) {
this.discordJar = discordJar;
this.clazz = clazz;
this.discordRequest = request;
+ isMember = clazz == Member.class;
new Thread(() -> {
while (true) {
@@ -55,7 +58,9 @@ public Cache(DiscordJar discordJar, Class clazz, DiscordRequest request) {
public void cache(@NotNull T t) {
String id;
try {
- id = (String) t.getClass().getMethod("id").invoke(t);
+ if (isMember) {
+ id = ((Member) t).user().id();
+ } else id = (String) t.getClass().getMethod("id").invoke(t);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
@@ -63,7 +68,9 @@ public void cache(@NotNull T t) {
for (T cacheMember : cache) {
String cacheId;
try {
- cacheId = (String) cacheMember.getClass().getMethod("id").invoke(cacheMember);
+ if (isMember) {
+ cacheId = ((Member) cacheMember).user().id();
+ } else cacheId = (String) t.getClass().getMethod("id").invoke(t);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
@@ -103,19 +110,27 @@ public T getById(String id) throws DiscordRequest.UnhandledDiscordAPIErrorExcept
cacheCopy.forEach(t -> {
String itemId;
- for (Method method : clazz.getMethods()) {
- if (method.getName().equals("id")) {
- try {
- itemId = (String) method.invoke(t);
- if (Objects.equals(itemId, id))
- returnObject.set(t);
- } catch (IllegalAccessException | InvocationTargetException e) {
- e.printStackTrace();
+ if (isMember) {
+ itemId = ((Member) t).user().id();
+ if (Objects.equals(itemId, id))
+ returnObject.set(t);
+ } else {
+ for (Method method : clazz.getMethods()) {
+ if (method.getName().equals("id")) {
+ try {
+ itemId = (String) method.invoke(t);
+ if (Objects.equals(itemId, id)) {
+ returnObject.set(t);
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ }
}
}
}
});
+
if (returnObject.get() == null) {
// request from discord
DiscordResponse response;
diff --git a/src/main/java/com/seailz/discordjar/command/CommandDispatcher.java b/src/main/java/com/seailz/discordjar/command/CommandDispatcher.java
index 337764ca..eb91ebee 100644
--- a/src/main/java/com/seailz/discordjar/command/CommandDispatcher.java
+++ b/src/main/java/com/seailz/discordjar/command/CommandDispatcher.java
@@ -5,6 +5,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.events.EventDispatcher;
import com.seailz.discordjar.events.model.interaction.command.CommandInteractionEvent;
import com.seailz.discordjar.events.model.interaction.command.SlashCommandInteractionEvent;
import com.seailz.discordjar.model.interaction.data.command.ResolvedCommandOption;
@@ -42,40 +43,44 @@ public void registerSubCommand(SlashCommandListener top, SlashSubCommand sub, Su
}
public void dispatch(String name, CommandInteractionEvent event) {
- if ((event instanceof SlashCommandInteractionEvent) && ((SlashCommandInteractionEvent) event).getOptionsInternal() != null && !((SlashCommandInteractionEvent) event).getOptionsInternal().isEmpty()) {
- for (ResolvedCommandOption option : ((SlashCommandInteractionEvent) event).getOptionsInternal()) {
- if (option.type() == CommandOptionType.SUB_COMMAND) {
- for (ArrayList detailsList : subListeners.values()) {
- for (SlashSubCommandDetails details : detailsList) {
- if (details.sub.getName().equals(option.name())) {
- SlashCommandListener top = subListeners.keySet().stream().toList()
- .get(subListeners.values().stream().toList().indexOf(detailsList));
+ new Thread(() -> {
+ Class extends CommandInteractionEvent> eventClass = (event instanceof SlashCommandInteractionEvent ? SlashCommandInteractionEvent.class : CommandInteractionEvent.class);
+ event.getBot().getEventDispatcher().dispatchEvent(event, eventClass, event.getBot());
+ if ((event instanceof SlashCommandInteractionEvent) && ((SlashCommandInteractionEvent) event).getOptionsInternal() != null && !((SlashCommandInteractionEvent) event).getOptionsInternal().isEmpty()) {
+ for (ResolvedCommandOption option : ((SlashCommandInteractionEvent) event).getOptionsInternal()) {
+ if (option.type() == CommandOptionType.SUB_COMMAND) {
+ for (ArrayList detailsList : subListeners.values()) {
+ for (SlashSubCommandDetails details : detailsList) {
+ if (details.sub.getName().equals(option.name())) {
+ SlashCommandListener top = subListeners.keySet().stream().toList()
+ .get(subListeners.values().stream().toList().indexOf(detailsList));
- if (Objects.equals(name, top.getClass().getAnnotation(SlashCommandInfo.class).name())) {
- details.listener().onCommand(event);
- }
- return;
+ if (Objects.equals(name, top.getClass().getAnnotation(SlashCommandInfo.class).name())) {
+ new Thread(() -> details.listener().onCommand(event)).start();
+ }
+ return;
/*if (event.getName().startsWith(top.getClass().getAnnotation(SlashCommandInfo.class).name())) {
}*/
+ }
}
}
- }
- } else if (option.type() == CommandOptionType.SUB_COMMAND_GROUP) {
- List subOptions = new ArrayList<>();
+ } else if (option.type() == CommandOptionType.SUB_COMMAND_GROUP) {
+ List subOptions = new ArrayList<>();
- for (int i = 1; i < option.options().size(); i++) {
- subOptions.add(option.options().get(i));
- }
+ for (int i = 1; i < option.options().size(); i++) {
+ subOptions.add(option.options().get(i));
+ }
- for (ResolvedCommandOption subs : option.options()) {
- for (ArrayList detailsList : subListeners.values()) {
- for (SlashSubCommandDetails details : detailsList) {
- if (details.sub.getName().equals(subs.name())) {
- SlashCommandListener top = subListeners.keySet().stream().toList()
- .get(subListeners.values().stream().toList().indexOf(detailsList));
+ for (ResolvedCommandOption subs : option.options()) {
+ for (ArrayList detailsList : subListeners.values()) {
+ for (SlashSubCommandDetails details : detailsList) {
+ if (details.sub.getName().equals(subs.name())) {
+ SlashCommandListener top = subListeners.keySet().stream().toList()
+ .get(subListeners.values().stream().toList().indexOf(detailsList));
- if (Objects.equals(name, top.getClass().getAnnotation(SlashCommandInfo.class).name())) {
- details.listener().onCommand(event);
+ if (Objects.equals(name, top.getClass().getAnnotation(SlashCommandInfo.class).name())) {
+ new Thread(() -> details.listener().onCommand(event)).start();
+ }
}
}
}
@@ -83,8 +88,8 @@ public void dispatch(String name, CommandInteractionEvent event) {
}
}
}
- }
- listeners.get(name).onCommand(event);
+ new Thread(() -> listeners.get(name).onCommand(event)).start();
+ }, "Command Dispatcher (discord.jar)").start();
}
record SlashSubCommandDetails(
diff --git a/src/main/java/com/seailz/discordjar/events/DiscordListener.java b/src/main/java/com/seailz/discordjar/events/DiscordListener.java
index 74fb5759..8d434f8b 100644
--- a/src/main/java/com/seailz/discordjar/events/DiscordListener.java
+++ b/src/main/java/com/seailz/discordjar/events/DiscordListener.java
@@ -1,5 +1,6 @@
package com.seailz.discordjar.events;
+import com.seailz.discordjar.command.listeners.CommandListener;
import com.seailz.discordjar.events.model.command.CommandPermissionUpdateEvent;
import com.seailz.discordjar.events.model.general.ReadyEvent;
import com.seailz.discordjar.events.model.guild.GuildCreateEvent;
@@ -7,6 +8,7 @@
import com.seailz.discordjar.events.model.guild.member.GuildMemberRemoveEvent;
import com.seailz.discordjar.events.model.guild.member.GuildMemberUpdateEvent;
import com.seailz.discordjar.events.model.interaction.button.ButtonInteractionEvent;
+import com.seailz.discordjar.events.model.interaction.command.SlashCommandInteractionEvent;
import com.seailz.discordjar.events.model.interaction.modal.ModalInteractionEvent;
import com.seailz.discordjar.events.model.interaction.select.StringSelectMenuInteractionEvent;
import com.seailz.discordjar.events.model.interaction.select.entity.ChannelSelectMenuInteractionEvent;
@@ -53,6 +55,15 @@ public void onGuildMemberRemove(@NotNull GuildMemberRemoveEvent event) {
public void onCommandPermissionUpdate(@NotNull CommandPermissionUpdateEvent event) {
}
+ /**
+ * It is not recommend to use this method. Instead, where possible, you should use {@link com.seailz.discordjar.command.listeners.slash.SlashCommandListener SLashCommandListener} with a
+ * {@link com.seailz.discordjar.command.annotation.SlashCommandInfo SlashCommandInfo} annotation and then register it using {@link com.seailz.discordjar.DiscordJar#registerCommands(CommandListener...)}
+ * @param event The event
+ */
+ @Deprecated
+ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
+ }
+
// Message Component Events
// Select Menu Events
public void onStringSelectMenuInteraction(@NotNull StringSelectMenuInteractionEvent event) {
diff --git a/src/main/java/com/seailz/discordjar/events/EventDispatcher.java b/src/main/java/com/seailz/discordjar/events/EventDispatcher.java
index 54dc22bd..b8e4906a 100644
--- a/src/main/java/com/seailz/discordjar/events/EventDispatcher.java
+++ b/src/main/java/com/seailz/discordjar/events/EventDispatcher.java
@@ -3,11 +3,17 @@
import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.events.annotation.EventMethod;
import com.seailz.discordjar.events.model.Event;
+import com.seailz.discordjar.events.model.interaction.CustomIdable;
import com.seailz.discordjar.events.model.message.MessageCreateEvent;
+import com.seailz.discordjar.utils.annotation.RequireCustomId;
import com.seailz.discordjar.utils.rest.DiscordRequest;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
* This class is used to dispatch events to the correct listeners.
@@ -18,10 +24,22 @@
*/
public class EventDispatcher {
- private final HashMap listeners;
+ // Map: Event type -> List of pairs (Listener, Method)
+ private final Map, List> listenersByEventType = new HashMap<>();
+
+ // Pair of listener instance and method to call
+ private static class ListenerMethodPair {
+ final DiscordListener listener;
+ final Method method;
+
+ ListenerMethodPair(DiscordListener listener, Method method) {
+ this.listener = listener;
+ this.method = method;
+ }
+ }
+
public EventDispatcher(DiscordJar bot) {
- listeners = new HashMap<>();
}
/**
@@ -35,7 +53,8 @@ public void addListener(DiscordListener... listeners) {
for (DiscordListener listener : listeners) {
for (Method method : listener.getClass().getMethods()) {
if (method.isAnnotationPresent(EventMethod.class)) {
- this.listeners.put(listener, method);
+ Class extends Event> eventType = (Class extends Event>) method.getParameterTypes()[0];
+ listenersByEventType.computeIfAbsent(eventType, k -> new ArrayList<>()).add(new ListenerMethodPair(listener, method));
}
}
}
@@ -51,29 +70,36 @@ public void addListener(DiscordListener... listeners) {
*/
public void dispatchEvent(Event event, Class extends Event> type, DiscordJar djv) {
new Thread(() -> {
- if (event instanceof MessageCreateEvent) {
- try {
- if (((MessageCreateEvent) event).getMessage().author().id().equals(djv.getSelfUser().id()))
- return;
- } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
- throw new RuntimeException(e);
- }
+ long start = System.currentTimeMillis();
+ List listenersForEventType = listenersByEventType.get(type);
+ if (listenersForEventType == null) {
+ return;
}
- int index = 0;
- for (Method i : listeners.values()) {
- if (i.isAnnotationPresent(EventMethod.class)) {
- if (i.getParameterTypes()[0].equals(type)) {
- try {
- i.setAccessible(true);
- i.invoke(listeners.keySet().toArray()[index], event);
- } catch (Exception e) {
- e.printStackTrace();
+ for (ListenerMethodPair listenerMethodPair : listenersForEventType) {
+ Method method = listenerMethodPair.method;
+ if (method.isAnnotationPresent(RequireCustomId.class)) {
+ if (event instanceof CustomIdable) {
+ if (((CustomIdable) event).getCustomId() == null) {
+ continue;
+ }
+
+ if (!((CustomIdable) event).getCustomId().matches(method.getAnnotation(RequireCustomId.class).value())) {
+ continue;
}
}
}
- index++;
+
+ method.setAccessible(true);
+ new Thread(() -> {
+ try {
+ method.invoke(listenerMethodPair.listener, event);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }).start();
}
+ System.out.println("Event " + event.getClass().getSimpleName() + " took " + (System.currentTimeMillis() - start) + "ms to dispatch.");
}, "EventDispatcher").start();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/seailz/discordjar/events/annotation/EventMethod.java b/src/main/java/com/seailz/discordjar/events/annotation/EventMethod.java
index 77d79e90..dc991aec 100644
--- a/src/main/java/com/seailz/discordjar/events/annotation/EventMethod.java
+++ b/src/main/java/com/seailz/discordjar/events/annotation/EventMethod.java
@@ -6,8 +6,10 @@
import java.lang.annotation.Target;
/**
- * This annotation is used to mark methods that should be called when an event is fired.
- * If a listener method isn't marked with this annotation, it will not be called.
+ * This annotation is used to mark methods that should be called when an event is fired.
+ * If a listener method isn't marked with this annotation, it will not be called.
+ *
+ * THIS ANNOTATION IS NO LONGER REQUIRED. It exists purely for backwards compatibility.
*
* @author Seailz
* @see com.seailz.discordjar.events.DiscordListener
diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/CustomIdable.java b/src/main/java/com/seailz/discordjar/events/model/interaction/CustomIdable.java
new file mode 100644
index 00000000..79d2faad
--- /dev/null
+++ b/src/main/java/com/seailz/discordjar/events/model/interaction/CustomIdable.java
@@ -0,0 +1,10 @@
+package com.seailz.discordjar.events.model.interaction;
+
+/**
+ * Marks an interaction event that has a custom id.
+ * @author Seailz
+ * @since 1.0
+ */
+public interface CustomIdable {
+ String getCustomId();
+}
diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/button/ButtonInteractionEvent.java b/src/main/java/com/seailz/discordjar/events/model/interaction/button/ButtonInteractionEvent.java
index e64c1aec..837cfcbd 100644
--- a/src/main/java/com/seailz/discordjar/events/model/interaction/button/ButtonInteractionEvent.java
+++ b/src/main/java/com/seailz/discordjar/events/model/interaction/button/ButtonInteractionEvent.java
@@ -2,6 +2,7 @@
import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.action.interaction.ModalInteractionCallbackAction;
+import com.seailz.discordjar.events.model.interaction.CustomIdable;
import com.seailz.discordjar.events.model.interaction.InteractionEvent;
import com.seailz.discordjar.model.interaction.callback.InteractionCallbackType;
import com.seailz.discordjar.model.interaction.data.message.MessageComponentInteractionData;
@@ -12,7 +13,7 @@
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
-public class ButtonInteractionEvent extends InteractionEvent {
+public class ButtonInteractionEvent extends InteractionEvent implements CustomIdable {
public ButtonInteractionEvent(@NotNull DiscordJar bot, long sequence, @NotNull JSONObject data) {
super(bot, sequence, data);
// First checks the button registry for any actions that match the custom id.
@@ -41,6 +42,7 @@ public MessageComponentInteractionData getInteractionData() {
* @return {@link String} object containing the custom id.
*/
@NotNull
+ @Override
public String getCustomId() {
return getInteractionData().customId();
}
diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/command/CommandInteractionEvent.java b/src/main/java/com/seailz/discordjar/events/model/interaction/command/CommandInteractionEvent.java
index 63daca26..3ead9168 100644
--- a/src/main/java/com/seailz/discordjar/events/model/interaction/command/CommandInteractionEvent.java
+++ b/src/main/java/com/seailz/discordjar/events/model/interaction/command/CommandInteractionEvent.java
@@ -43,7 +43,7 @@ public ApplicationCommandInteractionData getCommandData() {
return (ApplicationCommandInteractionData) getInteraction().data();
}
- public ModalInteractionCallbackAction replyModal(Modal modal) {
+ public ModalInteractionCallbackAction reply(Modal modal) {
return new ModalInteractionCallbackAction(
InteractionCallbackType.MODAL,
new InteractionModalResponse(modal.title(), modal.customId(), modal.components()),
diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/modal/ModalInteractionEvent.java b/src/main/java/com/seailz/discordjar/events/model/interaction/modal/ModalInteractionEvent.java
index 8b41bf7d..91529d9d 100644
--- a/src/main/java/com/seailz/discordjar/events/model/interaction/modal/ModalInteractionEvent.java
+++ b/src/main/java/com/seailz/discordjar/events/model/interaction/modal/ModalInteractionEvent.java
@@ -2,6 +2,7 @@
import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.events.DiscordListener;
+import com.seailz.discordjar.events.model.interaction.CustomIdable;
import com.seailz.discordjar.events.model.interaction.InteractionEvent;
import com.seailz.discordjar.model.component.modal.ResolvedModalComponent;
import com.seailz.discordjar.model.interaction.data.modal.ModalSubmitInteractionData;
@@ -24,7 +25,7 @@
* @see Modal
* @since 1.0
*/
-public class ModalInteractionEvent extends InteractionEvent {
+public class ModalInteractionEvent extends InteractionEvent implements CustomIdable {
public ModalInteractionEvent(@NotNull DiscordJar bot, long sequence, @NotNull JSONObject data) {
super(bot, sequence, data);
}
@@ -49,6 +50,7 @@ public ModalSubmitInteractionData getInteractionData() {
* @return {@link String} object containing the custom id.
*/
@NotNull
+ @Override
public String getCustomId() {
return getInteractionData().customId();
}
diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/select/StringSelectMenuInteractionEvent.java b/src/main/java/com/seailz/discordjar/events/model/interaction/select/StringSelectMenuInteractionEvent.java
index 73bfff61..6e9da571 100644
--- a/src/main/java/com/seailz/discordjar/events/model/interaction/select/StringSelectMenuInteractionEvent.java
+++ b/src/main/java/com/seailz/discordjar/events/model/interaction/select/StringSelectMenuInteractionEvent.java
@@ -3,6 +3,7 @@
import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.action.interaction.ModalInteractionCallbackAction;
import com.seailz.discordjar.events.DiscordListener;
+import com.seailz.discordjar.events.model.interaction.CustomIdable;
import com.seailz.discordjar.events.model.interaction.InteractionEvent;
import com.seailz.discordjar.model.component.select.SelectOption;
import com.seailz.discordjar.model.component.select.string.StringSelectMenu;
@@ -28,7 +29,7 @@
* @see com.seailz.discordjar.model.component.select.string.StringSelectMenu
* @since 1.0
*/
-public class StringSelectMenuInteractionEvent extends InteractionEvent {
+public class StringSelectMenuInteractionEvent extends InteractionEvent implements CustomIdable {
public StringSelectMenuInteractionEvent(DiscordJar bot, long sequence, JSONObject data) {
super(bot, sequence, data);
@@ -66,6 +67,7 @@ public List getSelectedOptions() {
* @return {@link String} object containing the custom id.
*/
@NotNull
+ @Override
public String getCustomId() {
return getInteractionData().customId();
}
diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/ChannelSelectMenuInteractionEvent.java b/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/ChannelSelectMenuInteractionEvent.java
index bdc1e832..bd7c6887 100644
--- a/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/ChannelSelectMenuInteractionEvent.java
+++ b/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/ChannelSelectMenuInteractionEvent.java
@@ -3,6 +3,7 @@
import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.action.interaction.ModalInteractionCallbackAction;
import com.seailz.discordjar.events.DiscordListener;
+import com.seailz.discordjar.events.model.interaction.CustomIdable;
import com.seailz.discordjar.events.model.interaction.InteractionEvent;
import com.seailz.discordjar.model.channel.Channel;
import com.seailz.discordjar.model.component.select.entity.ChannelSelectMenu;
@@ -29,7 +30,7 @@
* @see com.seailz.discordjar.model.component.select.entity.ChannelSelectMenu
* @since 1.0
*/
-public class ChannelSelectMenuInteractionEvent extends InteractionEvent {
+public class ChannelSelectMenuInteractionEvent extends InteractionEvent implements CustomIdable {
public ChannelSelectMenuInteractionEvent(DiscordJar bot, long sequence, JSONObject data) {
super(bot, sequence, data);
@@ -73,6 +74,7 @@ public List getSelectedChannels() {
* @return {@link String} object containing the custom id.
*/
@NotNull
+ @Override
public String getCustomId() {
return getInteractionData().customId();
}
diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/RoleSelectMenuInteractionEvent.java b/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/RoleSelectMenuInteractionEvent.java
index abb3f50a..45d16717 100644
--- a/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/RoleSelectMenuInteractionEvent.java
+++ b/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/RoleSelectMenuInteractionEvent.java
@@ -3,6 +3,7 @@
import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.action.interaction.ModalInteractionCallbackAction;
import com.seailz.discordjar.events.DiscordListener;
+import com.seailz.discordjar.events.model.interaction.CustomIdable;
import com.seailz.discordjar.events.model.interaction.InteractionEvent;
import com.seailz.discordjar.model.component.select.entity.RoleSelectMenu;
import com.seailz.discordjar.model.interaction.callback.InteractionCallbackType;
@@ -28,7 +29,7 @@
* @see com.seailz.discordjar.model.component.select.entity.RoleSelectMenu
* @since 1.0
*/
-public class RoleSelectMenuInteractionEvent extends InteractionEvent {
+public class RoleSelectMenuInteractionEvent extends InteractionEvent implements CustomIdable {
public RoleSelectMenuInteractionEvent(DiscordJar bot, long sequence, JSONObject data) {
super(bot, sequence, data);
@@ -70,6 +71,7 @@ public List getSelectedRoles() {
* @return {@link String} object containing the custom id.
*/
@NotNull
+ @Override
public String getCustomId() {
return getInteractionData().customId();
}
diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/UserSelectMenuInteractionEvent.java b/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/UserSelectMenuInteractionEvent.java
index 76dc1eb8..36178c63 100644
--- a/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/UserSelectMenuInteractionEvent.java
+++ b/src/main/java/com/seailz/discordjar/events/model/interaction/select/entity/UserSelectMenuInteractionEvent.java
@@ -3,6 +3,7 @@
import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.action.interaction.ModalInteractionCallbackAction;
import com.seailz.discordjar.events.DiscordListener;
+import com.seailz.discordjar.events.model.interaction.CustomIdable;
import com.seailz.discordjar.events.model.interaction.InteractionEvent;
import com.seailz.discordjar.model.component.select.entity.UserSelectMenu;
import com.seailz.discordjar.model.interaction.callback.InteractionCallbackType;
@@ -29,7 +30,7 @@
* @see com.seailz.discordjar.model.component.select.entity.UserSelectMenu
* @since 1.0
*/
-public class UserSelectMenuInteractionEvent extends InteractionEvent {
+public class UserSelectMenuInteractionEvent extends InteractionEvent implements CustomIdable {
public UserSelectMenuInteractionEvent(DiscordJar bot, long sequence, JSONObject data) {
super(bot, sequence, data);
@@ -69,6 +70,7 @@ public List getSelectedUsers() throws DiscordRequest.UnhandledDiscordAPIEr
* @return {@link String} object containing the custom id.
*/
@NotNull
+ @Override
public String getCustomId() {
return getInteractionData().customId();
}
diff --git a/src/main/java/com/seailz/discordjar/events/model/message/MessageCreateEvent.java b/src/main/java/com/seailz/discordjar/events/model/message/MessageCreateEvent.java
index 93784d8b..9e174777 100644
--- a/src/main/java/com/seailz/discordjar/events/model/message/MessageCreateEvent.java
+++ b/src/main/java/com/seailz/discordjar/events/model/message/MessageCreateEvent.java
@@ -5,6 +5,7 @@
import com.seailz.discordjar.model.message.Message;
import com.seailz.discordjar.utils.rest.DiscordRequest;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;
/**
@@ -39,12 +40,13 @@ public Message getMessage() {
/**
* The {@link Guild} the message was sent in
- * This shouldn't return null.
+ * This will return null if the message was sent in a DM.
*
* @return A {@link Guild} object
*/
- @NotNull
+ @Nullable
public Guild getGuild() throws DiscordRequest.UnhandledDiscordAPIErrorException {
+ if (!getJson().getJSONObject("d").has("guild_id") || getJson().getJSONObject("d").isNull("guild_id")) return null;
return getBot().getGuildCache().getById((getJson().getJSONObject("d").getString("guild_id")));
}
}
diff --git a/src/main/java/com/seailz/discordjar/gateway/GatewayFactory.java b/src/main/java/com/seailz/discordjar/gateway/GatewayFactory.java
index 4a0af54f..b9406f8e 100644
--- a/src/main/java/com/seailz/discordjar/gateway/GatewayFactory.java
+++ b/src/main/java/com/seailz/discordjar/gateway/GatewayFactory.java
@@ -31,6 +31,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
@@ -61,18 +62,28 @@ public class GatewayFactory extends TextWebSocketHandler {
private boolean readyForMessages = false;
public HashMap memberRequestChunks = new HashMap<>();
private final boolean debug;
+ public UUID uuid = UUID.randomUUID();
- public GatewayFactory(DiscordJar discordJar, boolean debug) throws ExecutionException, InterruptedException, DiscordRequest.UnhandledDiscordAPIErrorException {
+ public GatewayFactory(DiscordJar discordJar, boolean debug) throws ExecutionException, InterruptedException {
this.discordJar = discordJar;
this.debug = debug;
- DiscordResponse response = new DiscordRequest(
- new JSONObject(),
- new HashMap<>(),
- "/gateway",
- discordJar,
- "/gateway", RequestMethod.GET
- ).invoke();
- this.gatewayUrl = response.body().getString("url");
+
+ discordJar.setGatewayFactory(this);
+
+ DiscordResponse response = null;
+ try {
+ response = new DiscordRequest(
+ new JSONObject(),
+ new HashMap<>(),
+ "/gateway",
+ discordJar,
+ "/gateway", RequestMethod.GET
+ ).invoke();
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException ignored) {}
+ if (response == null || response.body() == null || !response.body().has("url")) {
+ // In case the request fails, we can attempt to use the backup gateway URL instead.
+ this.gatewayUrl = URLS.GATEWAY.BASE_URL;
+ } else this.gatewayUrl = response.body().getString("url");
connect();
}
@@ -85,10 +96,12 @@ public void connect(String customUrl) throws ExecutionException, InterruptedExce
if (debug) {
logger.info("[DISCORD.JAR - DEBUG] Gateway connection established.");
}
+ discordJar.setGatewayFactory(this);
}
public void connect() throws ExecutionException, InterruptedException {
connect(gatewayUrl);
+
}
@Override
@@ -182,6 +195,12 @@ public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
}
}
+
+ /**
+ * Do not use this method - it is for internal use only.
+ * @param status The status to set.
+ */
+ @Deprecated
public void setStatus(Status status) {
this.status = status;
}
@@ -207,6 +226,10 @@ public void queueMessage(JSONObject payload) {
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
JSONObject payload = new JSONObject(message.getPayload());
+ if (discordJar.getGateway() != this) {
+ logger.info("[DISCORD.JAR] Received message from a gateway that isn't the main gateway. This is usually a bug, please report it on discord.jar's GitHub with this log message. Payload: " + payload.toString());
+ return;
+ }
if (debug) {
logger.info("[DISCORD.JAR - DEBUG] Received message: " + payload.toString());
@@ -287,6 +310,13 @@ private void handleDispatched(JSONObject payload) throws NoSuchMethodException,
this.sessionId = payload.getJSONObject("d").getString("session_id");
this.resumeUrl = payload.getJSONObject("d").getString("resume_gateway_url");
readyForMessages = true;
+
+ if (discordJar.getStatus() != null) {
+ JSONObject json = new JSONObject();
+ json.put("d", discordJar.getStatus().compile());
+ json.put("op", 3);
+ queueMessage(json);
+ }
break;
case GUILD_CREATE:
discordJar.getGuildCache().cache(Guild.decompile(payload.getJSONObject("d"), discordJar));
@@ -315,7 +345,7 @@ public void killConnection() throws IOException {
heartbeatManager = null;
readyForMessages = false;
// close connection
- session.close(CloseStatus.SERVER_ERROR);
+ if (session != null) session.close(CloseStatus.SERVER_ERROR);
if (debug) {
logger.info("[DISCORD.JAR - DEBUG] Connection closed.");
diff --git a/src/main/java/com/seailz/discordjar/model/channel/thread/Thread.java b/src/main/java/com/seailz/discordjar/model/channel/thread/Thread.java
index 6e3ad32f..b4d0c304 100644
--- a/src/main/java/com/seailz/discordjar/model/channel/thread/Thread.java
+++ b/src/main/java/com/seailz/discordjar/model/channel/thread/Thread.java
@@ -5,6 +5,9 @@
import com.seailz.discordjar.model.channel.GuildChannel;
import com.seailz.discordjar.model.channel.MessagingChannel;
import com.seailz.discordjar.model.channel.TextChannel;
+import com.seailz.discordjar.model.channel.interfaces.MessageRetrievable;
+import com.seailz.discordjar.model.channel.interfaces.Messageable;
+import com.seailz.discordjar.model.channel.interfaces.Transcriptable;
import com.seailz.discordjar.model.channel.interfaces.Typeable;
import com.seailz.discordjar.model.channel.internal.ThreadImpl;
import com.seailz.discordjar.model.channel.utils.ChannelType;
@@ -52,7 +55,7 @@
* @since 1.0
* @see GuildChannel
*/
-public interface Thread extends GuildChannel, Typeable {
+public interface Thread extends GuildChannel, Typeable, Messageable, MessageRetrievable, Transcriptable {
/**
* The id of the parent channel
diff --git a/src/main/java/com/seailz/discordjar/model/embed/Embeder.java b/src/main/java/com/seailz/discordjar/model/embed/Embeder.java
index 500d1c87..19f897ca 100644
--- a/src/main/java/com/seailz/discordjar/model/embed/Embeder.java
+++ b/src/main/java/com/seailz/discordjar/model/embed/Embeder.java
@@ -17,9 +17,15 @@ public interface Embeder {
Embeder timestamp(String timestamp);
Embeder timestamp();
+ Embeder removeField(String name);
+
Embeder field(EmbedField field);
+ Embeder field(EmbedField field, int index);
+ Embeder field(String name, String value);
+ Embeder field(String name, String value, int index);
Embeder field(String name, String value, boolean inline);
+ Embeder field(String name, String value, boolean inline, int index);
Embeder color(Color color);
Embeder color(int color);
diff --git a/src/main/java/com/seailz/discordjar/model/embed/EmbederImpl.java b/src/main/java/com/seailz/discordjar/model/embed/EmbederImpl.java
index 95c73443..fd5ad27e 100644
--- a/src/main/java/com/seailz/discordjar/model/embed/EmbederImpl.java
+++ b/src/main/java/com/seailz/discordjar/model/embed/EmbederImpl.java
@@ -55,19 +55,40 @@ public Embeder url(String url) {
@Override
public Embeder field(EmbedField field) {
+ return field(field, fields.length);
+ }
+
+ @Override
+ public Embeder field(EmbedField field, int index) {
EmbedField[] fields = this.fields;
EmbedField[] newFields = new EmbedField[fields.length + 1];
- System.arraycopy(fields, 0, newFields, 0, fields.length);
- newFields[fields.length] = field;
+ System.arraycopy(fields, 0, newFields, 0, index);
+ newFields[index] = field;
+ System.arraycopy(fields, index, newFields, index + 1, fields.length - index);
this.fields = newFields;
return this;
}
+ @Override
+ public Embeder field(String name, String value) {
+ return field(new EmbedField(name, value, false));
+ }
+
+ @Override
+ public Embeder field(String name, String value, int index) {
+ return field(new EmbedField(name, value, false), index);
+ }
+
@Override
public Embeder field(String name, String value, boolean inline) {
return field(new EmbedField(name, value, inline));
}
+ @Override
+ public Embeder field(String name, String value, boolean inline, int index) {
+ return field(new EmbedField(name, value, inline), index);
+ }
+
@Override
public Embeder footer(EmbedFooter footer) {
this.footer = footer;
@@ -135,6 +156,20 @@ public Embeder timestamp() {
return timestamp(Instant.now().toString());
}
+ @Override
+ public Embeder removeField(String name) {
+ EmbedField[] fields = this.fields;
+ EmbedField[] newFields = new EmbedField[fields.length - 1];
+ int i = 0;
+ for (EmbedField field : fields) {
+ if (!field.name().equals(name)) {
+ newFields[i++] = field;
+ }
+ }
+ this.fields = newFields;
+ return this;
+ }
+
@Override
public JSONObject compile() {
diff --git a/src/main/java/com/seailz/discordjar/model/guild/Guild.java b/src/main/java/com/seailz/discordjar/model/guild/Guild.java
index efb237aa..25f13158 100644
--- a/src/main/java/com/seailz/discordjar/model/guild/Guild.java
+++ b/src/main/java/com/seailz/discordjar/model/guild/Guild.java
@@ -45,87 +45,287 @@
/**
* Represents a guild.
- *
- * @param id The id of the guild
- * @param name The name of the guild
- * @param icon The icon hash of the guild
- * @param iconHash The icon hash of the guild (included with template object)
- * @param splash The splash hash of the guild
- * @param discoverySplash The discovery splash hash of the guild
- * @param isOwner Whether the user is the owner of the guild
- * @param owner The owner of the guild
- * @param permissions The total permissions of the user in the guild (excludes overwrites)
- * @param afkChannel The afk channel of the guild
- * @param afkTimeout The afk timeout of the guild
- * @param isWidgetEnabled Whether the widget is enabled for the guild
- * @param widgetChannel The widget channel of the guild
- * @param verificationLevel The verification level of the guild
- * @param defaultMessageNotificationLevel The default message notification level of the guild
- * @param explicitContentFilterLevel The explicit content filter level of the guild
- * @param roles The roles of the guild
- * @param emojis The emojis of the guild
- * @param features The features of the guild
- * @param mfaLevel The mfa level of the guild
- * @param applicationId The application id of the guild
- * @param systemChannel The system channel of the guild
- * @param maxPresences The maximum presences of the guild
- * @param maxMembers The maximum members of the guild
- * @param vanityUrlCode The vanity url code of the guild
- * @param description The description of the guild
- * @param banner The banner hash of the guild
- * @param premiumTier The premium tier of the guild
- * @param premiumSubscriptionCount The premium subscription count of the guild
- * @param preferredLocale The preferred locale of the guild
- * @param publicUpdatesChannel The public updates channel of the guild
- * @param maxVideoChannelUsers The maximum video channel users of the guild
- * @param approximateMemberCount The approximate member count of the guild
- * @param approximatePresenceCount The approximate presence count of the guild
- * @param welcomeScreen The welcome screen of the guild
- * @param stickers The stickers of the guild
- * @param premiumProgressBarEnabled Whether the premium progress bar is enabled for the guild
*/
-public record Guild(
- String id,
- String name,
- String icon,
- String iconHash,
- String splash,
- String discoverySplash,
- boolean isOwner,
- User owner,
- String permissions,
- Channel afkChannel,
- int afkTimeout,
- boolean isWidgetEnabled,
- Channel widgetChannel,
- VerificationLevel verificationLevel,
- DefaultMessageNotificationLevel defaultMessageNotificationLevel,
- ExplicitContentFilterLevel explicitContentFilterLevel,
- List roles,
- List emojis,
- EnumSet features,
- MFALevel mfaLevel,
- String applicationId,
- Channel systemChannel,
- int maxPresences,
- int maxMembers,
- String vanityUrlCode,
- String description,
- String banner,
- PremiumTier premiumTier,
- int premiumSubscriptionCount,
- String preferredLocale,
- Channel publicUpdatesChannel,
- int maxVideoChannelUsers,
- int maxStageVideoChannelUsers,
- int approximateMemberCount,
- int approximatePresenceCount,
- WelcomeScreen welcomeScreen,
- List stickers,
- boolean premiumProgressBarEnabled,
- DiscordJar discordJar,
- JsonCache roleCache
-) implements Compilerable, Snowflake, CDNAble {
+public class Guild implements Compilerable, Snowflake, CDNAble {
+ private final String id;
+ private final String name;
+ private final String icon;
+ private final String iconHash;
+ private final String splash;
+ private final String discoverySplash;
+ private final boolean isOwner;
+ private final User owner;
+ private final String permissions;
+ private final Channel afkChannel;
+ private final int afkTimeout;
+ private final boolean isWidgetEnabled;
+ private Channel widgetChannel = null;
+ private final String widgetChannelId;
+ private final VerificationLevel verificationLevel;
+ private final DefaultMessageNotificationLevel defaultMessageNotificationLevel;
+ private final ExplicitContentFilterLevel explicitContentFilterLevel;
+ private final List roles;
+ private final List emojis;
+ private final EnumSet features;
+ private final MFALevel mfaLevel;
+ private final String applicationId;
+ private Channel systemChannel;
+ private final String systemChannelId;
+ private final int maxPresences;
+ private final int maxMembers;
+ private final String vanityUrlCode;
+ private final String description;
+ private final String banner;
+ private final PremiumTier premiumTier;
+ private final int premiumSubscriptionCount;
+ private final String preferredLocale;
+ private Channel publicUpdatesChannel;
+ private final String publicUpdatesChannelId;
+ private final int maxVideoChannelUsers;
+ private final int maxStageVideoChannelUsers;
+ private final int approximateMemberCount;
+ private final int approximatePresenceCount;
+ private final WelcomeScreen welcomeScreen;
+ private final List stickers;
+ private final boolean premiumProgressBarEnabled;
+ private final DiscordJar discordJar;
+ private final JsonCache roleCache;
+
+ public Guild(
+ String id,
+ String name,
+ String icon,
+ String iconHash,
+ String splash,
+ String discoverySplash,
+ boolean isOwner,
+ User owner,
+ String permissions,
+ Channel afkChannel,
+ int afkTimeout,
+ boolean isWidgetEnabled,
+ String widgetChannelId,
+ VerificationLevel verificationLevel,
+ DefaultMessageNotificationLevel defaultMessageNotificationLevel,
+ ExplicitContentFilterLevel explicitContentFilterLevel,
+ List roles,
+ List emojis,
+ EnumSet features,
+ MFALevel mfaLevel,
+ String applicationId,
+ Channel systemChannel,
+ String systemChannelId,
+ int maxPresences,
+ int maxMembers,
+ String vanityUrlCode,
+ String description,
+ String banner,
+ PremiumTier premiumTier,
+ int premiumSubscriptionCount,
+ String preferredLocale,
+ Channel publicUpdatesChannel,
+ String publicUpdatesChannelId,
+ int maxVideoChannelUsers,
+ int maxStageVideoChannelUsers,
+ int approximateMemberCount,
+ int approximatePresenceCount,
+ WelcomeScreen welcomeScreen,
+ List stickers,
+ boolean premiumProgressBarEnabled,
+ DiscordJar discordJar,
+ JsonCache roleCache
+ ) {
+ this.id = id;
+ this.name = name;
+ this.icon = icon;
+ this.iconHash = iconHash;
+ this.splash = splash;
+ this.discoverySplash = discoverySplash;
+ this.isOwner = isOwner;
+ this.owner = owner;
+ this.permissions = permissions;
+ this.afkChannel = afkChannel;
+ this.afkTimeout = afkTimeout;
+ this.isWidgetEnabled = isWidgetEnabled;
+ this.widgetChannelId = widgetChannelId;
+ this.verificationLevel = verificationLevel;
+ this.defaultMessageNotificationLevel = defaultMessageNotificationLevel;
+ this.explicitContentFilterLevel = explicitContentFilterLevel;
+ this.roles = roles;
+ this.emojis = emojis;
+ this.features = features;
+ this.mfaLevel = mfaLevel;
+ this.applicationId = applicationId;
+ this.systemChannel = systemChannel;
+ this.systemChannelId = systemChannelId;
+ this.maxPresences = maxPresences;
+ this.maxMembers = maxMembers;
+ this.vanityUrlCode = vanityUrlCode;
+ this.description = description;
+ this.banner = banner;
+ this.premiumTier = premiumTier;
+ this.premiumSubscriptionCount = premiumSubscriptionCount;
+ this.preferredLocale = preferredLocale;
+ this.publicUpdatesChannel = publicUpdatesChannel;
+ this.publicUpdatesChannelId = publicUpdatesChannelId;
+ this.maxVideoChannelUsers = maxVideoChannelUsers;
+ this.maxStageVideoChannelUsers = maxStageVideoChannelUsers;
+ this.approximateMemberCount = approximateMemberCount;
+ this.approximatePresenceCount = approximatePresenceCount;
+ this.welcomeScreen = welcomeScreen;
+ this.stickers = stickers;
+ this.premiumProgressBarEnabled = premiumProgressBarEnabled;
+ this.discordJar = discordJar;
+ this.roleCache = roleCache;
+ }
+
+ public String id() {
+ return id;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public String icon() {
+ return icon;
+ }
+
+ public String iconHash() {
+ return iconHash;
+ }
+
+ public String splash() {
+ return splash;
+ }
+
+ public String discoverySplash() {
+ return discoverySplash;
+ }
+
+ public boolean isOwner() {
+ return isOwner;
+ }
+
+ public User owner() {
+ return owner;
+ }
+
+ public String permissions() {
+ return permissions;
+ }
+
+ public Channel afkChannel() {
+ return afkChannel;
+ }
+
+ public int afkTimeout() {
+ return afkTimeout;
+ }
+
+ public boolean isWidgetEnabled() {
+ return isWidgetEnabled;
+ }
+
+ public VerificationLevel verificationLevel() {
+ return verificationLevel;
+ }
+
+ public DefaultMessageNotificationLevel defaultMessageNotificationLevel() {
+ return defaultMessageNotificationLevel;
+ }
+
+ public ExplicitContentFilterLevel explicitContentFilterLevel() {
+ return explicitContentFilterLevel;
+ }
+
+ public List emojis() {
+ return emojis;
+ }
+
+ public EnumSet features() {
+ return features;
+ }
+
+ public MFALevel mfaLevel() {
+ return mfaLevel;
+ }
+
+ public String applicationId() {
+ return applicationId;
+ }
+
+ public String systemChannelId() {
+ return systemChannelId;
+ }
+
+ public int maxPresences() {
+ return maxPresences;
+ }
+
+ public int maxMembers() {
+ return maxMembers;
+ }
+
+ public String vanityUrlCode() {
+ return vanityUrlCode;
+ }
+
+ public String description() {
+ return description;
+ }
+
+ public String banner() {
+ return banner;
+ }
+
+ public PremiumTier premiumTier() {
+ return premiumTier;
+ }
+
+ public int premiumSubscriptionCount() {
+ return premiumSubscriptionCount;
+ }
+
+ public String preferredLocale() {
+ return preferredLocale;
+ }
+
+ public int maxVideoChannelUsers() {
+ return maxVideoChannelUsers;
+ }
+
+ public int maxStageVideoChannelUsers() {
+ return maxStageVideoChannelUsers;
+ }
+
+ public int approximateMemberCount() {
+ return approximateMemberCount;
+ }
+
+ public int approximatePresenceCount() {
+ return approximatePresenceCount;
+ }
+
+ public WelcomeScreen welcomeScreen() {
+ return welcomeScreen;
+ }
+
+ public List stickers() {
+ return stickers;
+ }
+
+ public boolean premiumProgressBarEnabled() {
+ return premiumProgressBarEnabled;
+ }
+
+ public DiscordJar discordJar() {
+ return discordJar;
+ }
+
+ public JsonCache roleCache() {
+ return roleCache;
+ }
@Override
@@ -152,7 +352,7 @@ public JSONObject compile() {
.put("features", features)
.put("mfa_level", mfaLevel.getCode())
.put("application_id", applicationId)
- .put("system_channel_id", systemChannel.id())
+ .put("system_channel_id", systemChannelId)
.put("max_presences", maxPresences)
.put("max_members", maxMembers)
.put("vanity_url_code", vanityUrlCode)
@@ -161,7 +361,7 @@ public JSONObject compile() {
.put("premium_tier", premiumTier.getCode())
.put("premium_subscription_count", premiumSubscriptionCount)
.put("preferred_locale", preferredLocale)
- .put("public_updates_channel_id", publicUpdatesChannel.id())
+ .put("public_updates_channel_id", publicUpdatesChannelId)
.put("max_video_channel_users", maxVideoChannelUsers)
.put("max_stage_video_channel_users", maxStageVideoChannelUsers)
.put("approximate_member_count", approximateMemberCount)
@@ -173,6 +373,7 @@ public JSONObject compile() {
@NotNull
public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
+ long nano = System.nanoTime();
String id;
String name;
String icon;
@@ -185,7 +386,7 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
Channel afkChannel;
int afkTimeout;
boolean isWidgetEnabled;
- Channel widgetChannel;
+ String widgetChannelId;
VerificationLevel verificationLevel;
DefaultMessageNotificationLevel defaultMessageNotificationLevel;
ExplicitContentFilterLevel explicitContentFilterLevel;
@@ -194,7 +395,7 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
EnumSet features;
MFALevel mfaLevel;
String applicationId;
- Channel systemChannel;
+ String systemChannelId;
int maxPresences;
int maxMembers;
String vanityUrlCode;
@@ -203,7 +404,7 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
PremiumTier premiumTier;
int premiumSubscriptionCount;
String preferredLocale;
- Channel publicUpdatesChannel;
+ String publicUpdatesChannelId;
int maxVideoChannelUsers;
int maxStageVideoChannelUsers = 0;
int approximateMemberCount;
@@ -285,9 +486,9 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
}
try {
- widgetChannel = discordJar.getChannelById(obj.getString("widget_channel_id"));
+ widgetChannelId =obj.getString("widget_channel_id");
} catch (JSONException e) {
- widgetChannel = null;
+ widgetChannelId = null;
}
try {
@@ -347,9 +548,9 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
}
try {
- systemChannel = discordJar.getChannelById(obj.getString("system_channel_id"));
+ systemChannelId = obj.getString("system_channel_id");
} catch (IllegalArgumentException | JSONException e) {
- systemChannel = null;
+ systemChannelId = null;
}
try {
@@ -401,10 +602,9 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
}
try {
- publicUpdatesChannel =
- discordJar.getChannelById(obj.getString("public_updates_channel_id"));
+ publicUpdatesChannelId = obj.getString("public_updates_channel_id");
} catch (Exception e) {
- publicUpdatesChannel = null;
+ publicUpdatesChannelId = null;
}
try {
@@ -466,7 +666,7 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
afkChannel,
afkTimeout,
isWidgetEnabled,
- widgetChannel,
+ widgetChannelId,
verificationLevel,
defaultMessageNotificationLevel,
explicitContentFilterLevel,
@@ -475,7 +675,8 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
features,
mfaLevel,
applicationId,
- systemChannel,
+ null,
+ systemChannelId,
maxPresences,
maxMembers,
vanityUrlCode,
@@ -484,7 +685,8 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
premiumTier,
premiumSubscriptionCount,
preferredLocale,
- publicUpdatesChannel,
+ null,
+ publicUpdatesChannelId,
maxVideoChannelUsers,
maxStageVideoChannelUsers,
approximateMemberCount,
@@ -506,6 +708,27 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) {
return g;
}
+ public Channel systemChannel() {
+ if (systemChannelId == null) return null;
+ if (this.systemChannel != null) return this.systemChannel;
+ this.systemChannel = discordJar.getChannelById(systemChannelId);
+ return this.systemChannel;
+ }
+
+ public Channel publicUpdatesChannel() {
+ if (publicUpdatesChannelId == null) return null;
+ if (this.publicUpdatesChannel != null) return this.publicUpdatesChannel;
+ this.publicUpdatesChannel = discordJar.getChannelById(publicUpdatesChannelId);
+ return this.publicUpdatesChannel;
+ }
+
+ public Channel widgetChannel() {
+ if (widgetChannelId == null) return null;
+ if (this.widgetChannel != null) return this.widgetChannel;
+ this.widgetChannel = discordJar.getChannelById(widgetChannelId);
+ return this.widgetChannel;
+ }
+
/**
* Leaves a guild
*/
diff --git a/src/main/java/com/seailz/discordjar/model/interaction/callback/internal/InteractionHandlerImpl.java b/src/main/java/com/seailz/discordjar/model/interaction/callback/internal/InteractionHandlerImpl.java
index c160b9e0..a8c8fd4a 100644
--- a/src/main/java/com/seailz/discordjar/model/interaction/callback/internal/InteractionHandlerImpl.java
+++ b/src/main/java/com/seailz/discordjar/model/interaction/callback/internal/InteractionHandlerImpl.java
@@ -31,68 +31,88 @@ public InteractionFollowupAction followup(String content) {
}
@Override
- public Message getOriginalResponse() throws DiscordRequest.UnhandledDiscordAPIErrorException {
- return Message.decompile(
- new DiscordRequest(
- new JSONObject(),
- new HashMap<>(),
- URLS.GET.INTERACTIONS.GET_ORIGINAL_INTERACTION_RESPONSE.replace("{interaction.token}", token).replace("{application.id}", discordJar.getSelfInfo().id()),
- discordJar,
- URLS.GET.INTERACTIONS.GET_ORIGINAL_INTERACTION_RESPONSE,
- RequestMethod.GET
- ).invoke().body(), discordJar
- );
+ public Message getOriginalResponse() {
+ try {
+ return Message.decompile(
+ new DiscordRequest(
+ new JSONObject(),
+ new HashMap<>(),
+ URLS.GET.INTERACTIONS.GET_ORIGINAL_INTERACTION_RESPONSE.replace("{interaction.token}", token).replace("{application.id}", discordJar.getSelfInfo().id()),
+ discordJar,
+ URLS.GET.INTERACTIONS.GET_ORIGINAL_INTERACTION_RESPONSE,
+ RequestMethod.GET
+ ).invoke().body(), discordJar
+ );
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
- public void deleteOriginalResponse() throws DiscordRequest.UnhandledDiscordAPIErrorException {
- new DiscordRequest(
- new JSONObject(),
- new HashMap<>(),
- URLS.DELETE.INTERACTION.DELETE_ORIGINAL_INTERACTION_RESPONSE.replace("{interaction.token}", token).replace("{application.id}", discordJar.getSelfInfo().id()),
- discordJar,
- URLS.DELETE.INTERACTION.DELETE_ORIGINAL_INTERACTION_RESPONSE,
- RequestMethod.DELETE
- ).invoke();
+ public void deleteOriginalResponse() {
+ try {
+ new DiscordRequest(
+ new JSONObject(),
+ new HashMap<>(),
+ URLS.DELETE.INTERACTION.DELETE_ORIGINAL_INTERACTION_RESPONSE.replace("{interaction.token}", token).replace("{application.id}", discordJar.getSelfInfo().id()),
+ discordJar,
+ URLS.DELETE.INTERACTION.DELETE_ORIGINAL_INTERACTION_RESPONSE,
+ RequestMethod.DELETE
+ ).invoke();
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
- public Message getFollowup(String id) throws DiscordRequest.UnhandledDiscordAPIErrorException {
- return Message.decompile(
- new DiscordRequest(
- new JSONObject(),
- new HashMap<>(),
- URLS.GET.INTERACTIONS.GET_FOLLOWUP_MESSAGE.replace("{interaction.token}", token).replace("{application.id}", discordJar.getSelfInfo().id())
- .replace("{message.id}", id),
- discordJar,
- URLS.GET.INTERACTIONS.GET_FOLLOWUP_MESSAGE,
- RequestMethod.GET
- ).invoke().body(), discordJar
- );
+ public Message getFollowup(String id) {
+ try {
+ return Message.decompile(
+ new DiscordRequest(
+ new JSONObject(),
+ new HashMap<>(),
+ URLS.GET.INTERACTIONS.GET_FOLLOWUP_MESSAGE.replace("{interaction.token}", token).replace("{application.id}", discordJar.getSelfInfo().id())
+ .replace("{message.id}", id),
+ discordJar,
+ URLS.GET.INTERACTIONS.GET_FOLLOWUP_MESSAGE,
+ RequestMethod.GET
+ ).invoke().body(), discordJar
+ );
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
- public void deleteFollowup(String id) throws DiscordRequest.UnhandledDiscordAPIErrorException {
- new DiscordRequest(
- new JSONObject(),
- new HashMap<>(),
- URLS.DELETE.INTERACTION.DELETE_FOLLOWUP_MESSAGE.replace("{interaction.token}", token).replace("{application.id}", discordJar.getSelfInfo().id())
- .replace("{message.id}", id),
- discordJar,
- URLS.DELETE.INTERACTION.DELETE_FOLLOWUP_MESSAGE,
- RequestMethod.DELETE
- ).invoke();
+ public void deleteFollowup(String id) {
+ try {
+ new DiscordRequest(
+ new JSONObject(),
+ new HashMap<>(),
+ URLS.DELETE.INTERACTION.DELETE_FOLLOWUP_MESSAGE.replace("{interaction.token}", token).replace("{application.id}", discordJar.getSelfInfo().id())
+ .replace("{message.id}", id),
+ discordJar,
+ URLS.DELETE.INTERACTION.DELETE_FOLLOWUP_MESSAGE,
+ RequestMethod.DELETE
+ ).invoke();
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
- public EditInteractionMessageAction editOriginalResponse() throws DiscordRequest.UnhandledDiscordAPIErrorException {
- return new EditInteractionMessageAction(
- discordJar.getSelfInfo().id(),
- token,
- discordJar,
- true,
- null
- );
+ public EditInteractionMessageAction editOriginalResponse() {
+ try {
+ return new EditInteractionMessageAction(
+ discordJar.getSelfInfo().id(),
+ token,
+ discordJar,
+ true,
+ null
+ );
+ } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
diff --git a/src/main/java/com/seailz/discordjar/model/interaction/data/ResolvedData.java b/src/main/java/com/seailz/discordjar/model/interaction/data/ResolvedData.java
index 3127712a..8c2034f9 100644
--- a/src/main/java/com/seailz/discordjar/model/interaction/data/ResolvedData.java
+++ b/src/main/java/com/seailz/discordjar/model/interaction/data/ResolvedData.java
@@ -87,68 +87,64 @@ public JSONObject compile() {
@NotNull
public static ResolvedData decompile(JSONObject obj, DiscordJar discordJar) {
- HashMap users;
- HashMap members;
- HashMap roles = null;
- HashMap channels;
- HashMap messages;
- HashMap attachments;
-
- try {
- JSONObject resolved = obj.getJSONObject("users");
- users = new HashMap<>();
- HashMap finalUsers = users;
- resolved.toMap().forEach((key, value) -> finalUsers.put(key, User.decompile((JSONObject) value, discordJar)));
- } catch (Exception e) {
+ HashMap users = new HashMap<>();
+ HashMap members = new HashMap<>();
+ HashMap roles = new HashMap<>();
+ HashMap channels = new HashMap<>();
+ HashMap messages = new HashMap<>();
+ HashMap attachments = new HashMap<>();
+
+ if (obj.has("users") && !obj.isNull("users")) {
+ JSONObject usersObj = obj.getJSONObject("users");
+ for (String s : usersObj.keySet()) {
+ users.put(s, User.decompile(usersObj.getJSONObject(s), discordJar));
+ }
+ } else {
users = null;
}
- try {
- JSONObject resolved = obj.getJSONObject("members");
- members = new HashMap<>();
- HashMap finalMembers = members;
- resolved.toMap().forEach((key, value) -> finalMembers.put(key, Member.decompile((JSONObject) value, discordJar, null, null)));
- } catch (Exception e) {
+ if (obj.has("members") && !obj.isNull("members")) {
+ JSONObject membersObj = obj.getJSONObject("members");
+ for (String s : membersObj.keySet()) {
+ members.put(s, Member.decompile(membersObj.getJSONObject(s), discordJar, null, null));
+ }
+ } else {
members = null;
}
- try {
+ if (obj.has("roles") && !obj.isNull("roles")) {
JSONObject rolesObj = obj.getJSONObject("roles");
- roles = new HashMap<>();
-
- HashMap finalRoles = roles;
- rolesObj.toMap().forEach((key, value) -> {
- String json = new com.google.gson.Gson().toJson(value);
- finalRoles.put(key, Role.decompile(new JSONObject(json)));
- });
- } catch (Exception e) {
+ for (String s : rolesObj.keySet()) {
+ roles.put(s, Role.decompile(rolesObj.getJSONObject(s)));
+ }
+ } else {
roles = null;
}
- try {
+ if (obj.has("channels") && !obj.isNull("channels")) {
JSONObject channelsObj = obj.getJSONObject("channels");
- channels = new HashMap<>();
- HashMap finalChannels = channels;
- channelsObj.toMap().forEach((key, value) -> finalChannels.put(key, Channel.decompile((JSONObject) value, discordJar)));
- } catch (Exception e) {
+ for (String s : channelsObj.keySet()) {
+ channels.put(s, Channel.decompile(channelsObj.getJSONObject(s), discordJar));
+ }
+ } else {
channels = null;
}
- try {
+ if (obj.has("messages") && !obj.isNull("messages")) {
JSONObject messagesObj = obj.getJSONObject("messages");
- messages = new HashMap<>();
- HashMap finalMessages = messages;
- messagesObj.toMap().forEach((key, value) -> finalMessages.put(key, Message.decompile((JSONObject) value, discordJar)));
- } catch (Exception e) {
+ for (String s : messagesObj.keySet()) {
+ messages.put(s, Message.decompile(messagesObj.getJSONObject(s), discordJar));
+ }
+ } else {
messages = null;
}
- try {
+ if (obj.has("attachments") && !obj.isNull("attachments")) {
JSONObject attachmentsObj = obj.getJSONObject("attachments");
- attachments = new HashMap<>();
- HashMap finalAttachments = attachments;
- attachmentsObj.toMap().forEach((key, value) -> finalAttachments.put(key, Attachment.decompile((JSONObject) value)));
- } catch (Exception e) {
+ for (String s : attachmentsObj.keySet()) {
+ attachments.put(s, Attachment.decompile(attachmentsObj.getJSONObject(s)));
+ }
+ } else {
attachments = null;
}
diff --git a/src/main/java/com/seailz/discordjar/model/interaction/data/command/ResolvedCommandOption.java b/src/main/java/com/seailz/discordjar/model/interaction/data/command/ResolvedCommandOption.java
index 427e98d9..be9397b0 100644
--- a/src/main/java/com/seailz/discordjar/model/interaction/data/command/ResolvedCommandOption.java
+++ b/src/main/java/com/seailz/discordjar/model/interaction/data/command/ResolvedCommandOption.java
@@ -3,12 +3,14 @@
import com.seailz.discordjar.core.Compilerable;
import com.seailz.discordjar.command.CommandOptionType;
import com.seailz.discordjar.model.interaction.data.ResolvedData;
+import com.seailz.discordjar.model.message.Attachment;
import com.seailz.discordjar.model.role.Role;
import com.seailz.discordjar.model.user.User;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -87,6 +89,10 @@ public int getAsInt() {
public boolean getAsBoolean() {
return (boolean) data;
}
+ public double getAsDouble() {
+ BigDecimal bd = (BigDecimal) data;
+ return bd.doubleValue();
+ }
/**
* Returns a raw version of the data of the option.
@@ -102,6 +108,9 @@ public Role getAsRole() {
public User getAsUser() {
return this.resolved.users().get(getAsString());
}
+ public Attachment getAsAttachment() {
+ return this.resolved.attachments().get(getAsString());
+ }
public CommandOptionType type() {
return type;
@@ -111,6 +120,10 @@ public List options() {
return options;
}
+ public ResolvedData resolved() {
+ return resolved;
+ }
+
public boolean focused() {
return focused;
}
diff --git a/src/main/java/com/seailz/discordjar/model/message/Message.java b/src/main/java/com/seailz/discordjar/model/message/Message.java
index e6448849..061e0925 100644
--- a/src/main/java/com/seailz/discordjar/model/message/Message.java
+++ b/src/main/java/com/seailz/discordjar/model/message/Message.java
@@ -28,6 +28,7 @@
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -416,7 +417,7 @@ public void delete() throws DiscordRequest.UnhandledDiscordAPIErrorException {
* @return The MessageEditAction object.
*/
public MessageEditAction edit() {
- return new MessageEditAction(channelId, discordJar, id);
+ return new MessageEditAction(channelId, discordJar, id, Arrays.stream(flags).toList().contains(MessageFlag.IS_VOICE_MESSAGE));
}
/**
diff --git a/src/main/java/com/seailz/discordjar/model/message/MessageFlag.java b/src/main/java/com/seailz/discordjar/model/message/MessageFlag.java
index 943707fa..2f666409 100644
--- a/src/main/java/com/seailz/discordjar/model/message/MessageFlag.java
+++ b/src/main/java/com/seailz/discordjar/model/message/MessageFlag.java
@@ -29,7 +29,8 @@ public enum MessageFlag {
// this message failed to mention some roles and add their members to the thread
FAILED_THREAD_MEMBER_ADD(8, false),
// this message is "silent"
- SUPPRESS_NOTICICATIONS(12, true)
+ SUPPRESS_NOTICICATIONS(12, true),
+ IS_VOICE_MESSAGE(13, true)
;
private final int id;
diff --git a/src/main/java/com/seailz/discordjar/model/user/User.java b/src/main/java/com/seailz/discordjar/model/user/User.java
index 3ece8c75..f659b789 100644
--- a/src/main/java/com/seailz/discordjar/model/user/User.java
+++ b/src/main/java/com/seailz/discordjar/model/user/User.java
@@ -235,4 +235,9 @@ public String getMentionablePrefix() {
public StringFormatter formatter() {
return new StringFormatter("avatars", id, avatarHash());
}
+
+ @Override
+ public String iconHash() {
+ return avatarHash;
+ }
}
diff --git a/src/main/java/com/seailz/discordjar/utils/CDNAble.java b/src/main/java/com/seailz/discordjar/utils/CDNAble.java
index 2dc346e5..2c0c70d5 100644
--- a/src/main/java/com/seailz/discordjar/utils/CDNAble.java
+++ b/src/main/java/com/seailz/discordjar/utils/CDNAble.java
@@ -9,8 +9,10 @@
public interface CDNAble {
StringFormatter formatter();
+ String iconHash();
default String imageUrl() {
+ if (iconHash() == null) return null;
return formatter().format("https://cdn.discordapp.com/{0}/{1}/{2}.png");
}
diff --git a/src/main/java/com/seailz/discordjar/utils/annotation/RequireCustomId.java b/src/main/java/com/seailz/discordjar/utils/annotation/RequireCustomId.java
new file mode 100644
index 00000000..0794edbf
--- /dev/null
+++ b/src/main/java/com/seailz/discordjar/utils/annotation/RequireCustomId.java
@@ -0,0 +1,32 @@
+package com.seailz.discordjar.utils.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * If you annotate a method with this and that method is a interaction event listener method,
+ *
then the event will only run if the customid of the event == the value of this annotation.
+ *
Example:
+ *
+ *
+ *
@EventMethod
+ *
@RequireCustomId("my_custom_id")
+ *
public void onModalInteractionEvent(@NotNull ModalInteractionEvent event) {
+ *
// This will only run if the custom id of the event is "my_custom_id"
+ *
event.reply("The custom ID is my_custom_id!").run();
+ *
}
+ *
+ *
+ * It's also a regex, so you can do things like this: @RequireCustomId("my_custom_id|my_custom_id2")
which will match both "my_custom_id" and "my_custom_id2" ,
+ * or @RequireCustomId("my_custom_id-.*")
which will match any custom id that starts with "my_custom_id-".
+ *
+ * Using this annotation, however, will cause some performance loss, so use it sparingly or if time is not a concern.
+ * @author Seailz
+ * @since 1.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({java.lang.annotation.ElementType.METHOD})
+public @interface RequireCustomId {
+ String value();
+}
diff --git a/src/main/java/com/seailz/discordjar/utils/permission/Permission.java b/src/main/java/com/seailz/discordjar/utils/permission/Permission.java
index fe12a401..f9c54018 100644
--- a/src/main/java/com/seailz/discordjar/utils/permission/Permission.java
+++ b/src/main/java/com/seailz/discordjar/utils/permission/Permission.java
@@ -97,7 +97,9 @@ public enum Permission implements Bitwiseable {
// Allows for viewing role subscription insights
VIEW_CREATOR_MONETIZATION_ANALYTICS(41),
// Allows for using soundboard in a voice channel
- USE_SOUNDBOARD(42)
+ USE_SOUNDBOARD(42),
+ // Allows sending voice messages
+ SEND_VOICE_MESSAGES(46),
;
private final int code;
diff --git a/src/main/java/com/seailz/discordjar/utils/rest/DiscordRequest.java b/src/main/java/com/seailz/discordjar/utils/rest/DiscordRequest.java
index 4952f092..f8d5446d 100644
--- a/src/main/java/com/seailz/discordjar/utils/rest/DiscordRequest.java
+++ b/src/main/java/com/seailz/discordjar/utils/rest/DiscordRequest.java
@@ -3,6 +3,8 @@
import com.seailz.discordjar.DiscordJar;
import com.seailz.discordjar.utils.URLS;
import com.seailz.discordjar.utils.rest.ratelimit.Bucket;
+import okhttp3.*;
+import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -88,19 +90,15 @@ public void queueRequest(double resetAfter, Bucket bucket) {
private DiscordResponse invoke(String contentType, boolean auth) throws UnhandledDiscordAPIErrorException {
try {
String url = URLS.BASE_URL + this.url;
- URL obj = new URL(url);
-
- HttpRequest.Builder con = HttpRequest.newBuilder();
- con.uri(obj.toURI());
+ OkHttpClient client = new OkHttpClient();
+ Request.Builder requestBuilder = new Request.Builder().url(url);
boolean useBaseUrlForRateLimit = !url.contains("channels") && !url.contains("guilds");
Bucket bucket = djv.getBucketForUrl(url);
if (bucket != null) {
- if (bucket.getRemaining() == 0 ) {
+ if (bucket.getRemaining() == 0) {
if (djv.isDebug()) {
- Logger.getLogger("RATELIMIT").info(
- "[RATE LIMIT] Request has been rate-limited. It has been queued."
- );
+ Logger.getLogger("RATELIMIT").info("[RATE LIMIT] Request has been rate-limited. It has been queued.");
}
queueRequest(bucket.getResetAfter(), bucket);
return new DiscordResponse(429, null, null, null);
@@ -108,76 +106,94 @@ private DiscordResponse invoke(String contentType, boolean auth) throws Unhandle
}
String s = body != null ? body.toString() : aBody.toString();
+ RequestBody requestBody = null;
+
+ if (contentType == null) {
+ contentType = "application/json";
+ }
+
if (requestMethod == RequestMethod.POST) {
- con.POST(HttpRequest.BodyPublishers.ofString(s));
+ requestBody = RequestBody.create(MediaType.parse(contentType), s);
+ requestBuilder.post(requestBody);
} else if (requestMethod == RequestMethod.PATCH) {
- con.method("PATCH", HttpRequest.BodyPublishers.ofString(s));
+ requestBody = RequestBody.create(MediaType.parse(contentType), s);
+ requestBuilder.patch(requestBody);
} else if (requestMethod == RequestMethod.PUT) {
- con.method("PUT", HttpRequest.BodyPublishers.ofString(s));
- }
- else if (requestMethod == RequestMethod.DELETE) {
- con.method("DELETE", HttpRequest.BodyPublishers.ofString(s));
+ requestBody = RequestBody.create(MediaType.parse(contentType), s);
+ requestBuilder.put(requestBody);
+ } else if (requestMethod == RequestMethod.DELETE) {
+ requestBody = RequestBody.create(MediaType.parse(contentType), s);
+ requestBuilder.delete(requestBody);
} else if (requestMethod == RequestMethod.GET) {
- con.GET();
+ requestBuilder.get();
} else {
- con.method(requestMethod.name(), HttpRequest.BodyPublishers.ofString(s));
+ requestBody = RequestBody.create(MediaType.parse(contentType), s);
+ requestBuilder.method(requestMethod.name(), requestBody);
}
- con.header("User-Agent", "discord.jar (https://github.com/discord-jar/, 1.0.0)");
- if (auth) con.header("Authorization", "Bot " + djv.getToken());
- if (contentType == null) con.header("Content-Type", "application/json");
- if (contentType != null) con.header("Content-Type", contentType); // switch to url search params
- headers.forEach(con::header);
-
- HttpRequest request = con.build();
- HttpClient client = HttpClient.newHttpClient();
+ requestBuilder.addHeader("User-Agent", "discord.jar (https://github.com/discord-jar/, 1.0.0)");
+ if (auth) {
+ requestBuilder.addHeader("Authorization", "Bot " + djv.getToken());
+ }
+ if (contentType == null) {
+ requestBuilder.addHeader("Content-Type", "application/json");
+ }
+ if (contentType != null) {
+ requestBuilder.addHeader("Content-Type", contentType);
+ }
+ headers.forEach((key, value) -> requestBuilder.addHeader(key, value));
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ Request request = requestBuilder.build();
+ Response response = client.newCall(request).execute();
- int responseCode = response.statusCode();
+ int responseCode = response.code();
+ String sb = response.body().string();
if (djv.isDebug()) {
- System.out.println(request.method() + " " + request.uri() + " with " + body + " returned " + responseCode + " with " + response.body());
+ System.out.println(request.method() + " " + request.url() + " with " + body + " returned " + responseCode + " with " + sb);
}
HashMap headers = new HashMap<>();
- response.headers().map().forEach((key, value) -> headers.put(key, value.get(0)));
+ Headers responseHeaders = response.headers();
+ for (String name : responseHeaders.names()) {
+ headers.put(name, responseHeaders.get(name));
+ }
// check headers for rate-limit
- if (response.headers().map().containsKey("X-RateLimit-Bucket")) {
- String bucketId = response.headers().map().get("X-RateLimit-Bucket").get(0);
+ if (responseHeaders.get("X-RateLimit-Bucket") != null) {
+ String bucketId = responseHeaders.get("X-RateLimit-Bucket");
Bucket buck = djv.getBucket(bucketId);
List affectedRoutes = buck == null ? new ArrayList<>() : new ArrayList<>(buck.getAffectedRoutes());
- if (useBaseUrlForRateLimit) affectedRoutes.add(baseUrl);
- else affectedRoutes.add(url);
+ if (useBaseUrlForRateLimit) {
+ affectedRoutes.add(baseUrl);
+ } else {
+ affectedRoutes.add(url);
+ }
djv.updateBucket(bucketId, new Bucket(
- bucketId, Integer.parseInt(response.headers().map().get("X-RateLimit-Remaining").get(0)),
- Double.parseDouble(response.headers().map().get(
- "X-RateLimit-Reset"
- ).get(0))
+ bucketId,
+ Integer.parseInt(responseHeaders.get("X-RateLimit-Remaining")),
+ Double.parseDouble(responseHeaders.get("X-RateLimit-Reset"))
).setAffectedRoutes(affectedRoutes));
}
-
try {
- if (response.body().startsWith("[")) {
- new JSONArray(response.body());
+ String responseBody = sb;
+ if (responseBody.startsWith("[")) {
+ new JSONArray(responseBody);
} else {
- new JSONObject(response.body());
+ new JSONObject(responseBody);
}
} catch (JSONException err) {
- System.out.println(response.body());
+ System.out.println(sb);
}
if (responseCode == 429) {
if (djv.isDebug()) {
- Logger.getLogger("RateLimit").warning("[RATE LIMIT] Rate limit has been exceeded. Please make sure" +
- " you are not sending too many requests.");
+ Logger.getLogger("RateLimit").warning("[RATE LIMIT] Rate limit has been exceeded. Please make sure you are not sending too many requests.");
}
- JSONObject body = new JSONObject(response.body());
- Bucket exceededBucket = djv.getBucket(response.headers().map().get("X-RateLimit-Bucket").get(0));
- //queueRequest(Double.parseDouble(response.headers().map().get("X-RateLimit-Reset").get(0)), exceededBucket);
+ JSONObject body = new JSONObject(sb);
if (body.has("retry_after")) {
+ String finalContentType = contentType;
new Thread(() -> {
try {
Thread.sleep((long) (body.getFloat("retry_after") * 1000));
@@ -185,58 +201,57 @@ else if (requestMethod == RequestMethod.DELETE) {
e.printStackTrace();
}
try {
- invoke(contentType, auth);
+ invoke(finalContentType, auth);
} catch (UnhandledDiscordAPIErrorException e) {
throw new RuntimeException(e);
}
}).start();
} else {
- queueRequest(Double.parseDouble(response.headers().map().get("X-RateLimit-Reset").get(0)), exceededBucket);
+ if (responseHeaders.get("X-RateLimit-Bucket") == null || responseHeaders.get("X-RateLimit-Reset") == null) {
+ return new DiscordResponse(429, null, null, null);
+ }
+ Bucket exceededBucket = djv.getBucket(responseHeaders.get("X-RateLimit-Bucket"));
+ queueRequest(Double.parseDouble(responseHeaders.get("X-RateLimit-Reset")), exceededBucket);
}
if (body.getBoolean("global")) {
- Logger.getLogger("RateLimit").severe(
- "[RATE LIMIT] This seems to be a global rate limit. If you are not sending a huge amount" +
- " of requests unexpectedly, and your bot is in a lot of servers (100k+), contact Discord" +
- " developer support."
- );
+ Logger.getLogger("RateLimit").severe("[RATE LIMIT] This seems to be a global rate limit. If you are not sending a huge amount of requests unexpectedly, and your bot is in a lot of servers (100k+), contact Discord developer support.");
}
return new DiscordResponse(429, null, null, null);
}
if (responseCode == 200 || responseCode == 201) {
-
- var body = new Object();
-
- if (response.body().startsWith("[")) {
- body = new JSONArray(response.body());
+ Object body;
+ String responseBody = sb;
+ if (responseBody.startsWith("[")) {
+ body = new JSONArray(responseBody);
} else {
- body = new JSONObject(response.body());
+ try {
+ body = new JSONObject(responseBody);
+ } catch (JSONException err) {
+ throw new DiscordUnexpectedError(new RuntimeException("Invalid JSON response from Discord API: " + responseBody));
+ }
}
return new DiscordResponse(responseCode, (body instanceof JSONObject) ? (JSONObject) body : null, headers, (body instanceof JSONArray) ? (JSONArray) body : null);
}
- if (responseCode == 204) return null;
-
- if (responseCode == 201) return null;
+ if (responseCode == 204) {
+ return null;
+ }
- if (!auth && responseCode == 401) {
+ if (responseCode == 401 && !auth) {
return new DiscordResponse(401, null, null, null);
}
- JSONObject error = new JSONObject(response.body());
+ JSONObject error = new JSONObject(sb);
JSONArray errorArray;
if (responseCode == 404) {
- Logger.getLogger("DISCORDJAR")
- .warning("Received 404 error from the Discord API. It's likely that you're trying to access a resource that doesn't exist.");
+ Logger.getLogger("DISCORDJAR").warning("Received 404 error from the Discord API. It's likely that you're trying to access a resource that doesn't exist.");
return new DiscordResponse(404, null, null, null);
}
- throw new UnhandledDiscordAPIErrorException(
- responseCode,
- "Unhandled Discord API Error. Please report this to the developer of DiscordJar." + error
- );
- } catch (InterruptedException | IOException | URISyntaxException e) {
+ throw new UnhandledDiscordAPIErrorException(new JSONObject(sb));
+ } catch (IOException e) {
// attempt gateway reconnect
throw new DiscordUnexpectedError(e);
}
@@ -340,7 +355,6 @@ public DiscordResponse invokeWithFiles(File... files) {
HashMap headers = new HashMap<>();
response.headers().map().forEach((key, value) -> headers.put(key, value.get(0)));
- System.out.println(response.body());
if (responseCode == 200 || responseCode == 201) {
@@ -358,38 +372,8 @@ public DiscordResponse invokeWithFiles(File... files) {
JSONObject error = new JSONObject(response.body());
- JSONArray errorArray;
- try {
- errorArray = error.getJSONArray("errors").getJSONArray(3);
- } catch (JSONException e) {
- try {
- errorArray = error.getJSONArray("errors").getJSONArray(1);
- } catch (JSONException ex) {
- try {
- errorArray = error.getJSONArray("errors").getJSONArray(0);
- } catch (JSONException exx) {
- throw new UnhandledDiscordAPIErrorException(
- responseCode,
- "Unhandled Discord API Error. Please report this to the developer of DiscordJar." + error
- );
- }
- }
- }
-
- errorArray.forEach(o -> {
- JSONObject errorObject = (JSONObject) o;
- try {
- throw new DiscordAPIErrorException(
- responseCode,
- errorObject.getString("code"),
- errorObject.getString("message"),
- error.toString()
- );
- } catch (DiscordAPIErrorException e) {
- throw new RuntimeException(e);
- }
- });
+ throw new UnhandledDiscordAPIErrorException(error);
} catch (Exception e) {
e.printStackTrace();
}
@@ -404,8 +388,25 @@ public DiscordAPIErrorException(int code, String errorCode, String error, String
}
public static class UnhandledDiscordAPIErrorException extends Exception {
- public UnhandledDiscordAPIErrorException(int code, String error) {
- super("DiscordAPI [Error " + HttpStatus.valueOf(code) + "]: " + error);
+ private int code;
+ private JSONObject body;
+ private String error;
+ public UnhandledDiscordAPIErrorException(JSONObject body) {
+ this.body = body.has("errors") ? body.getJSONObject("errors") : body;
+ this.code = body.getInt("code");
+ this.error = body.getString("message");
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public JSONObject getBody() {
+ return body;
+ }
+
+ public String getError() {
+ return error;
}
}
diff --git a/src/main/java/com/seailz/discordjar/utils/rest/Response.java b/src/main/java/com/seailz/discordjar/utils/rest/Response.java
new file mode 100644
index 00000000..9d298888
--- /dev/null
+++ b/src/main/java/com/seailz/discordjar/utils/rest/Response.java
@@ -0,0 +1,120 @@
+package com.seailz.discordjar.utils.rest;
+
+import org.json.JSONObject;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Response from Discord's API.
+ * @see DiscordRequest
+ * @author Seailz
+ * @since 1.0.0
+ */
+public class Response {
+
+ private final CompletableFuture responseFuture = new CompletableFuture<>();
+ private final CompletableFuture errorFuture = new CompletableFuture<>();
+ private boolean throwOnError = false;
+
+ public Response() {}
+
+ public T awaitCompleted() {
+ return responseFuture.join();
+ }
+
+ public Error awaitError() {
+ return errorFuture.join();
+ }
+
+ public Error getError() {
+ return errorFuture.getNow(null);
+ }
+
+ public T getResponse() {
+ return responseFuture.getNow(null);
+ }
+
+ /**
+ * Tells the class to throw a runtime exception if an error is received.
+ */
+ public Response throwOnError() {
+ throwOnError = true;
+ return this;
+ }
+
+ public Response complete(T response) {
+ responseFuture.complete(response);
+ return this;
+ }
+
+ public Response completeError(Error error) {
+ errorFuture.complete(error);
+ if (throwOnError) {
+ throw new DiscordResponseError(error);
+ }
+ return this;
+ }
+
+ public Response onCompletion(Consumer consumer) {
+ responseFuture.thenAccept(consumer);
+ return this;
+ }
+
+ public Response onError(Consumer consumer) {
+ errorFuture.thenAccept(consumer);
+ return this;
+ }
+
+ public Response completeAsync(Supplier response) {
+ CompletableFuture.supplyAsync(response).thenAccept(this::complete);
+ return this;
+ }
+
+ public static class DiscordResponseError extends RuntimeException {
+ private final Error error;
+
+ public DiscordResponseError(Error error) {
+ super(error.getMessage());
+ this.error = error;
+ }
+
+ public Error getError() {
+ return error;
+ }
+ }
+
+ public static class Error {
+ private int code;
+ private String message;
+ private JSONObject errors;
+
+ public Error(int code, String message, JSONObject errors) {
+ this.code = code;
+ this.message = message;
+ this.errors = errors;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public JSONObject getErrors() {
+ return errors;
+ }
+
+ @Override
+ public String toString() {
+ return "Error{" +
+ "code=" + code +
+ ", message='" + message + '\'' +
+ ", errors=" + errors +
+ '}';
+ }
+ }
+}
\ No newline at end of file