diff --git a/src/main/java/com/seailz/discordjar/DiscordJar.java b/src/main/java/com/seailz/discordjar/DiscordJar.java index 463b19f0..10ff9979 100644 --- a/src/main/java/com/seailz/discordjar/DiscordJar.java +++ b/src/main/java/com/seailz/discordjar/DiscordJar.java @@ -494,7 +494,7 @@ public User getSelfUser() { if (getSelfUserCache == null) { getSelfUserCache = JsonCache.newc(response.body(), req); - getSelfUserCache.reset(60000); + getSelfUserCache.reset(60000, "self-user-cache"); } this.getSelfUserCache.update(response.body()); return User.decompile(response.body(), this); diff --git a/src/main/java/com/seailz/discordjar/action/guild/GetCurrentUserGuildsAction.java b/src/main/java/com/seailz/discordjar/action/guild/GetCurrentUserGuildsAction.java index 9278c672..b8dce67b 100644 --- a/src/main/java/com/seailz/discordjar/action/guild/GetCurrentUserGuildsAction.java +++ b/src/main/java/com/seailz/discordjar/action/guild/GetCurrentUserGuildsAction.java @@ -112,7 +112,7 @@ public List run() { } List returnGuilds = new ArrayList<>(); - response.arr().forEach(guild -> returnGuilds.add(Guild.decompile((JSONObject) guild, discordJar))); + response.arr().forEach(guild -> returnGuilds.add(Guild.decompile((JSONObject) guild, discordJar, false))); returnGuilds.forEach(g -> discordJar.getGuildCache().cache(g)); return returnGuilds; diff --git a/src/main/java/com/seailz/discordjar/cache/Cache.java b/src/main/java/com/seailz/discordjar/cache/Cache.java index b76f125c..cef5a35c 100644 --- a/src/main/java/com/seailz/discordjar/cache/Cache.java +++ b/src/main/java/com/seailz/discordjar/cache/Cache.java @@ -119,7 +119,20 @@ public List getCache() { } /** - * Gets an item from the cache + * This method is used to get an item from the cache by its ID. + *
Unlike {@link #getById(String)}, this method will not make a request to Discord if the item is not in the cache, and will instead return null. + * @param id The ID of the item to get + * @return The item, or null if it is not in the cache + */ + public T returnFromCache(String id) { + if (!discordJar.getCacheTypes().contains(type) && !discordJar.getCacheTypes().contains(CacheType.ALL)) return null; + return getFromCacheByIdOrNull(id); + } + + /** + * Gets an item from the cache. + *
If the provided ID matches no items in the cache, a request will be made to Discord to get the item. + *
If the item is not in the cache, and the request to Discord fails, null will be returned. * * @param id The id of the item to get * @return The item diff --git a/src/main/java/com/seailz/discordjar/cache/JsonCache.java b/src/main/java/com/seailz/discordjar/cache/JsonCache.java index fa6d174e..8182f484 100644 --- a/src/main/java/com/seailz/discordjar/cache/JsonCache.java +++ b/src/main/java/com/seailz/discordjar/cache/JsonCache.java @@ -7,6 +7,7 @@ import org.json.JSONObject; import java.util.Random; +import java.util.logging.Logger; /** * A cache that can be used to store JSON objects. @@ -62,7 +63,8 @@ public interface JsonCache { * * @param interval The interval on which to invalidate the cache. */ - default void reset(int interval) { + default void reset(int interval, String origin) { + Logger.getLogger(origin).info("Starting cache invalidation timer for " + origin + " with interval " + interval + "ms"); new Thread(() -> { while (true) { try { @@ -75,6 +77,18 @@ default void reset(int interval) { }, "djar--CacheInvalidate" + new Random().nextInt(999)).start(); } + default void resetSingle(int interval, String origin) { + Logger.getLogger(origin).info("Starting cache invalidation timer for " + origin + " with interval " + interval + "ms"); + new Thread(() -> { + try { + Thread.sleep(interval); + } catch (InterruptedException e) { + e.printStackTrace(); + } + invalidate(); + }, "djar--CacheInvalidate" + new Random().nextInt(999)).start(); + } + /** * Updates the cached object. *

diff --git a/src/main/java/com/seailz/discordjar/events/model/guild/GuildCreateEvent.java b/src/main/java/com/seailz/discordjar/events/model/guild/GuildCreateEvent.java index 9a2bfa6f..4eb3e018 100644 --- a/src/main/java/com/seailz/discordjar/events/model/guild/GuildCreateEvent.java +++ b/src/main/java/com/seailz/discordjar/events/model/guild/GuildCreateEvent.java @@ -21,6 +21,6 @@ public GuildCreateEvent(@NotNull DiscordJar bot, long sequence, @NotNull JSONObj @NotNull public Guild getGuild() { - return Guild.decompile(getJson().getJSONObject("d"), getBot()); + return Guild.decompile(getJson().getJSONObject("d"), getBot(), false); } } diff --git a/src/main/java/com/seailz/discordjar/events/model/guild/GuildUpdateEvent.java b/src/main/java/com/seailz/discordjar/events/model/guild/GuildUpdateEvent.java index b272667d..febe3d2a 100644 --- a/src/main/java/com/seailz/discordjar/events/model/guild/GuildUpdateEvent.java +++ b/src/main/java/com/seailz/discordjar/events/model/guild/GuildUpdateEvent.java @@ -13,6 +13,6 @@ public GuildUpdateEvent(@NotNull DiscordJar bot, long sequence, @NotNull JSONObj @NotNull public Guild getGuild() { - return Guild.decompile(getJson().getJSONObject("d"), getBot()); + return Guild.decompile(getJson().getJSONObject("d"), getBot(), false); } } diff --git a/src/main/java/com/seailz/discordjar/events/model/interaction/InteractionEvent.java b/src/main/java/com/seailz/discordjar/events/model/interaction/InteractionEvent.java index c522517d..710249fa 100644 --- a/src/main/java/com/seailz/discordjar/events/model/interaction/InteractionEvent.java +++ b/src/main/java/com/seailz/discordjar/events/model/interaction/InteractionEvent.java @@ -98,6 +98,14 @@ public MessageInteractionCallbackAction replyWithEmbeds(Embeder... embeds) { return new MessageInteractionCallbackAction(InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, new InteractionMessageResponse(embeds), getInteraction().token(), getInteraction().id(), getBot()); } + /** + * Replies to this interaction with a button that will re-direct users to the premium payment window. + *
This is type {@link InteractionCallbackType#PREMIUM_REQUIRED} (10), part of app monetization/subscriptions, and will only work if the bot is monetized. + */ + public void replyRequirePremium(boolean ephemeral) { + getHandler().requirePremium(ephemeral); + } + @NotNull public InteractionHandler getHandler() { return InteractionHandler.from(getInteraction().token(), getInteraction().id(), getBot()); diff --git a/src/main/java/com/seailz/discordjar/gateway/events/DispatchedEvents.java b/src/main/java/com/seailz/discordjar/gateway/events/DispatchedEvents.java index d5b7c55a..752292f1 100644 --- a/src/main/java/com/seailz/discordjar/gateway/events/DispatchedEvents.java +++ b/src/main/java/com/seailz/discordjar/gateway/events/DispatchedEvents.java @@ -116,7 +116,7 @@ public enum DispatchedEvents { if (p.getJSONObject("d").has("unavailable") && p.getJSONObject("d").getBoolean("unavailable")) // Guild is unavailable, don't cache it return GuildCreateEvent.class; - Guild guild = Guild.decompile(p.getJSONObject("d"), g); + Guild guild = Guild.decompile(p.getJSONObject("d"), g, true); g.getGuildCache().cache(guild); JSONArray arr = p.getJSONObject("d").getJSONArray("channels"); @@ -153,14 +153,14 @@ public enum DispatchedEvents { }), GUILD_UPDATE((p, g, d) -> { // modify cached guild, if it exists - Guild guild = Guild.decompile(p.getJSONObject("d"), d); + Guild guild = Guild.decompile(p.getJSONObject("d"), d, true); d.getGuildCache().cache(guild); return GuildUpdateEvent.class; }), GUILD_DELETE((p, g, d) -> { // remove cached guild, if it exists - Guild guild = Guild.decompile(p.getJSONObject("d"), d); + Guild guild = Guild.decompile(p.getJSONObject("d"), d, true); d.getGuildCache().remove(guild); return GuildDeleteEvent.class; diff --git a/src/main/java/com/seailz/discordjar/model/application/Application.java b/src/main/java/com/seailz/discordjar/model/application/Application.java index d8f0f0b6..dd516cc3 100644 --- a/src/main/java/com/seailz/discordjar/model/application/Application.java +++ b/src/main/java/com/seailz/discordjar/model/application/Application.java @@ -277,7 +277,7 @@ public static Application decompile(JSONObject obj, DiscordJar discordJar) { } try { - guild = Guild.decompile(obj.getJSONObject("guild"), discordJar); + guild = Guild.decompile(obj.getJSONObject("guild"), discordJar, false); } catch (JSONException e) { guild = null; } 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 4739337d..4463839a 100644 --- a/src/main/java/com/seailz/discordjar/model/guild/Guild.java +++ b/src/main/java/com/seailz/discordjar/model/guild/Guild.java @@ -385,7 +385,16 @@ public JSONObject compile() { } @NotNull - public static Guild decompile(JSONObject obj, DiscordJar discordJar) { + public static Guild decompile(JSONObject obj, DiscordJar discordJar, boolean bypassCache) { + System.out.println("decomp guild"); + + // If the cache happens to contain the current guild, we'll use that instead of creating a new one. + // Helpful for slight memory optimization & performance. + Guild cachedGuild = discordJar.getGuildCache().returnFromCache(obj.getString("id")); + if (cachedGuild != null && !bypassCache) { + return cachedGuild; + } + long nano = System.nanoTime(); String id; String name; @@ -723,7 +732,6 @@ public static Guild decompile(JSONObject obj, DiscordJar discordJar) { RequestMethod.GET )) ); - g.roleCache.reset(60000); return g; } @@ -1130,6 +1138,7 @@ public List roles() { if (roleCache != null) { roleCache.update(new JSONObject().put("data", res)); + roleCache.resetSingle(60000, "roles"); } return roles; } diff --git a/src/main/java/com/seailz/discordjar/model/interaction/Interaction.java b/src/main/java/com/seailz/discordjar/model/interaction/Interaction.java index 408e279e..fb3de459 100644 --- a/src/main/java/com/seailz/discordjar/model/interaction/Interaction.java +++ b/src/main/java/com/seailz/discordjar/model/interaction/Interaction.java @@ -7,11 +7,13 @@ import com.seailz.discordjar.model.guild.Guild; import com.seailz.discordjar.model.guild.Member; import com.seailz.discordjar.model.message.Message; +import com.seailz.discordjar.model.monetization.Entitlement; import com.seailz.discordjar.model.user.User; import com.seailz.discordjar.utils.flag.BitwiseUtil; import com.seailz.discordjar.utils.permission.Permission; import com.seailz.discordjar.utils.rest.DiscordRequest; import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; import org.json.JSONObject; import java.lang.reflect.InvocationTargetException; @@ -59,12 +61,14 @@ public class Interaction implements Compilerable { private final String locale; // Guild's preferred locale, if invoked in a guild private final String guildLocale; + // For monetized apps, array of Entitlements for the invoking user + private List entitlements; private final String raw; private final DiscordJar djar; @Deprecated private final String channelId; - public Interaction(String id, Application application, InteractionType type, InteractionData data, String guild, Channel channel, JSONObject member, User user, String token, int version, Message message, String appPermissions, String locale, String guildLocale, String raw, String channelId, DiscordJar discordJar) { + public Interaction(String id, Application application, InteractionType type, InteractionData data, String guild, Channel channel, JSONObject member, User user, String token, int version, Message message, String appPermissions, String locale, String guildLocale, List entitlements, String raw, String channelId, DiscordJar discordJar) { this.id = id; this.application = application; this.type = type; @@ -87,6 +91,7 @@ public Interaction(String id, Application application, InteractionType type, Int this.raw = raw; this.channelId = channelId; this.djar = discordJar; + this.entitlements = entitlements; } @Deprecated @@ -161,6 +166,10 @@ public String raw() { return raw; } + public List entitlements() { + return entitlements; + } + @Override public JSONObject compile() { JSONObject data = null; @@ -180,6 +189,13 @@ public JSONObject compile() { } } + JSONArray entitlements = new JSONArray(); + if (this.entitlements != null) { + for (Entitlement entitlement : this.entitlements) { + entitlements.put(entitlement.compile()); + } + } + return new JSONObject() .put("id", id) .put("application", application.id()) @@ -194,7 +210,8 @@ public JSONObject compile() { .put("app_permissions", appPermissions) .put("locale", locale) .put("guild_locale", guildLocale) - .put("channel_id", channelId); + .put("channel_id", channelId) + .put("entitlements", entitlements); } @NotNull @@ -215,7 +232,15 @@ public static Interaction decompile(JSONObject json, DiscordJar discordJar) thro String guildLocale = json.has("guild_locale") ? json.getString("guild_locale") : null; String channelId = json.has("channel_id") ? json.getString("channel_id") : null; - return new Interaction(id, application, type, data, guildId, channel, json.has("member") ? json.getJSONObject("member") : null, user, token, version, message, appPermissions, locale, guildLocale, json.toString(), channelId, discordJar); + JSONArray entitlements = json.has("entitlements") ? json.getJSONArray("entitlements") : null; + List entitlementList = new ArrayList<>(); + if (entitlements != null) { + for (int i = 0; i < entitlements.length(); i++) { + entitlementList.add(Entitlement.decompile(discordJar, entitlements.getJSONObject(i))); + } + } + + return new Interaction(id, application, type, data, guildId, channel, json.has("member") ? json.getJSONObject("member") : null, user, token, version, message, appPermissions, locale, guildLocale, entitlementList, json.toString(), channelId, discordJar); } diff --git a/src/main/java/com/seailz/discordjar/model/interaction/callback/InteractionCallbackType.java b/src/main/java/com/seailz/discordjar/model/interaction/callback/InteractionCallbackType.java index b19234fc..6841ed43 100644 --- a/src/main/java/com/seailz/discordjar/model/interaction/callback/InteractionCallbackType.java +++ b/src/main/java/com/seailz/discordjar/model/interaction/callback/InteractionCallbackType.java @@ -23,6 +23,8 @@ public enum InteractionCallbackType { APPLICATION_COMMAND_AUTOCOMPLETE_RESULT(8), // respond to an interaction with a popup modal MODAL(9), + // Respond to an interaction with an upgrade button. Only available for monetized apps. + PREMIUM_REQUIRED(10), UNKNOWN(-1); private final int code; diff --git a/src/main/java/com/seailz/discordjar/model/interaction/callback/InteractionHandler.java b/src/main/java/com/seailz/discordjar/model/interaction/callback/InteractionHandler.java index 46d23928..144f1d46 100644 --- a/src/main/java/com/seailz/discordjar/model/interaction/callback/InteractionHandler.java +++ b/src/main/java/com/seailz/discordjar/model/interaction/callback/InteractionHandler.java @@ -22,6 +22,7 @@ public interface InteractionHandler { Message getFollowup(String id); void deleteFollowup(String id); + void requirePremium(boolean ephemeral); EditInteractionMessageAction editOriginalResponse(); 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 d367fb02..84a2d680 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 @@ -3,11 +3,11 @@ import com.seailz.discordjar.DiscordJar; import com.seailz.discordjar.action.interaction.EditInteractionMessageAction; import com.seailz.discordjar.action.interaction.followup.InteractionFollowupAction; +import com.seailz.discordjar.model.interaction.callback.InteractionCallbackType; import com.seailz.discordjar.model.interaction.callback.InteractionHandler; import com.seailz.discordjar.model.message.Message; import com.seailz.discordjar.utils.URLS; import com.seailz.discordjar.utils.rest.DiscordRequest; -import lombok.SneakyThrows; import org.json.JSONObject; import org.springframework.web.bind.annotation.RequestMethod; @@ -138,6 +138,22 @@ public void defer(boolean ephemeral) { } } + @Override + public void requirePremium(boolean ephemeral) { + try { + new DiscordRequest( + new JSONObject().put("type", InteractionCallbackType.PREMIUM_REQUIRED.getCode()).put("data",new JSONObject().put("flags", ephemeral ? 64 : 0)), + new HashMap<>(), + URLS.POST.INTERACTIONS.CALLBACK.replace("{interaction.id}", id).replace("{interaction.token}", token), + discordJar, + URLS.POST.INTERACTIONS.CALLBACK, + RequestMethod.POST + ).invoke(); + } catch (DiscordRequest.UnhandledDiscordAPIErrorException e) { + throw new DiscordRequest.DiscordAPIErrorException(e); + } + } + @Override public void deferEdit() { try { diff --git a/src/main/java/com/seailz/discordjar/model/invite/internal/InviteImpl.java b/src/main/java/com/seailz/discordjar/model/invite/internal/InviteImpl.java index c5fea256..f3c8712f 100644 --- a/src/main/java/com/seailz/discordjar/model/invite/internal/InviteImpl.java +++ b/src/main/java/com/seailz/discordjar/model/invite/internal/InviteImpl.java @@ -38,7 +38,7 @@ public InviteImpl(String code, Guild guild, Channel channel, User inviter, Voice public static InviteImpl decompile(JSONObject obj, DiscordJar discordJar) { String code = obj.has("code") ? obj.getString("code") : null; - Guild guild = obj.has("guild") && obj.get("guild") != JSONObject.NULL ? Guild.decompile(obj.getJSONObject("guild"), discordJar) : null; + Guild guild = obj.has("guild") && obj.get("guild") != JSONObject.NULL ? Guild.decompile(obj.getJSONObject("guild"), discordJar, false) : null; Channel channel = obj.has("channel") && obj.get("channel") != JSONObject.NULL ? Channel.decompile(obj.getJSONObject("channel"), discordJar) : null; User inviter = obj.has("inviter") && obj.get("inviter") != JSONObject.NULL ? User.decompile(obj.getJSONObject("inviter"), discordJar) : null; VoiceInviteTargetType voiceInviteTarget = obj.has("target_type") ? VoiceInviteTargetType.fromCode(obj.getInt("target_type")) : null; diff --git a/src/main/java/com/seailz/discordjar/model/invite/internal/InviteMetadataImpl.java b/src/main/java/com/seailz/discordjar/model/invite/internal/InviteMetadataImpl.java index 945d01c6..a78eb60e 100644 --- a/src/main/java/com/seailz/discordjar/model/invite/internal/InviteMetadataImpl.java +++ b/src/main/java/com/seailz/discordjar/model/invite/internal/InviteMetadataImpl.java @@ -52,7 +52,7 @@ public String createdAt() { public static InviteMetadataImpl decompile(JSONObject obj, DiscordJar discordJar) { String code = obj.has("code") ? obj.getString("code") : null; - Guild guild = obj.has("guild") && obj.get("guild") != JSONObject.NULL ? Guild.decompile(obj.getJSONObject("guild"), discordJar) : null; + Guild guild = obj.has("guild") && obj.get("guild") != JSONObject.NULL ? Guild.decompile(obj.getJSONObject("guild"), discordJar, false) : null; Channel channel = obj.has("channel") && obj.get("channel") != JSONObject.NULL ? Channel.decompile(obj.getJSONObject("channel"), discordJar) : null; User inviter = obj.has("inviter") && obj.get("inviter") != JSONObject.NULL ? User.decompile(obj.getJSONObject("inviter"), discordJar) : null; VoiceInviteTargetType voiceInviteTarget = obj.has("target_type") ? VoiceInviteTargetType.fromCode(obj.getInt("target_type")) : null;