Skip to content

Commit

Permalink
Hacky User Installable Apps support
Browse files Browse the repository at this point in the history
This is a prototype only for fun, a lot of checks were removed and user installable apps when used in a guild that doesn't have the bot just acts like it is a private channel
  • Loading branch information
MrPowerGamerBR committed Mar 20, 2024
1 parent bdd230c commit 5137492
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 17 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ Keep in mind that this repo is constantly `git reset --hard`'d, we always reset
* Added `JDA#shutdown(closeCode)` and `JDA#shutdownNow(closeCode)`, which is useful if you want to shutdown the WebSocket but don't want to invalidate your current session.
* Added `getId()` to `StickerFormat`
* Changed `WebSocketClient#handleEvent` from `protected` to `public`
* Hacky User Installable Apps support
* This is a prototype only for fun, a lot of checks were removed and user installable apps when used in a guild that doesn't have the bot just acts like it is a private channel. Use `setIntegrationTypes(...)` and `setInteractionContextTypes(...)` when registering a command to enable commands in DM and group DMs.

## 👀 Examples

* [Bot with Session Checkpoint and Gateway Resuming](/src/examples/java/SessionCheckpointAndGatewayResumeExample.kt)
* [Bot with Session Checkpoint and Gateway Resuming](/src/examples/java/SessionCheckpointAndGatewayResumeExample.kt)
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,67 @@ public int getId()
}
}

enum IntegrationType
{
UNKNOWN(-1),
GUILD_INSTALL(0),
USER_INSTALL(1);

private final int id;

IntegrationType(int id)
{
this.id = id;
}

@Nonnull
public static IntegrationType fromId(int id)
{
for (IntegrationType type : values())
{
if (type.id == id)
return type;
}
return UNKNOWN;
}

public int getId()
{
return id;
}
}

enum InteractionContextType
{
UNKNOWN(-1),
GUILD(0),
BOT_DM(1),
PRIVATE_CHANNEL(2);

private final int id;

InteractionContextType(int id)
{
this.id = id;
}

@Nonnull
public static InteractionContextType fromId(int id)
{
for (InteractionContextType type : values())
{
if (type.id == id)
return type;
}
return UNKNOWN;
}

public int getId()
{
return id;
}
}

/**
* Predefined choice used for options.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Map;

/**
Expand Down Expand Up @@ -167,6 +168,12 @@ public interface CommandData extends SerializableData
@Nonnull
CommandData setNSFW(boolean nsfw);

@Nonnull
CommandData setIntegrationTypes(@Nonnull Command.IntegrationType integrationType, @Nonnull Command.IntegrationType... integrationTypes);

@Nonnull
CommandData setInteractionContextTypes(@Nonnull Command.InteractionContextType interactionContextType, @Nonnull Command.InteractionContextType... interactionContextTypes);

/**
* The current command name
*
Expand Down Expand Up @@ -203,6 +210,12 @@ public interface CommandData extends SerializableData
@Nonnull
DefaultMemberPermissions getDefaultPermissions();

@Nonnull
EnumSet<Command.IntegrationType> getIntegrationTypes();

@Nonnull
EnumSet<Command.InteractionContextType> getInteractionContextTypes();

/**
* Whether the command can only be used inside a guild.
* <br>Always true for guild commands.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
import net.dv8tion.jda.internal.utils.Checks;
import net.dv8tion.jda.internal.utils.localization.LocalizationUtils;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
Expand Down Expand Up @@ -122,6 +124,14 @@ public interface SlashCommandData extends CommandData
@Nonnull
SlashCommandData setDescriptionLocalizations(@Nonnull Map<DiscordLocale, String> map);

@Nonnull
@Override
CommandData setIntegrationTypes(@Nonnull Command.IntegrationType integrationType, @Nonnull Command.IntegrationType... integrationTypes);

@Nonnull
@Override
CommandData setInteractionContextTypes(@Nonnull Command.InteractionContextType interactionContextType, @Nonnull Command.InteractionContextType... interactionContextTypes);

/**
* The configured description
*
Expand All @@ -138,6 +148,14 @@ public interface SlashCommandData extends CommandData
@Nonnull
LocalizationMap getDescriptionLocalizations();

@Nonnull
@Override
EnumSet<Command.IntegrationType> getIntegrationTypes();

@Nonnull
@Override
EnumSet<Command.InteractionContextType> getInteractionContextTypes();

/**
* Removes all options that evaluate to {@code true} under the provided {@code condition}.
* <br>This will not affect options within subcommands.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,19 @@ protected Long handleInternally(DataObject content)
Guild guild = api.getGuildById(guildId);
if (api.getGuildSetupController().isLocked(guildId))
return guildId;
if (guildId != 0 && guild == null)
return null; // discard event if it is not from a guild we are currently in
// Don't discard - Interactions with user installed bots may happen in guilds that we aren't in
// if (guildId != 0 && guild == null)
// return null; // discard event if it is not from a guild we are currently in

// Check channel type
DataObject channelJson = content.getObject("channel");
ChannelType channelType = ChannelType.fromId(channelJson.getInt("type"));
if (!channelType.isMessage() || channelType == ChannelType.GROUP)
{
WebSocketClient.LOG.debug("Discarding INTERACTION_CREATE event from unexpected channel type. Channel: {}", channelJson);
return null;
}
// Don't discard - Interactions with user installed bots may happen in GDMs
// if (!channelType.isMessage() || channelType == ChannelType.GROUP)
// {
// WebSocketClient.LOG.debug("Discarding INTERACTION_CREATE event from unexpected channel type. Channel: {}", channelJson);
// return null;
// }

switch (InteractionType.fromKey(type))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@
import net.dv8tion.jda.internal.interactions.command.localization.LocalizationMapper;
import net.dv8tion.jda.internal.utils.Checks;
import net.dv8tion.jda.internal.utils.Helpers;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nonnull;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CommandDataImpl implements SlashCommandData
Expand All @@ -50,6 +52,9 @@ public class CommandDataImpl implements SlashCommandData
private boolean guildOnly = false;
private boolean nsfw = false;
private DefaultMemberPermissions defaultMemberPermissions = DefaultMemberPermissions.ENABLED;
private EnumSet<Command.IntegrationType> integrationTypes = EnumSet.of(Command.IntegrationType.GUILD_INSTALL);
// TODO: This is actually not "all", the docs says that it registers all contexts by default but in my experience that's not what happens
private EnumSet<Command.InteractionContextType> contexts = EnumSet.of(Command.InteractionContextType.GUILD, Command.InteractionContextType.BOT_DM, Command.InteractionContextType.PRIVATE_CHANNEL);

private final Command.Type type;

Expand Down Expand Up @@ -108,7 +113,9 @@ public DataObject toData()
.put("default_member_permissions", defaultMemberPermissions == DefaultMemberPermissions.ENABLED
? null
: Long.toUnsignedString(defaultMemberPermissions.getPermissionsRaw()))
.put("name_localizations", nameLocalizations);
.put("name_localizations", nameLocalizations)
.put("integration_types", DataArray.fromCollection(integrationTypes.stream().map(Command.IntegrationType::getId).collect(Collectors.toList())))
.put("contexts", DataArray.fromCollection(contexts.stream().map(Command.InteractionContextType::getId).collect(Collectors.toList())));

if (type == Command.Type.SLASH)
{
Expand All @@ -132,6 +139,20 @@ public DefaultMemberPermissions getDefaultPermissions()
return defaultMemberPermissions;
}

@NotNull
@Override
public EnumSet<Command.IntegrationType> getIntegrationTypes()
{
return EnumSet.copyOf(integrationTypes);
}

@NotNull
@Override
public EnumSet<Command.InteractionContextType> getInteractionContextTypes()
{
return EnumSet.copyOf(contexts);
}

@Override
public boolean isGuildOnly()
{
Expand Down Expand Up @@ -199,6 +220,28 @@ public CommandDataImpl setNSFW(boolean nsfw)
return this;
}

@Nonnull
@Override
public CommandData setIntegrationTypes(@Nonnull Command.IntegrationType integrationType, @Nonnull Command.IntegrationType... integrationTypes)
{
Checks.notNull(integrationType, "Integration Type");
Checks.noneNull(integrationTypes, "Integration Type");
EnumSet<Command.IntegrationType> set = EnumSet.of(integrationType, integrationTypes);
this.integrationTypes = set;
return this;
}

@Nonnull
@Override
public CommandData setInteractionContextTypes(@Nonnull Command.InteractionContextType interactionContextType, @Nonnull Command.InteractionContextType... interactionContextTypes)
{
Checks.notNull(interactionContextType, "Interaction Context Type");
Checks.noneNull(interactionContextTypes, "Interaction Context Type");
EnumSet<Command.InteractionContextType> set = EnumSet.of(interactionContextType, interactionContextTypes);
this.contexts = set;
return this;
}

@Nonnull
@Override
public CommandDataImpl addOptions(@Nonnull OptionData... options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public InteractionImpl(JDAImpl jda, DataObject data)
this.id = data.getUnsignedLong("id");
this.token = data.getString("token");
this.type = data.getInt("type");
this.guild = jda.getGuildById(data.getUnsignedLong("guild_id", 0L));
long guildId = data.getUnsignedLong("guild_id", 0L);
this.guild = jda.getGuildById(guildId);
this.channelId = data.getUnsignedLong("channel_id", 0L);
this.userLocale = DiscordLocale.from(data.getString("locale", "en-US"));

Expand All @@ -77,13 +78,35 @@ public InteractionImpl(JDAImpl jda, DataObject data)
throw new IllegalStateException("Failed to create channel instance for interaction! Channel Type: " + channelJson.getInt("type"));
this.channel = channel;
}
else
{
else if (guildId != 0L) {
// User installed application in a guild
this.member = null;
long channelId = channelJson.getUnsignedLong("id");

// Let's pretend that this is a private channel, in reality this is a guild channel in a guild that we aren't in
DataObject memberData = data.getObject("member");
DataObject userData = memberData.getObject("user");
PrivateChannel channel = jda.getEntityBuilder().createPrivateChannel(
DataObject.empty()
.put("id", channelId)
.put("recipient", userData)
);
this.channel = channel;

User user = channel.getUser();
if (user == null)
{
user = jda.getEntityBuilder().createUser(userData);
((PrivateChannelImpl) channel).setUser(user);
((UserImpl) user).setPrivateChannel(channel);
}
this.user = user;
} else {
member = null;
long channelId = channelJson.getUnsignedLong("id");
ChannelType type = ChannelType.fromId(channelJson.getInt("type"));
if (type != ChannelType.PRIVATE)
throw new IllegalArgumentException("Received interaction in unexpected channel type! Type " + type + " is not supported yet!");
// ChannelType type = ChannelType.fromId(channelJson.getInt("type"));
// if (type != ChannelType.PRIVATE)
// throw new IllegalArgumentException("Received interaction in unexpected channel type! Type " + type + " is not supported yet!");
PrivateChannel channel = jda.getPrivateChannelById(channelId);
if (channel == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ protected Message parse(DataObject interaction, DataObject resolved)
{
long guildId = interaction.getUnsignedLong("guild_id");
guild = api.getGuildById(guildId);
if (guild == null)
throw new IllegalStateException("Cannot find guild for resolved message object.");
// TODO: Check if the guild is authorized in "authorizing_integration_owners", if yes, then this should NOT be null and should throw
// if (guild == null)
// throw new IllegalStateException("Cannot find guild for resolved message object.");
}

return api.getEntityBuilder().createMessageWithLookup(message, guild, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import net.dv8tion.jda.api.interactions.DiscordLocale;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
Expand All @@ -37,6 +38,7 @@
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nonnull;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -210,6 +212,22 @@ public CommandCreateAction setDescriptionLocalizations(@Nonnull Map<DiscordLocal
return this;
}

@Nonnull
@Override
public CommandData setIntegrationTypes(@Nonnull Command.IntegrationType integrationType, @Nonnull Command.IntegrationType... integrationTypes)
{
data.setIntegrationTypes(integrationType, integrationTypes);
return this;
}

@Nonnull
@Override
public CommandData setInteractionContextTypes(@Nonnull Command.InteractionContextType interactionContextType, @Nonnull Command.InteractionContextType... interactionContextTypes)
{
data.setInteractionContextTypes(interactionContextType, interactionContextTypes);
return this;
}

@Nonnull
@Override
public String getDescription()
Expand All @@ -224,6 +242,20 @@ public LocalizationMap getDescriptionLocalizations()
return data.getDescriptionLocalizations();
}

@Nonnull
@Override
public EnumSet<Command.IntegrationType> getIntegrationTypes()
{
return data.getIntegrationTypes();
}

@Nonnull
@Override
public EnumSet<Command.InteractionContextType> getInteractionContextTypes()
{
return data.getInteractionContextTypes();
}

@Override
public boolean removeOptions(@NotNull Predicate<? super OptionData> condition)
{
Expand Down

0 comments on commit 5137492

Please sign in to comment.