Interaction events, add handling directly to Command

This commit is contained in:
Vankka 2022-08-07 19:54:16 +03:00
parent 2f3c9946b5
commit 602c74ba6b
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
17 changed files with 578 additions and 91 deletions

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.*;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.entity.guild.DiscordRole;
import com.discordsrv.api.discord.entity.interaction.command.Command;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
@ -116,4 +117,16 @@ public interface DiscordAPI {
*/
@NotNull
Optional<DiscordRole> getRoleById(long id);
/**
* Registers a Discord command.
* @param command the command to register
*/
Command.RegistrationResult registerCommand(Command command);
/**
* Unregisters a Discord command.
* @param command the command to unregister
*/
void unregisterCommand(Command command);
}

View File

@ -25,6 +25,7 @@ package com.discordsrv.api.discord.entity.interaction.command;
import com.discordsrv.api.discord.entity.JDAEntity;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.interaction.command.*;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.build.*;
@ -33,22 +34,34 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
/**
* A Discord command.
*/
public class Command implements JDAEntity<CommandData> {
private static final String CHAT_INPUT_NAME_REGEX = "(?U)[\\w-]{1,32}";
public static final Pattern CHAT_INPUT_NAME_PATTERN = Pattern.compile(CHAT_INPUT_NAME_REGEX);
/**
* Creates a chat input or slash command builder.
*
* @param id a unique identifier for this interaction, used to check if a given event was for this interaction
* @param name the name of the command
* @param name the name of the command, 1-32 characters alphanumeric and dashes
* @param description the description of the command
* @return a new chat input command builder
* @see com.discordsrv.api.discord.events.interaction.command.DiscordChatInputInteractionEvent
*/
public static ChatInputBuilder chatInput(ComponentIdentifier id, String name, String description) {
public static ChatInputBuilder chatInput(
ComponentIdentifier id,
@org.intellij.lang.annotations.Pattern(CHAT_INPUT_NAME_REGEX) String name,
String description
) {
if (!CHAT_INPUT_NAME_PATTERN.matcher(name).matches()) {
throw new IllegalArgumentException("Name must be alphanumeric (dashes allowed), 1 and 32 characters");
}
return new ChatInputBuilder(id, name, description);
}
@ -56,46 +69,58 @@ public class Command implements JDAEntity<CommandData> {
* Creates a new user context menu command.
*
* @param id a unique identifier for this interaction, used to check if a given event was for this interaction
* @param name the name of the command
* @param name the name of the command, 1-32 characters
* @return a new command builder
* @see com.discordsrv.api.discord.events.interaction.command.DiscordUserContextInteractionEvent
*/
public static Builder user(ComponentIdentifier id, String name) {
return new Builder(id, Type.USER, name);
public static Builder<DiscordUserContextInteractionEvent> user(
ComponentIdentifier id,
@org.intellij.lang.annotations.Pattern(".{1,32}") String name
) {
return new Builder<>(id, CommandType.USER, name);
}
/**
* Creates a new message context menu command.
*
* @param id a unique identifier for this interaction, used to check if a given event was for this interaction
* @param name the name of the command
* @param name the name of the command, 1-32 characters
* @return a new command builder
* @see com.discordsrv.api.discord.events.interaction.command.DiscordMessageContextInteractionEvent
*/
public static Builder message(ComponentIdentifier id, String name) {
return new Builder(id, Type.MESSAGE, name);
public static Builder<DiscordMessageContextInteractionEvent> message(
ComponentIdentifier id,
@org.intellij.lang.annotations.Pattern(".{1,32}") String name
) {
return new Builder<>(id, CommandType.MESSAGE, name);
}
private final ComponentIdentifier id;
private final Type type;
private final CommandType type;
private final Map<Locale, String> nameTranslations;
private final Map<Locale, String> descriptionTranslations;
private final List<SubCommandGroup> subCommandGroups;
private final List<Command> subCommands;
private final List<CommandOption> options;
private final Long guildId;
private final boolean guildOnly;
private final DefaultPermission defaultPermission;
private final Consumer<? extends AbstractCommandInteractionEvent<?>> eventHandler;
private final Consumer<DiscordCommandAutoCompleteInteractionEvent> autoCompleteHandler;
private Command(
ComponentIdentifier id,
Type type,
CommandType type,
Map<Locale, String> nameTranslations,
Map<Locale, String> descriptionTranslations,
List<SubCommandGroup> subCommandGroups,
List<Command> subCommands,
List<CommandOption> options,
Long guildId,
boolean guildOnly,
DefaultPermission defaultPermission
DefaultPermission defaultPermission,
Consumer<? extends AbstractCommandInteractionEvent<?>> eventHandler,
Consumer<DiscordCommandAutoCompleteInteractionEvent> autoCompleteHandler
) {
this.id = id;
this.type = type;
@ -104,8 +129,11 @@ public class Command implements JDAEntity<CommandData> {
this.subCommandGroups = subCommandGroups;
this.subCommands = subCommands;
this.options = options;
this.guildId = guildId;
this.guildOnly = guildOnly;
this.defaultPermission = defaultPermission;
this.eventHandler = eventHandler;
this.autoCompleteHandler = autoCompleteHandler;
}
@NotNull
@ -114,7 +142,12 @@ public class Command implements JDAEntity<CommandData> {
}
@NotNull
public Type getType() {
public Optional<Long> getGuildId() {
return Optional.ofNullable(guildId);
}
@NotNull
public CommandType getType() {
return type;
}
@ -161,6 +194,21 @@ public class Command implements JDAEntity<CommandData> {
return defaultPermission;
}
@NotNull
@SuppressWarnings("unchecked")
public <T extends AbstractCommandInteractionEvent<?>> Optional<Consumer<T>> getEventHandler() {
if (eventHandler == null) {
return Optional.empty();
}
return Optional.of((Consumer<T>) eventHandler);
}
@NotNull
public Optional<Consumer<DiscordCommandAutoCompleteInteractionEvent>> getAutoCompleteHandler() {
return Optional.ofNullable(autoCompleteHandler);
}
@Override
public CommandData asJDA() {
CommandData commandData;
@ -194,15 +242,16 @@ public class Command implements JDAEntity<CommandData> {
return data;
}
public static class ChatInputBuilder extends Builder {
public static class ChatInputBuilder extends Builder<DiscordChatInputInteractionEvent> {
private final Map<Locale, String> descriptionTranslations = new LinkedHashMap<>();
private final List<SubCommandGroup> subCommandGroups = new ArrayList<>();
private final List<Command> subCommands = new ArrayList<>();
private final List<CommandOption> options = new ArrayList<>();
private Consumer<DiscordCommandAutoCompleteInteractionEvent> autoCompleteHandler;
private ChatInputBuilder(ComponentIdentifier id, String name, String description) {
super(id, Type.CHAT_INPUT, name);
super(id, CommandType.CHAT_INPUT, name);
this.descriptionTranslations.put(Locale.ROOT, description);
}
@ -211,11 +260,11 @@ public class Command implements JDAEntity<CommandData> {
* @param locale the language
* @param translation the translation
* @return this builder, useful for chaining
* @throws IllegalStateException if this isn't a {@link Type#CHAT_INPUT} command
* @throws IllegalStateException if this isn't a {@link CommandType#CHAT_INPUT} command
*/
@NotNull
public ChatInputBuilder addDescriptionTranslation(@NotNull Locale locale, @NotNull String translation) {
if (type != Type.CHAT_INPUT) {
if (type != CommandType.CHAT_INPUT) {
throw new IllegalStateException("Descriptions are only available for CHAT_INPUT commands");
}
this.descriptionTranslations.put(locale, translation);
@ -258,6 +307,17 @@ public class Command implements JDAEntity<CommandData> {
return this;
}
/**
* Sets the auto complete handler for this command, this can be used instead of listening to the {@link DiscordCommandAutoCompleteInteractionEvent}.
* @param autoCompleteHandler the auto complete handler, only receives events for this command
* @return this builder, useful for chaining
*/
@NotNull
public ChatInputBuilder setAutoCompleteHandler(Consumer<DiscordCommandAutoCompleteInteractionEvent> autoCompleteHandler) {
this.autoCompleteHandler = autoCompleteHandler;
return this;
}
@Override
public Command build() {
return new Command(
@ -268,21 +328,26 @@ public class Command implements JDAEntity<CommandData> {
subCommandGroups,
subCommands,
options,
guildId,
guildOnly,
defaultPermission
defaultPermission,
eventHandler,
autoCompleteHandler
);
}
}
public static class Builder {
public static class Builder<E extends AbstractCommandInteractionEvent<?>> {
protected final ComponentIdentifier id;
protected final Type type;
protected final CommandType type;
protected final Map<Locale, String> nameTranslations = new LinkedHashMap<>();
protected Long guildId = null;
protected boolean guildOnly = true;
protected DefaultPermission defaultPermission = DefaultPermission.EVERYONE;
protected Consumer<? extends AbstractCommandInteractionEvent<?>> eventHandler;
private Builder(ComponentIdentifier id, Type type, String name) {
private Builder(ComponentIdentifier id, CommandType type, String name) {
this.id = id;
this.type = type;
this.nameTranslations.put(Locale.ROOT, name);
@ -295,18 +360,28 @@ public class Command implements JDAEntity<CommandData> {
* @return this builder, useful for chaining
*/
@NotNull
public Builder addNameTranslation(@NotNull Locale locale, @NotNull String translation) {
public Builder<E> addNameTranslation(@NotNull Locale locale, @NotNull String translation) {
this.nameTranslations.put(locale, translation);
return this;
}
/**
* Sets the id of the guild this command should be registered to, or {@code null} to register the command globally.
* @param guildId the guild id
* @return this builder, useful for chaining
*/
public Builder<E> setGuildId(Long guildId) {
this.guildId = guildId;
return this;
}
/**
* Sets if this command is limited to Discord servers.
* @param guildOnly if this command is limited to Discord servers
* @return this builder, useful for chaining
*/
@NotNull
public Builder setGuildOnly(boolean guildOnly) {
public Builder<E> setGuildOnly(boolean guildOnly) {
this.guildOnly = guildOnly;
return this;
}
@ -314,13 +389,25 @@ public class Command implements JDAEntity<CommandData> {
/**
* Sets the permission level required to use the command by default.
* @param defaultPermission the permission level
* @return this builder, useful for chaining
*/
@NotNull
public Builder setDefaultPermission(@NotNull DefaultPermission defaultPermission) {
public Builder<E> setDefaultPermission(@NotNull DefaultPermission defaultPermission) {
this.defaultPermission = defaultPermission;
return this;
}
/**
* Sets the event handler for this command, this can be used instead of listening to the specific interaction event.
* @param eventHandler the event handler, only receives events for this command
* @return this builder, useful for chaining
*/
@NotNull
public Builder<E> setEventHandler(Consumer<E> eventHandler) {
this.eventHandler = eventHandler;
return this;
}
public Command build() {
return new Command(
id,
@ -330,8 +417,11 @@ public class Command implements JDAEntity<CommandData> {
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
guildId,
guildOnly,
defaultPermission
defaultPermission,
eventHandler,
null
);
}
}
@ -379,21 +469,24 @@ public class Command implements JDAEntity<CommandData> {
}
}
public enum Type implements JDAEntity<net.dv8tion.jda.api.interactions.commands.Command.Type> {
public enum RegistrationResult {
CHAT_INPUT(net.dv8tion.jda.api.interactions.commands.Command.Type.SLASH),
USER(net.dv8tion.jda.api.interactions.commands.Command.Type.USER),
MESSAGE(net.dv8tion.jda.api.interactions.commands.Command.Type.MESSAGE);
/**
* Indicates that the command was successfully added to the registry, and will be registered.
*/
REGISTERED,
private final net.dv8tion.jda.api.interactions.commands.Command.Type jda;
/**
* There was already a command with the same name,
* therefor the command won't be registered unless other commands with the same name are unregistered.
*/
NAME_ALREADY_IN_USE,
Type(net.dv8tion.jda.api.interactions.commands.Command.Type jda) {
this.jda = jda;
}
/**
* There are too many commands of the same type,
* therefore the command won't be registered unless other commands of the same type are unregistered.
*/
TOO_MANY_COMMANDS
@Override
public net.dv8tion.jda.api.interactions.commands.Command.Type asJDA() {
return jda;
}
}
}

View File

@ -0,0 +1,29 @@
package com.discordsrv.api.discord.entity.interaction.command;
import com.discordsrv.api.discord.entity.JDAEntity;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
public enum CommandType implements JDAEntity<Command.Type> {
CHAT_INPUT(Command.Type.SLASH, Commands.MAX_SLASH_COMMANDS),
USER(Command.Type.USER, Commands.MAX_USER_COMMANDS),
MESSAGE(Command.Type.MESSAGE, Commands.MAX_MESSAGE_COMMANDS);
private final Command.Type jda;
private final int maximumCount;
CommandType(Command.Type jda, int maximumCount) {
this.jda = jda;
this.maximumCount = maximumCount;
}
@Override
public Command.Type asJDA() {
return jda;
}
public int getMaximumCount() {
return maximumCount;
}
}

View File

@ -23,20 +23,23 @@
package com.discordsrv.api.discord.entity.interaction.component;
import org.intellij.lang.annotations.Pattern;
import org.intellij.lang.annotations.Subst;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* An identifier for commands and components to match up with interaction events, and to avoid conflicts between extensions.
*/
public class ComponentIdentifier {
public static final String ID_PREFIX = "DiscordSRV/";
private static final String ID_PREFIX = "DiscordSRV/";
private static final char PART_SEPARATOR = ':';
private static final String REGEX = "[\\w\\d-_]{1,40}";
private static final java.util.regex.Pattern PATTERN = java.util.regex.Pattern.compile(REGEX);
private static final Pattern PATTERN = java.util.regex.Pattern.compile(REGEX);
/**
* Creates a new {@link ComponentIdentifier}.
@ -48,8 +51,8 @@ public class ComponentIdentifier {
*/
@NotNull
public static ComponentIdentifier of(
@NotNull @Pattern(REGEX) String extensionName,
@NotNull @Pattern(REGEX) String identifier
@NotNull @org.intellij.lang.annotations.Pattern(REGEX) String extensionName,
@NotNull @org.intellij.lang.annotations.Pattern(REGEX) String identifier
) {
if (!PATTERN.matcher(extensionName).matches()) {
throw new IllegalArgumentException("Extension name does not match the required pattern");
@ -59,6 +62,26 @@ public class ComponentIdentifier {
return new ComponentIdentifier(extensionName, identifier);
}
@NotNull
public static Optional<ComponentIdentifier> parseFromDiscord(@NotNull String discordIdentifier) {
if (!discordIdentifier.startsWith(ID_PREFIX)) {
return Optional.empty();
}
discordIdentifier = discordIdentifier.substring(ID_PREFIX.length());
@Subst("Example:Test")
String[] parts = discordIdentifier.split(Pattern.quote(ID_PREFIX));
if (parts.length != 2) {
return Optional.empty();
}
try {
return Optional.of(of(parts[0], parts[1]));
} catch (IllegalStateException ignored) {
return Optional.empty();
}
}
private final String extensionName;
private final String identifier;
@ -76,7 +99,7 @@ public class ComponentIdentifier {
}
public String getDiscordIdentifier() {
return ID_PREFIX + getExtensionName() + ":" + getIdentifier();
return ID_PREFIX + getExtensionName() + PART_SEPARATOR + getIdentifier();
}
@Override

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent;
import org.jetbrains.annotations.NotNull;
@ -36,12 +37,13 @@ public abstract class AbstractDeferrableInteractionEvent<T extends GenericIntera
public AbstractDeferrableInteractionEvent(
T jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel,
DiscordInteractionHook hook
) {
super(jdaEvent, user, member, channel);
super(jdaEvent, identifier, user, member, channel);
this.hook = hook;
}

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.AbstractDiscordEvent;
import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent;
import org.jetbrains.annotations.NotNull;
@ -35,17 +36,29 @@ import java.util.Optional;
public abstract class AbstractInteractionEvent<T extends GenericInteractionCreateEvent> extends AbstractDiscordEvent<T> {
protected final ComponentIdentifier identifier;
protected final DiscordUser user;
protected final DiscordGuildMember member;
protected final DiscordMessageChannel channel;
public AbstractInteractionEvent(T jdaEvent, DiscordUser user, DiscordGuildMember member, DiscordMessageChannel channel) {
public AbstractInteractionEvent(
T jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel
) {
super(jdaEvent);
this.identifier = identifier;
this.user = user;
this.member = member;
this.channel = channel;
}
public boolean isFor(ComponentIdentifier identifier) {
return this.identifier.equals(identifier);
}
@NotNull
public DiscordUser getUser() {
return user;

View File

@ -27,17 +27,19 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
public class DiscordModalInteractionEvent extends AbstractDeferrableInteractionEvent<ModalInteractionEvent> {
public DiscordModalInteractionEvent(
ModalInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel,
DiscordInteractionHook hook
) {
super(jdaEvent, user, member, channel, hook);
super(jdaEvent, identifier, user, member, channel, hook);
}
}

View File

@ -0,0 +1,24 @@
package com.discordsrv.api.discord.events.interaction.command;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.interaction.AbstractDeferrableInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent;
public class AbstractCommandInteractionEvent<E extends GenericCommandInteractionEvent>
extends AbstractDeferrableInteractionEvent<E> {
public AbstractCommandInteractionEvent(
E jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -27,18 +27,19 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.events.interaction.AbstractDeferrableInteractionEvent;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
public class DiscordChatInputInteractionEvent extends AbstractDeferrableInteractionEvent<SlashCommandInteractionEvent> {
public class DiscordChatInputInteractionEvent extends AbstractCommandInteractionEvent<SlashCommandInteractionEvent> {
public DiscordChatInputInteractionEvent(
SlashCommandInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, user, member, channel, interaction);
super(jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -26,6 +26,7 @@ package com.discordsrv.api.discord.events.interaction.command;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.interaction.AbstractInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
@ -38,11 +39,12 @@ public class DiscordCommandAutoCompleteInteractionEvent extends AbstractInteract
public DiscordCommandAutoCompleteInteractionEvent(
CommandAutoCompleteInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel
) {
super(jdaEvent, user, member, channel);
super(jdaEvent, identifier, user, member, channel);
}
public void addChoice(String key, String value) {

View File

@ -27,19 +27,20 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.events.interaction.AbstractDeferrableInteractionEvent;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
public class DiscordMessageContextInteractionEvent extends
AbstractDeferrableInteractionEvent<MessageContextInteractionEvent> {
public class DiscordMessageContextInteractionEvent
extends AbstractCommandInteractionEvent<MessageContextInteractionEvent> {
public DiscordMessageContextInteractionEvent(
MessageContextInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, user, member, channel, interaction);
super(jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -27,18 +27,19 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.events.interaction.AbstractDeferrableInteractionEvent;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent;
public class DiscordUserContextInteractionEvent extends AbstractDeferrableInteractionEvent<UserContextInteractionEvent> {
public class DiscordUserContextInteractionEvent extends AbstractCommandInteractionEvent<UserContextInteractionEvent> {
public DiscordUserContextInteractionEvent(
UserContextInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, user, member, channel, interaction);
super(jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.interaction.AbstractDeferrableInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
@ -34,11 +35,12 @@ public class DiscordButtonInteractionEvent extends AbstractDeferrableInteraction
public DiscordButtonInteractionEvent(
ButtonInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, user, member, channel, interaction);
super(jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -27,6 +27,7 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.interaction.AbstractDeferrableInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
@ -34,11 +35,12 @@ public class DiscordSelectMenuInteractionEvent extends AbstractDeferrableInterac
public DiscordSelectMenuInteractionEvent(
SelectMenuInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, user, member, channel, interaction);
super(jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -22,11 +22,10 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.command.CommandType;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.interaction.DiscordModalInteractionEvent;
import com.discordsrv.api.discord.events.interaction.command.DiscordChatInputInteractionEvent;
import com.discordsrv.api.discord.events.interaction.command.DiscordCommandAutoCompleteInteractionEvent;
import com.discordsrv.api.discord.events.interaction.command.DiscordMessageContextInteractionEvent;
import com.discordsrv.api.discord.events.interaction.command.DiscordUserContextInteractionEvent;
import com.discordsrv.api.discord.events.interaction.command.*;
import com.discordsrv.api.discord.events.interaction.component.DiscordButtonInteractionEvent;
import com.discordsrv.api.discord.events.interaction.component.DiscordSelectMenuInteractionEvent;
import com.discordsrv.api.discord.events.member.role.DiscordMemberRoleAddEvent;
@ -40,16 +39,15 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.discord.api.entity.component.DiscordInteractionHookImpl;
import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.module.type.AbstractModule;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent;
import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.*;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent;
import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
@ -130,9 +128,20 @@ public class DiscordAPIEventModule extends AbstractModule<DiscordSRV> {
DiscordGuildMember guildMember = member != null ? api().getGuildMember(member) : null;
DiscordMessageChannel channel = api().getMessageChannel(event.getMessageChannel());
if (event instanceof CommandAutoCompleteInteractionEvent) {
com.discordsrv.api.discord.entity.interaction.command.Command command = discordSRV.discordAPI().getActiveCommand(
((CommandAutoCompleteInteractionEvent) event).isGuildCommand() ? event.getGuild() : null,
CommandType.CHAT_INPUT,
((CommandAutoCompleteInteractionEvent) event).getName()
).orElse(null);
if (command == null) {
return;
}
DiscordCommandAutoCompleteInteractionEvent autoComplete = new DiscordCommandAutoCompleteInteractionEvent(
(CommandAutoCompleteInteractionEvent) event, user, guildMember, channel);
(CommandAutoCompleteInteractionEvent) event, command.getId(), user, guildMember, channel);
discordSRV.eventBus().publish(autoComplete);
command.getAutoCompleteHandler().ifPresent(handler -> handler.accept(autoComplete));
List<Command.Choice> choices = new ArrayList<>();
for (Map.Entry<String, Object> entry : autoComplete.getChoices().entrySet()) {
String key = entry.getKey();
@ -150,22 +159,90 @@ public class DiscordAPIEventModule extends AbstractModule<DiscordSRV> {
}
DiscordInteractionHook hook = new DiscordInteractionHookImpl(discordSRV, ((IDeferrableCallback) event).getHook());
Event newEvent;
Event newEvent = null;
if (event instanceof GenericCommandInteractionEvent) {
Guild guild = ((GenericCommandInteractionEvent) event).isGuildCommand() ? event.getGuild() : null;
String name = ((GenericCommandInteractionEvent) event).getName();
if (event instanceof MessageContextInteractionEvent) {
newEvent = new DiscordMessageContextInteractionEvent((MessageContextInteractionEvent) event, user, guildMember, channel, hook);
} else if (event instanceof UserContextInteractionEvent) {
newEvent = new DiscordUserContextInteractionEvent((UserContextInteractionEvent) event, user, guildMember, channel, hook);
} else if (event instanceof SlashCommandInteractionEvent) {
newEvent = new DiscordChatInputInteractionEvent((SlashCommandInteractionEvent) event, user, guildMember, channel, hook);
} else if (event instanceof ButtonInteractionEvent) {
newEvent = new DiscordButtonInteractionEvent((ButtonInteractionEvent) event, user, guildMember, channel, hook);
} else if (event instanceof SelectMenuInteractionEvent) {
newEvent = new DiscordSelectMenuInteractionEvent((SelectMenuInteractionEvent) event, user, guildMember, channel, hook);
} else if (event instanceof ModalInteractionEvent) {
newEvent = new DiscordModalInteractionEvent((ModalInteractionEvent) event, user, guildMember, channel, hook);
} else {
com.discordsrv.api.discord.entity.interaction.command.Command command = discordSRV.discordAPI()
.getActiveCommand(guild, CommandType.CHAT_INPUT, name).orElse(null);
if (command == null) {
return;
}
DiscordMessageContextInteractionEvent interactionEvent = new DiscordMessageContextInteractionEvent(
(MessageContextInteractionEvent) event,
command.getId(),
user,
guildMember,
channel,
hook
);
newEvent = interactionEvent;
command.getEventHandler().ifPresent(handler -> handler.accept(interactionEvent));
} else if (event instanceof UserContextInteractionEvent) {
com.discordsrv.api.discord.entity.interaction.command.Command command = discordSRV.discordAPI()
.getActiveCommand(guild, CommandType.MESSAGE, name).orElse(null);
if (command == null) {
return;
}
DiscordUserContextInteractionEvent interactionEvent = new DiscordUserContextInteractionEvent(
(UserContextInteractionEvent) event,
command.getId(),
user,
guildMember,
channel,
hook
);
newEvent = interactionEvent;
command.getEventHandler().ifPresent(handler -> handler.accept(interactionEvent));
} else if (event instanceof SlashCommandInteractionEvent) {
com.discordsrv.api.discord.entity.interaction.command.Command command = discordSRV.discordAPI()
.getActiveCommand(guild, CommandType.USER, name).orElse(null);
if (command == null) {
return;
}
DiscordChatInputInteractionEvent interactionEvent = new DiscordChatInputInteractionEvent(
(SlashCommandInteractionEvent) event,
command.getId(),
user,
guildMember,
channel,
hook
);
newEvent = interactionEvent;
command.getEventHandler().ifPresent(handler -> handler.accept(interactionEvent));
}
} else if (event instanceof GenericComponentInteractionCreateEvent) {
ComponentIdentifier identifier = ComponentIdentifier.parseFromDiscord(
((GenericComponentInteractionCreateEvent) event).getComponentId()).orElse(null);
if (identifier == null) {
return;
}
if (event instanceof ButtonInteractionEvent) {
newEvent = new DiscordButtonInteractionEvent(
(ButtonInteractionEvent) event, identifier, user, guildMember, channel, hook);
} else if (event instanceof SelectMenuInteractionEvent) {
newEvent = new DiscordSelectMenuInteractionEvent(
(SelectMenuInteractionEvent) event, identifier, user, guildMember, channel, hook);
}
} else if (event instanceof ModalInteractionEvent) {
ComponentIdentifier identifier = ComponentIdentifier.parseFromDiscord(
((ModalInteractionEvent) event).getModalId()).orElse(null);
if (identifier == null) {
return;
}
newEvent = new DiscordModalInteractionEvent((ModalInteractionEvent) event, identifier, user, guildMember, channel, hook);
}
if (newEvent != null) {
discordSRV.eventBus().publish(newEvent);
}
}
}

View File

@ -26,6 +26,8 @@ import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.*;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.entity.guild.DiscordRole;
import com.discordsrv.api.discord.entity.interaction.command.Command;
import com.discordsrv.api.discord.entity.interaction.command.CommandType;
import com.discordsrv.api.discord.exception.NotReadyException;
import com.discordsrv.api.discord.exception.RestErrorResponseException;
import com.discordsrv.common.DiscordSRV;
@ -43,7 +45,6 @@ import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.RemovalListener;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
@ -64,17 +65,14 @@ import java.util.function.Consumer;
public class DiscordAPIImpl implements DiscordAPI {
private final DiscordSRV discordSRV;
private final DiscordCommandRegistry commandRegistry;
private final AsyncLoadingCache<Long, WebhookClient> cachedClients;
private final List<ThreadChannelLookup> threadLookups = new CopyOnWriteArrayList<>();
public DiscordAPIImpl(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
this.commandRegistry = new DiscordCommandRegistry(discordSRV);
this.cachedClients = discordSRV.caffeineBuilder()
.removalListener((RemovalListener<Long, WebhookClient>) (id, client, cause) -> {
if (client != null) {
client.close();
}
})
.expireAfter(new WebhookCacheExpiry())
.buildAsync(new WebhookCacheLoader());
}
@ -301,9 +299,7 @@ public class DiscordAPIImpl implements DiscordAPI {
try {
return mapExceptions(futureSupplier.get());
} catch (Throwable t) {
CompletableFuture<T> future = new CompletableFuture<>();
future.completeExceptionally(t);
return future;
return CompletableFutureUtil.failed(t);
}
}
@ -452,6 +448,20 @@ public class DiscordAPIImpl implements DiscordAPI {
return new DiscordRoleImpl(discordSRV, jda);
}
@Override
public Command.RegistrationResult registerCommand(Command command) {
return commandRegistry.register(command);
}
@Override
public void unregisterCommand(Command command) {
commandRegistry.unregister(command);
}
public Optional<Command> getActiveCommand(@Nullable Guild guild, CommandType type, String name) {
return Optional.ofNullable(commandRegistry.getActive(guild != null ? guild.getIdLong() : null, type, name));
}
private class WebhookCacheLoader implements AsyncCacheLoader<Long, WebhookClient> {
@Override

View File

@ -0,0 +1,192 @@
package com.discordsrv.common.discord.api;
import com.discordsrv.api.discord.entity.JDAEntity;
import com.discordsrv.api.discord.entity.interaction.command.Command;
import com.discordsrv.api.discord.entity.interaction.command.CommandType;
import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
public class DiscordCommandRegistry {
private static final Long GLOBAL_ID = -1L;
private final Map<Long, Map<CommandType, Registry>> registries = new ConcurrentHashMap<>();
private final DiscordSRV discordSRV;
public DiscordCommandRegistry(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
}
public Command.RegistrationResult register(Command command) {
CommandType type = command.getType();
Registry registry = registries
.computeIfAbsent(command.getGuildId().orElse(GLOBAL_ID), key -> new EnumMap<>(CommandType.class))
.computeIfAbsent(type, key -> new Registry());
boolean first = registry.register(command);
if (!first) {
return Command.RegistrationResult.NAME_ALREADY_IN_USE;
}
if (registry.getInTimeOrder().indexOf(command) >= type.getMaximumCount()) {
return Command.RegistrationResult.TOO_MANY_COMMANDS;
}
return Command.RegistrationResult.REGISTERED;
}
public void unregister(Command command) {
Registry registry = registries
.computeIfAbsent(command.getGuildId().orElse(GLOBAL_ID), key -> Collections.emptyMap())
.get(command.getType());
if (registry != null) {
registry.unregister(command);
}
}
@Nullable
public Command getActive(Long guildId, CommandType type, String name) {
return registries
.computeIfAbsent(guildId != null ? guildId : GLOBAL_ID, key -> Collections.emptyMap())
.get(type).getActive(name);
}
public void registerCommandsToDiscord() {
JDA jda = discordSRV.jda().orElse(null);
if (jda == null) {
return;
}
List<Long> ids = new ArrayList<>();
ids.add(GLOBAL_ID);
for (Guild guild : jda.getGuilds()) {
ids.add(guild.getIdLong());
}
for (long guildId : ids) {
Map<CommandType, Registry> commandsByType = registries.getOrDefault(guildId, Collections.emptyMap());
Map<CommandType, Set<Command>> commandsToRegister = new EnumMap<>(CommandType.class);
boolean updateNeeded = false;
for (Map.Entry<CommandType, Registry> entry : commandsByType.entrySet()) {
Registry registry = entry.getValue();
List<Command> commands = registry.getInTimeOrder();
Set<Command> currentCommands = new LinkedHashSet<>();
int max = Math.min(commands.size(), entry.getKey().getMaximumCount());
for (int i = 0; i < max; i++) {
Command command = commands.get(i);
currentCommands.add(command);
}
commandsToRegister.put(entry.getKey(), currentCommands);
Collection<Command> activeCommands = registry.activeCommands.values();
if (activeCommands.size() != currentCommands.size() || !currentCommands.containsAll(activeCommands)) {
updateNeeded = true;
}
}
if (updateNeeded) {
CommandListUpdateAction action;
if (Objects.equals(guildId, GLOBAL_ID)) {
action = jda.updateCommands();
} else {
Guild guild = jda.getGuildById(guildId);
if (guild == null) {
continue;
}
action = guild.updateCommands();
}
List<Command> allCommands = new ArrayList<>();
commandsToRegister.values().forEach(allCommands::addAll);
action.addCommands(allCommands.stream().map(JDAEntity::asJDA).collect(Collectors.toList()))
.queue(v -> {
for (CommandType value : CommandType.values()) {
commandsByType.get(value).putActiveCommands(commandsToRegister.get(value));
}
});
}
}
}
private static class Registry {
private final Map<String, List<Registration>> registry = new ConcurrentHashMap<>();
private final Map<String, Command> activeCommands = new HashMap<>();
public boolean register(@NotNull Command command) {
List<Registration> commands = registry.computeIfAbsent(command.getName(), key -> new CopyOnWriteArrayList<>());
boolean empty = commands.isEmpty();
commands.add(new Registration(command));
return empty;
}
public void unregister(@NotNull Command command) {
List<Registration> commands = registry.get(command.getName());
if (commands == null) {
return;
}
commands.removeIf(reg -> reg.command == command);
if (commands.isEmpty()) {
registry.remove(command.getName());
}
}
public void putActiveCommands(Set<Command> commands) {
synchronized (activeCommands) {
activeCommands.clear();
for (Command command : commands) {
activeCommands.put(command.getName(), command);
}
}
}
public List<Command> getInTimeOrder() {
List<Registration> registrations = registry.values().stream()
.map(list -> list.get(0))
.collect(Collectors.toList());
return registrations.stream()
.sorted(Comparator.comparingLong(Registration::getTime))
.map(Registration::getCommand)
.collect(Collectors.toList());
}
@Nullable
public Command getActive(String name) {
synchronized (activeCommands) {
return activeCommands.get(name);
}
}
}
private static class Registration {
private final Command command;
private final long time;
public Registration(Command command) {
this.command = command;
this.time = System.currentTimeMillis();
}
public Command getCommand() {
return command;
}
public long getTime() {
return time;
}
}
}