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 Ghsponsors Singular badge - - - Kofi Singular badge - 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 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 eventType = (Class) 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 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