Merge branch 'execute-command'

This commit is contained in:
Vankka 2023-07-09 13:16:37 +03:00
commit 2e48e91a8f
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
37 changed files with 935 additions and 206 deletions

View File

@ -27,7 +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 com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -147,11 +147,11 @@ public interface DiscordAPI {
* Registers a Discord command.
* @param command the command to register
*/
Command.RegistrationResult registerCommand(Command command);
DiscordCommand.RegistrationResult registerCommand(DiscordCommand command);
/**
* Unregisters a Discord command.
* @param command the command to unregister
*/
void unregisterCommand(Command command);
void unregisterCommand(DiscordCommand command);
}

View File

@ -40,7 +40,7 @@ import java.util.regex.Pattern;
/**
* A Discord command.
*/
public class Command implements JDAEntity<CommandData> {
public class DiscordCommand 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);
@ -100,27 +100,27 @@ public class Command implements JDAEntity<CommandData> {
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<DiscordCommand> 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 final AutoCompleteHandler autoCompleteHandler;
private Command(
private DiscordCommand(
ComponentIdentifier id,
CommandType type,
Map<Locale, String> nameTranslations,
Map<Locale, String> descriptionTranslations,
List<SubCommandGroup> subCommandGroups,
List<Command> subCommands,
List<DiscordCommand> subCommands,
List<CommandOption> options,
Long guildId,
boolean guildOnly,
DefaultPermission defaultPermission,
Consumer<? extends AbstractCommandInteractionEvent<?>> eventHandler,
Consumer<DiscordCommandAutoCompleteInteractionEvent> autoCompleteHandler
AutoCompleteHandler autoCompleteHandler
) {
this.id = id;
this.type = type;
@ -181,7 +181,7 @@ public class Command implements JDAEntity<CommandData> {
@NotNull
@Unmodifiable
public List<Command> getSubCommands() {
public List<DiscordCommand> getSubCommands() {
return Collections.unmodifiableList(subCommands);
}
@ -211,7 +211,7 @@ public class Command implements JDAEntity<CommandData> {
}
@Nullable
public Consumer<DiscordCommandAutoCompleteInteractionEvent> getAutoCompleteHandler() {
public AutoCompleteHandler getAutoCompleteHandler() {
return autoCompleteHandler;
}
@ -228,7 +228,8 @@ public class Command implements JDAEntity<CommandData> {
case CHAT_INPUT:
SlashCommandData slashCommandData = Commands.slash(getName(), Objects.requireNonNull(getDescription()));
slashCommandData.addSubcommandGroups(subCommandGroups.stream().map(JDAEntity::asJDA).toArray(SubcommandGroupData[]::new));
slashCommandData.addSubcommands(subCommands.stream().map(Command::asJDASubcommand).toArray(SubcommandData[]::new));
slashCommandData.addSubcommands(subCommands.stream().map(
DiscordCommand::asJDASubcommand).toArray(SubcommandData[]::new));
slashCommandData.addOptions(options.stream().map(JDAEntity::asJDA).toArray(OptionData[]::new));
commandData = slashCommandData;
break;
@ -252,9 +253,9 @@ public class Command implements JDAEntity<CommandData> {
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<DiscordCommand> subCommands = new ArrayList<>();
private final List<CommandOption> options = new ArrayList<>();
private Consumer<DiscordCommandAutoCompleteInteractionEvent> autoCompleteHandler;
private AutoCompleteHandler autoCompleteHandler;
private ChatInputBuilder(ComponentIdentifier id, String name, String description) {
super(id, CommandType.CHAT_INPUT, name);
@ -296,7 +297,7 @@ public class Command implements JDAEntity<CommandData> {
* @return this builder, useful for chaining
*/
@NotNull
public ChatInputBuilder addSubCommand(@NotNull Command command) {
public ChatInputBuilder addSubCommand(@NotNull DiscordCommand command) {
this.subCommands.add(command);
return this;
}
@ -319,14 +320,14 @@ public class Command implements JDAEntity<CommandData> {
* @return this builder, useful for chaining
*/
@NotNull
public ChatInputBuilder setAutoCompleteHandler(Consumer<DiscordCommandAutoCompleteInteractionEvent> autoCompleteHandler) {
public ChatInputBuilder setAutoCompleteHandler(AutoCompleteHandler autoCompleteHandler) {
this.autoCompleteHandler = autoCompleteHandler;
return this;
}
@Override
public Command build() {
return new Command(
public DiscordCommand build() {
return new DiscordCommand(
id,
type,
nameTranslations,
@ -343,6 +344,13 @@ public class Command implements JDAEntity<CommandData> {
}
}
@FunctionalInterface
public interface AutoCompleteHandler {
void autoComplete(DiscordCommandAutoCompleteInteractionEvent event);
}
public static class Builder<E extends AbstractCommandInteractionEvent<?>> {
protected final ComponentIdentifier id;
@ -414,8 +422,8 @@ public class Command implements JDAEntity<CommandData> {
return this;
}
public Command build() {
return new Command(
public DiscordCommand build() {
return new DiscordCommand(
id,
type,
nameTranslations,

View File

@ -43,15 +43,15 @@ public class SubCommandGroup implements JDAEntity<SubcommandGroupData> {
* @return a new sub command group
*/
@NotNull
public static SubCommandGroup of(@NotNull String name, @NotNull String description, @NotNull Command... commands) {
public static SubCommandGroup of(@NotNull String name, @NotNull String description, @NotNull DiscordCommand... commands) {
return new SubCommandGroup(name, description, Arrays.asList(commands));
}
private final String name;
private final String description;
private final List<Command> commands;
private final List<DiscordCommand> commands;
private SubCommandGroup(String name, String description, List<Command> commands) {
private SubCommandGroup(String name, String description, List<DiscordCommand> commands) {
this.name = name;
this.description = description;
this.commands = commands;
@ -69,13 +69,13 @@ public class SubCommandGroup implements JDAEntity<SubcommandGroupData> {
@NotNull
@Unmodifiable
public List<Command> getCommands() {
public List<DiscordCommand> getCommands() {
return commands;
}
@Override
public SubcommandGroupData asJDA() {
return new SubcommandGroupData(name, description)
.addSubcommands(commands.stream().map(Command::asJDASubcommand).toArray(SubcommandData[]::new));
.addSubcommands(commands.stream().map(DiscordCommand::asJDASubcommand).toArray(SubcommandData[]::new));
}
}

View File

@ -23,7 +23,7 @@
package com.discordsrv.api.discord.events.interaction.command;
import com.discordsrv.api.discord.entity.interaction.command.Command;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.event.events.Event;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
@ -34,24 +34,24 @@ import java.util.Collections;
import java.util.List;
/**
* An event for registering {@link com.discordsrv.api.discord.entity.interaction.command.Command}s,
* an alternative to {@link com.discordsrv.api.discord.DiscordAPI#registerCommand(Command)}.
* An event for registering {@link DiscordCommand}s,
* an alternative to {@link com.discordsrv.api.discord.DiscordAPI#registerCommand(DiscordCommand)}.
*/
public class CommandRegisterEvent implements Event {
private final List<Command> commands = new ArrayList<>();
private final List<DiscordCommand> commands = new ArrayList<>();
/**
* Add events to be registered.
* @param commands the commands to be registered, use of the same command instances is recommended
*/
public void registerCommands(@NotNull Command... commands) {
public void registerCommands(@NotNull DiscordCommand... commands) {
this.commands.addAll(Arrays.asList(commands));
}
@NotNull
@Unmodifiable
public List<Command> getCommands() {
public List<DiscordCommand> getCommands() {
return Collections.unmodifiableList(commands);
}
}

View File

@ -47,16 +47,16 @@ public class DiscordCommandAutoCompleteInteractionEvent extends AbstractInteract
super(jdaEvent, identifier, user, member, channel);
}
public void addChoice(String key, String value) {
this.choices.put(key, value);
public void addChoice(String name, String value) {
this.choices.put(name, value);
}
public void addChoice(String key, double value) {
this.choices.put(key, value);
public void addChoice(String name, double value) {
this.choices.put(name, value);
}
public void addChoice(String key, long value) {
this.choices.put(key, value);
public void addChoice(String name, long value) {
this.choices.put(name, value);
}
public Map<String, Object> getChoices() {

View File

@ -0,0 +1,24 @@
package com.discordsrv.bukkit;
import org.bukkit.Server;
import java.util.Set;
public class PaperCommandMap {
public static final boolean IS_AVAILABLE;
static {
boolean is = false;
try {
Class<?> serverClass = Server.class;
serverClass.getDeclaredMethod("getCommandMap");
is = true;
} catch (Throwable ignored) {}
IS_AVAILABLE = is;
}
public static Set<String> getKnownCommands(Server server) {
return server.getCommandMap().getKnownCommands().keySet();
}
}

View File

@ -19,7 +19,6 @@
package com.discordsrv.bukkit.component;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.ComponentFactory;
import com.discordsrv.common.component.util.ComponentUtil;
import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer;
@ -60,21 +59,14 @@ public class PaperComponentHandle<T> {
this.handle = handle;
}
public MinecraftComponent getComponent(DiscordSRV discordSRV, T target) {
public MinecraftComponent getComponent(T target) {
if (handle != null) {
Object unrelocated = null;
try {
unrelocated = handle.invoke(target);
} catch (Throwable ignored) {}
if (unrelocated != null) {
MinecraftComponent component = discordSRV.componentFactory().empty();
MinecraftComponent.Adapter<Object> adapter = component.unrelocatedAdapter();
if (adapter == null) {
throw new IllegalStateException("Unrelocated adventure unavailable");
}
adapter.setComponent(unrelocated);
return component;
return ComponentUtil.fromUnrelocated(unrelocated);
}
}

View File

@ -31,8 +31,8 @@ import java.util.UUID;
import java.util.function.Consumer;
@SuppressWarnings("deprecation") // Paper
@Proxy(value = CommandSender.class, className = "BukkitCommandExecutorProxy")
public abstract class BukkitCommandExecutorProxyTemplate implements CommandSender {
@Proxy(value = CommandSender.class, className = "BukkitCommandFeedbackExecutorProxy")
public abstract class BukkitCommandFeedbackExecutorProxyTemplate implements CommandSender {
@Original
private final CommandSender commandSender;
@ -40,7 +40,7 @@ public abstract class BukkitCommandExecutorProxyTemplate implements CommandSende
private Spigot spigot;
public BukkitCommandExecutorProxyTemplate(CommandSender commandSender, Consumer<Component> componentConsumer) {
public BukkitCommandFeedbackExecutorProxyTemplate(CommandSender commandSender, Consumer<Component> componentConsumer) {
this.commandSender = commandSender;
this.componentConsumer = componentConsumer;
try {

View File

@ -0,0 +1,31 @@
package com.discordsrv.bukkit.console.executor;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.unrelocate.net.kyori.adventure.text.Component;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import java.util.function.Consumer;
public class PaperCommandFeedbackExecutor implements Consumer<Component> {
private final Consumer<MinecraftComponent> componentConsumer;
private final CommandSender sender;
@SuppressWarnings("unchecked")
public PaperCommandFeedbackExecutor(Server server, Consumer<MinecraftComponent> componentConsumer) {
this.componentConsumer = componentConsumer;
this.sender = server.createCommandSender((Consumer<? super net.kyori.adventure.text.Component>) (Object) this);
}
public CommandSender sender() {
return sender;
}
@Override
public void accept(Component component) {
MinecraftComponent minecraftComponent = ComponentUtil.fromUnrelocated(component);
componentConsumer.accept(minecraftComponent);
}
}

View File

@ -45,11 +45,8 @@ public class PaperModernAdvancementListener extends AbstractBukkitAwardListener
);
}
private final DiscordSRV discordSRV;
public PaperModernAdvancementListener(DiscordSRV discordSRV, IBukkitAwardForwarder forwarder) {
super(discordSRV, forwarder);
this.discordSRV = discordSRV;
}
@EventHandler(priority = EventPriority.MONITOR)
@ -62,8 +59,8 @@ public class PaperModernAdvancementListener extends AbstractBukkitAwardListener
return;
}
MinecraftComponent message = MESSAGE_HANDLE.getComponent(discordSRV, event);
MinecraftComponent displayName = DISPLAY_NAME_HANDLE.getComponent(discordSRV, advancement);
MinecraftComponent message = MESSAGE_HANDLE.getComponent(event);
MinecraftComponent displayName = DISPLAY_NAME_HANDLE.getComponent(advancement);
forwarder.publishEvent(event, event.getPlayer(), displayName, message, false);
}
}

View File

@ -20,7 +20,6 @@ package com.discordsrv.bukkit.listener.chat;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.bukkit.component.PaperComponentHandle;
import com.discordsrv.common.DiscordSRV;
import io.papermc.paper.event.player.AsyncChatEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@ -38,17 +37,15 @@ public class PaperChatListener implements Listener {
);
}
private final DiscordSRV discordSRV;
private final IBukkitChatForwarder listener;
public PaperChatListener(DiscordSRV discordSRV, IBukkitChatForwarder listener) {
this.discordSRV = discordSRV;
public PaperChatListener(IBukkitChatForwarder listener) {
this.listener = listener;
}
@EventHandler(priority = EventPriority.MONITOR)
public void onAsyncChat(AsyncChatEvent event) {
MinecraftComponent component = COMPONENT_HANDLE.getComponent(discordSRV, event);
MinecraftComponent component = COMPONENT_HANDLE.getComponent(event);
listener.publishEvent(event, event.getPlayer(), component, event.isCancelled());
}
}

View File

@ -19,6 +19,7 @@
package com.discordsrv.bukkit;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.bukkit.command.game.BukkitGameCommandExecutionHelper;
import com.discordsrv.bukkit.command.game.handler.AbstractBukkitCommandHandler;
import com.discordsrv.bukkit.component.translation.BukkitTranslationLoader;
import com.discordsrv.bukkit.config.connection.BukkitConnectionConfig;
@ -66,6 +67,7 @@ public class BukkitDiscordSRV extends ServerDiscordSRV<DiscordSRVBukkitBootstrap
private final BukkitPluginManager pluginManager;
private AbstractBukkitCommandHandler commandHandler;
private final BukkitRequiredLinkingListener requiredLinkingListener;
private final BukkitGameCommandExecutionHelper autoCompleteHelper;
private final BukkitConnectionConfigManager connectionConfigManager;
private final BukkitConfigManager configManager;
@ -94,6 +96,7 @@ public class BukkitDiscordSRV extends ServerDiscordSRV<DiscordSRVBukkitBootstrap
load();
this.requiredLinkingListener = new BukkitRequiredLinkingListener(this);
this.autoCompleteHelper = new BukkitGameCommandExecutionHelper(this);
}
public JavaPlugin plugin() {
@ -225,4 +228,9 @@ public class BukkitDiscordSRV extends ServerDiscordSRV<DiscordSRVBukkitBootstrap
requiredLinkingListener.disable();
audiences.close();
}
@Override
public BukkitGameCommandExecutionHelper executeHelper() {
return autoCompleteHelper;
}
}

View File

@ -0,0 +1,111 @@
package com.discordsrv.bukkit.command.game;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.bukkit.PaperCommandMap;
import com.discordsrv.common.command.game.GameCommandExecutionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
public class BukkitGameCommandExecutionHelper implements GameCommandExecutionHelper {
private final BukkitDiscordSRV discordSRV;
public BukkitGameCommandExecutionHelper(BukkitDiscordSRV discordSRV) {
this.discordSRV = discordSRV;
}
@Override
public CompletableFuture<List<String>> suggestCommands(List<String> parts) {
String commandName = !parts.isEmpty() ? parts.remove(0) : null;
Command command = commandName != null ? discordSRV.server().getPluginCommand(commandName) : null;
if (command == null) {
if (parts.size() > 1) {
// Command is not known but there are arguments, nothing to auto complete...
return CompletableFuture.completedFuture(Collections.emptyList());
} else {
// List out commands
List<String> suggestions = new ArrayList<>();
if (PaperCommandMap.IS_AVAILABLE) {
// If Paper's CommandMap is available we can list out 'root' commands
CompletableFuture<List<String>> future = new CompletableFuture<>();
discordSRV.scheduler().runOnMainThread(discordSRV.server().getConsoleSender(), () -> {
try {
for (String cmd : PaperCommandMap.getKnownCommands(discordSRV.server())) {
if (commandName == null || cmd.startsWith(commandName)) {
suggestions.add(cmd);
}
}
future.complete(suggestions);
} catch (Throwable t) {
future.completeExceptionally(t);
}
});
return future;
}
return CompletableFuture.completedFuture(suggestions);
}
}
// Get the arguments minus the last one (if any)
String prefix = String.join(" ", parts.subList(0, parts.size() - (!parts.isEmpty() ? 1 : 0)));
if (!prefix.isEmpty()) {
prefix = prefix + " ";
}
CompletableFuture<List<String>> future = new CompletableFuture<>();
String finalPrefix = prefix;
CommandSender commandSender = discordSRV.server().getConsoleSender();
discordSRV.scheduler().runOnMainThread(commandSender, () -> {
try {
List<String> completions = command.tabComplete(commandSender, commandName, parts.toArray(new String[0]));
List<String> suggestions = new ArrayList<>();
for (String suggestion : completions) {
suggestions.add(commandName + " " + finalPrefix + suggestion);
}
future.complete(suggestions);
} catch (Throwable t) {
future.completeExceptionally(t);
}
});
return future;
}
@Override
public List<String> getAliases(String command) {
PluginCommand pluginCommand = discordSRV.server().getPluginCommand(command);
if (pluginCommand == null) {
return Collections.emptyList();
}
List<String> aliases = new ArrayList<>(pluginCommand.getAliases());
aliases.add(pluginCommand.getName());
String pluginName = pluginCommand.getName().toLowerCase(Locale.ROOT);
int originalMax = aliases.size();
for (int i = 0; i < originalMax; i++) {
// plugin:command
aliases.add(pluginName + ":" + aliases.get(i));
}
return aliases;
}
@Override
public boolean isSameCommand(String command1, String command2) {
PluginCommand pluginCommand1 = discordSRV.server().getPluginCommand(command1);
PluginCommand pluginCommand2 = discordSRV.server().getPluginCommand(command2);
return pluginCommand1 == pluginCommand2;
}
}

View File

@ -21,7 +21,9 @@ package com.discordsrv.bukkit.console.executor;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.command.game.executor.CommandExecutor;
import com.discordsrv.common.command.game.executor.CommandExecutorProvider;
import com.discordsrv.common.component.util.ComponentUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import java.util.function.Consumer;
@ -33,7 +35,9 @@ public class BukkitCommandExecutorProvider implements CommandExecutorProvider {
static {
boolean has = false;
try {
has = PaperCommandExecutor.CREATE_COMMAND_SENDER != null;
//noinspection JavaReflectionMemberAccess
Server.class.getDeclaredMethod("createCommandSender", Consumer.class);
has = true;
} catch (Throwable ignored) {}
HAS_PAPER_FORWARDING = has;
}
@ -48,11 +52,15 @@ public class BukkitCommandExecutorProvider implements CommandExecutorProvider {
public CommandExecutor getConsoleExecutor(Consumer<Component> componentConsumer) {
if (HAS_PAPER_FORWARDING) {
try {
return new PaperCommandExecutor(discordSRV, componentConsumer);
CommandSender sender = new PaperCommandFeedbackExecutor(
discordSRV.server(),
apiComponent -> componentConsumer.accept(ComponentUtil.fromAPI(apiComponent))
).sender();
return new CommandSenderExecutor(discordSRV, sender);
} catch (Throwable ignored) {}
}
CommandSender commandSender = new BukkitCommandExecutorProxy(discordSRV.server().getConsoleSender(), componentConsumer).getProxy();
CommandSender commandSender = new BukkitCommandFeedbackExecutorProxy(discordSRV.server().getConsoleSender(), componentConsumer).getProxy();
return new CommandSenderExecutor(discordSRV, commandSender);
}
}

View File

@ -1,49 +0,0 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.bukkit.console.executor;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import net.kyori.adventure.text.Component;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.function.Consumer;
@SuppressWarnings("JavaLangInvokeHandleSignature") // PaperAPI that is not included at compile accessed via reflection
public class PaperCommandExecutor extends CommandSenderExecutor {
public static final MethodHandle CREATE_COMMAND_SENDER;
static {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = null;
try {
MethodType methodType = MethodType.methodType(CommandSender.class, Consumer.class);
handle = lookup.findVirtual(Server.class, "createCommandSender", methodType);
} catch (Throwable ignored) {}
CREATE_COMMAND_SENDER = handle;
}
public PaperCommandExecutor(BukkitDiscordSRV discordSRV, Consumer<Component> componentConsumer) throws Throwable {
super(discordSRV, (CommandSender) CREATE_COMMAND_SENDER.invoke(componentConsumer));
}
}

View File

@ -65,7 +65,7 @@ public class BukkitDeathListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent event) {
DiscordSRVPlayer player = discordSRV.playerProvider().player(event.getEntity());
MinecraftComponent component = COMPONENT_HANDLE.getComponent(discordSRV, event);
MinecraftComponent component = COMPONENT_HANDLE.getComponent(event);
boolean cancelled = false;
if (CANCELLED_HANDLE != null) {

View File

@ -57,7 +57,7 @@ public class BukkitStatusMessageListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
DiscordSRVPlayer player = discordSRV.playerProvider().player(event.getPlayer());
MinecraftComponent component = JOIN_HANDLE.getComponent(discordSRV, event);
MinecraftComponent component = JOIN_HANDLE.getComponent(event);
boolean firstJoin = !event.getPlayer().hasPlayedBefore();
discordSRV.scheduler().run(() -> discordSRV.eventBus().publish(
@ -68,7 +68,7 @@ public class BukkitStatusMessageListener implements Listener {
@EventHandler(priority = EventPriority.HIGH)
public void onPlayerQuit(PlayerQuitEvent event) {
DiscordSRVPlayer player = discordSRV.playerProvider().player(event.getPlayer());
MinecraftComponent component = QUIT_HANDLE.getComponent(discordSRV, event);
MinecraftComponent component = QUIT_HANDLE.getComponent(event);
discordSRV.scheduler().run(() -> discordSRV.eventBus().publish(
new LeaveMessageReceiveEvent(event, player, component, null, false)

View File

@ -34,7 +34,7 @@ public class BukkitChatForwarder implements IBukkitChatForwarder {
// TODO: config option
//noinspection ConstantConditions,PointlessBooleanExpression
if (1 == 2 && PaperComponentHandle.IS_PAPER_ADVENTURE) {
return new PaperChatListener(discordSRV, new BukkitChatForwarder(discordSRV));
return new PaperChatListener(new BukkitChatForwarder(discordSRV));
}
return new BukkitChatListener(new BukkitChatForwarder(discordSRV));

View File

@ -33,7 +33,6 @@ public class BukkitPlayer extends BukkitCommandSender implements IPlayer {
private static final PaperComponentHandle<Player> DISPLAY_NAME_HANDLE = makeDisplayNameHandle();
@SuppressWarnings("deprecation") // Paper
private static PaperComponentHandle<Player> makeDisplayNameHandle() {
return new PaperComponentHandle<>(
Player.class,
@ -63,7 +62,7 @@ public class BukkitPlayer extends BukkitCommandSender implements IPlayer {
@Override
public @NotNull Component displayName() {
return ComponentUtil.fromAPI(DISPLAY_NAME_HANDLE.getComponent(discordSRV, player));
return ComponentUtil.fromAPI(DISPLAY_NAME_HANDLE.getComponent(player));
}
@Override

View File

@ -23,6 +23,7 @@ import com.discordsrv.api.module.type.Module;
import com.discordsrv.api.placeholder.DiscordPlaceholders;
import com.discordsrv.common.bootstrap.IBootstrap;
import com.discordsrv.common.channel.ChannelConfigHelper;
import com.discordsrv.common.command.game.GameCommandExecutionHelper;
import com.discordsrv.common.command.game.handler.ICommandHandler;
import com.discordsrv.common.component.ComponentFactory;
import com.discordsrv.common.config.connection.ConnectionConfig;
@ -34,8 +35,8 @@ import com.discordsrv.common.debug.data.OnlineMode;
import com.discordsrv.common.debug.data.VersionInfo;
import com.discordsrv.common.dependency.DiscordSRVDependencyManager;
import com.discordsrv.common.discord.api.DiscordAPIImpl;
import com.discordsrv.common.discord.connection.jda.JDAConnectionManager;
import com.discordsrv.common.discord.connection.details.DiscordConnectionDetailsImpl;
import com.discordsrv.common.discord.connection.jda.JDAConnectionManager;
import com.discordsrv.common.linking.LinkProvider;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.impl.DiscordSRVLogger;
@ -152,4 +153,9 @@ public interface DiscordSRV extends DiscordSRVApi {
List<ReloadResult> runReload(Set<ReloadFlag> flags, boolean silent);
CompletableFuture<Void> invokeDisable();
@Nullable
default GameCommandExecutionHelper executeHelper() {
return null;
}
}

View File

@ -18,7 +18,7 @@
package com.discordsrv.common.command.combined.commands;
import com.discordsrv.api.discord.entity.interaction.command.Command;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.command.CommandOption;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.common.DiscordSRV;
@ -43,7 +43,7 @@ public class DebugCommand extends CombinedCommand {
private static DebugCommand INSTANCE;
private static GameCommand GAME;
private static Command DISCORD;
private static DiscordCommand DISCORD;
private static DebugCommand getInstance(DiscordSRV discordSRV) {
return INSTANCE != null ? INSTANCE : (INSTANCE = new DebugCommand(discordSRV));
@ -67,10 +67,10 @@ public class DebugCommand extends CombinedCommand {
return GAME;
}
public static Command getDiscord(DiscordSRV discordSRV) {
public static DiscordCommand getDiscord(DiscordSRV discordSRV) {
if (DISCORD == null) {
DebugCommand command = getInstance(discordSRV);
DISCORD = Command.chatInput(ComponentIdentifier.of("DiscordSRV", "debug"), "debug", "Create a debug report")
DISCORD = DiscordCommand.chatInput(ComponentIdentifier.of("DiscordSRV", "debug"), "debug", "Create a debug report")
.addOption(
CommandOption.builder(CommandOption.Type.STRING, "format", "The format to generate the debug report")
.addChoice(".zip", "zip")

View File

@ -1,6 +1,6 @@
package com.discordsrv.common.command.combined.commands;
import com.discordsrv.api.discord.entity.interaction.command.Command;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.combined.abstraction.CombinedCommand;
@ -24,7 +24,7 @@ public class ResyncCommand extends CombinedCommand {
private static ResyncCommand INSTANCE;
private static GameCommand GAME;
private static Command DISCORD;
private static DiscordCommand DISCORD;
private static ResyncCommand getInstance(DiscordSRV discordSRV) {
return INSTANCE != null ? INSTANCE : (INSTANCE = new ResyncCommand(discordSRV));
@ -41,10 +41,10 @@ public class ResyncCommand extends CombinedCommand {
return GAME;
}
public static Command getDiscord(DiscordSRV discordSRV) {
public static DiscordCommand getDiscord(DiscordSRV discordSRV) {
if (DISCORD == null) {
ResyncCommand command = getInstance(discordSRV);
DISCORD = Command.chatInput(ComponentIdentifier.of("DiscordSRV", "resync"), "resync", "Perform group resync for online players")
DISCORD = DiscordCommand.chatInput(ComponentIdentifier.of("DiscordSRV", "resync"), "resync", "Perform group resync for online players")
.setEventHandler(command)
.build();
}

View File

@ -19,7 +19,7 @@
package com.discordsrv.common.command.combined.commands;
import com.discordsrv.api.color.Color;
import com.discordsrv.api.discord.entity.interaction.command.Command;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.combined.abstraction.CombinedCommand;
@ -38,7 +38,7 @@ public class VersionCommand extends CombinedCommand {
private static VersionCommand INSTANCE;
private static GameCommand GAME;
private static Command DISCORD;
private static DiscordCommand DISCORD;
private static VersionCommand getInstance(DiscordSRV discordSRV) {
return INSTANCE != null ? INSTANCE : (INSTANCE = new VersionCommand(discordSRV));
@ -55,10 +55,10 @@ public class VersionCommand extends CombinedCommand {
return GAME;
}
public static Command getDiscord(DiscordSRV discordSRV) {
public static DiscordCommand getDiscord(DiscordSRV discordSRV) {
if (DISCORD == null) {
VersionCommand command = getInstance(discordSRV);
DISCORD = Command.chatInput(ComponentIdentifier.of("DiscordSRV", "version"), "version", "Get the DiscordSRV version")
DISCORD = DiscordCommand.chatInput(ComponentIdentifier.of("DiscordSRV", "version"), "version", "Get the DiscordSRV version")
.setEventHandler(command)
.build();
}

View File

@ -1,26 +1,36 @@
package com.discordsrv.common.command.discord.commands;
import com.discordsrv.api.discord.entity.interaction.command.Command;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.combined.commands.DebugCommand;
import com.discordsrv.common.command.combined.commands.ResyncCommand;
import com.discordsrv.common.command.combined.commands.VersionCommand;
import com.discordsrv.common.command.discord.commands.subcommand.ExecuteCommand;
import com.discordsrv.common.config.main.DiscordCommandConfig;
public class DiscordSRVDiscordCommand {
private static final ComponentIdentifier IDENTIFIER = ComponentIdentifier.of("DiscordSRV", "discordsrv");
private static Command INSTANCE;
private static DiscordCommand INSTANCE;
public static Command get(DiscordSRV discordSRV) {
public static DiscordCommand get(DiscordSRV discordSRV) {
if (INSTANCE == null) {
INSTANCE = Command.chatInput(IDENTIFIER, "discordsrv", "DiscordSRV related commands")
DiscordCommandConfig config = discordSRV.config().discordCommand;
DiscordCommand.ChatInputBuilder builder = DiscordCommand.chatInput(IDENTIFIER, "discordsrv", "DiscordSRV related commands")
.addSubCommand(DebugCommand.getDiscord(discordSRV))
.addSubCommand(VersionCommand.getDiscord(discordSRV))
.addSubCommand(ResyncCommand.getDiscord(discordSRV))
.addSubCommand(ResyncCommand.getDiscord(discordSRV));
if (config.execute.enabled) {
builder = builder.addSubCommand(ExecuteCommand.get(discordSRV));
}
INSTANCE = builder
.setGuildOnly(false)
.setDefaultPermission(Command.DefaultPermission.ADMINISTRATOR)
.setDefaultPermission(DiscordCommand.DefaultPermission.ADMINISTRATOR)
.build();
}

View File

@ -0,0 +1,255 @@
package com.discordsrv.common.command.discord.commands.subcommand;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.command.CommandOption;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.interaction.command.DiscordChatInputInteractionEvent;
import com.discordsrv.api.discord.events.interaction.command.DiscordCommandAutoCompleteInteractionEvent;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.game.GameCommandExecutionHelper;
import com.discordsrv.common.config.main.DiscordCommandConfig;
import com.discordsrv.common.config.main.generic.GameCommandFilterConfig;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.NamedLogger;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.kyori.adventure.text.Component;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent>, DiscordCommand.AutoCompleteHandler {
private static DiscordCommand INSTANCE;
public static DiscordCommand get(DiscordSRV discordSRV) {
if (INSTANCE == null) {
DiscordCommandConfig.ExecuteConfig config = discordSRV.config().discordCommand.execute;
ExecuteCommand command = new ExecuteCommand(discordSRV);
INSTANCE = DiscordCommand.chatInput(ComponentIdentifier.of("DiscordSRV", "execute"), "execute", "Run a Minecraft console command")
.addOption(
CommandOption.builder(CommandOption.Type.STRING, "command", "The command to execute")
.setAutoComplete(config.suggest)
.setRequired(true)
.build()
)
.setAutoCompleteHandler(command)
.setEventHandler(command)
.build();
}
return INSTANCE;
}
private final DiscordSRV discordSRV;
private final GameCommandExecutionHelper helper;
private final Logger logger;
public ExecuteCommand(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
this.helper = discordSRV.executeHelper();
this.logger = new NamedLogger(discordSRV, "EXECUTE_COMMAND");
}
public boolean isNotAcceptableCommand(DiscordGuildMember member, DiscordUser user, String command, boolean suggestions) {
DiscordCommandConfig.ExecuteConfig config = discordSRV.config().discordCommand.execute;
for (GameCommandFilterConfig filter : config.filters) {
if (!filter.isAcceptableCommand(member, user, command, suggestions, helper)) {
return true;
}
}
return false;
}
@Override
public void accept(DiscordChatInputInteractionEvent event) {
DiscordCommandConfig.ExecuteConfig config = discordSRV.config().discordCommand.execute;
if (!config.enabled) {
event.asJDA().reply("The execute command is disabled").setEphemeral(true).queue();
return;
}
OptionMapping mapping = event.asJDA().getOption("command");
if (mapping == null) {
return;
}
String command = mapping.getAsString();
if (isNotAcceptableCommand(event.getMember(), event.getUser(), command, false)) {
event.asJDA().reply("You do not have permission to run that command").setEphemeral(true).queue();
return;
}
boolean ephemeral = config.ephemeral;
event.asJDA().reply("Executing command `" + command + "`")
.setEphemeral(ephemeral)
.queue(ih -> new ExecutionContext(discordSRV, ih, config.getOutputMode(), ephemeral).run(command));
}
@Override
public void autoComplete(DiscordCommandAutoCompleteInteractionEvent event) {
if (helper == null) {
// No suggestions available.
return;
}
DiscordCommandConfig.ExecuteConfig config = discordSRV.config().discordCommand.execute;
if (!config.suggest) {
return;
}
OptionMapping mapping = event.asJDA().getOption("command");
if (mapping == null) {
return;
}
String command = mapping.getAsString();
List<String> parts = new ArrayList<>(Arrays.asList(command.split(" ")));
List<String> suggestions = getSuggestions(parts);
if (suggestions == null) {
return;
}
if (suggestions.isEmpty() || suggestions.contains(command)) {
parts.add("");
List<String> newSuggestions = getSuggestions(parts);
if (newSuggestions == null) {
return;
}
suggestions = new ArrayList<>(newSuggestions);
if (suggestions.isEmpty()) {
suggestions.add(command);
}
}
suggestions.sort((s1, s2) -> {
// Options with semicolons (eg. plugin:command) are at the bottom
int semi1 = s1.indexOf(':');
int semi2 = s2.indexOf(':');
if (semi1 > semi2) {
return 1;
} else if (semi2 > semi1) {
return -1;
}
// Otherwise alphabetically sorted
return s1.toLowerCase(Locale.ROOT).compareTo(s2.toLowerCase(Locale.ROOT));
});
for (String suggestion : suggestions) {
if (event.getChoices().size() >= 25) {
break;
}
if (config.filterSuggestions && isNotAcceptableCommand(event.getMember(), event.getUser(), suggestion, true)) {
continue;
}
event.addChoice(suggestion, suggestion);
}
}
private List<String> getSuggestions(List<String> parts) {
try {
return helper.suggestCommands(new ArrayList<>(parts)).get(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} catch (TimeoutException e) {
return null;
} catch (ExecutionException e) {
logger.error("Failed to suggest commands", e.getCause());
return null;
} catch (Throwable t) {
logger.error("Failed to suggest commands", t);
return null;
}
}
private static class ExecutionContext {
private final DiscordSRV discordSRV;
private final InteractionHook hook;
private final DiscordCommandConfig.OutputMode outputMode;
private final boolean ephemeral;
private ScheduledFuture<?> future;
private final Queue<Component> queued = new LinkedBlockingQueue<>();
public ExecutionContext(
DiscordSRV discordSRV,
InteractionHook hook,
DiscordCommandConfig.OutputMode outputMode,
boolean ephemeral
) {
this.discordSRV = discordSRV;
this.hook = hook;
this.outputMode = outputMode;
this.ephemeral = ephemeral;
}
public void run(String command) {
discordSRV.console().commandExecutorProvider()
.getConsoleExecutor(this::consumeComponent)
.runCommand(command);
}
private void consumeComponent(Component component) {
if (outputMode == DiscordCommandConfig.OutputMode.OFF) {
return;
}
synchronized (queued) {
queued.offer(component);
if (future == null) {
future = discordSRV.scheduler().runLater(this::send, 500);
}
}
}
private void send() {
boolean ansi = outputMode == DiscordCommandConfig.OutputMode.ANSI;
boolean plainBlock = outputMode == DiscordCommandConfig.OutputMode.PLAIN_BLOCK;
String prefix = ansi ? "```ansi\n" : (plainBlock ? "```\n" : "");
String suffix = ansi ? "```" : (plainBlock ? "```" : "");
String delimiter = "\n";
StringJoiner joiner = new StringJoiner(delimiter);
Component component;
synchronized (queued) {
while ((component = queued.poll()) != null) {
String discord;
switch (outputMode) {
default:
case MARKDOWN:
discord = discordSRV.componentFactory().discordSerializer().serialize(component);
break;
case ANSI:
discord = discordSRV.componentFactory().ansiSerializer().serialize(component);
break;
case PLAIN:
case PLAIN_BLOCK:
discord = discordSRV.componentFactory().plainSerializer().serialize(component);
break;
}
if (prefix.length() + suffix.length() + discord.length() + joiner.length() + delimiter.length() > Message.MAX_CONTENT_LENGTH) {
hook.sendMessage(prefix + joiner + suffix).setEphemeral(ephemeral).queue();
joiner = new StringJoiner(delimiter);
}
joiner.add(discord);
}
future = null;
}
hook.sendMessage(prefix + joiner + suffix).setEphemeral(ephemeral).queue();
}
}
}

View File

@ -0,0 +1,12 @@
package com.discordsrv.common.command.game;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface GameCommandExecutionHelper {
CompletableFuture<List<String>> suggestCommands(List<String> parts);
List<String> getAliases(String command);
boolean isSameCommand(String command1, String command2);
}

View File

@ -70,14 +70,14 @@ public final class ComponentUtil {
}
}
public static Component fromUnrelocated(Object unrelocatedAdventure) {
public static MinecraftComponent fromUnrelocated(Object unrelocatedAdventure) {
MinecraftComponentImpl component = MinecraftComponentImpl.empty();
MinecraftComponent.Adapter<Object> adapter = component.unrelocatedAdapter();
if (adapter == null) {
throw new IllegalStateException("Could not get unrelocated adventure gson serializer");
}
adapter.setComponent(unrelocatedAdventure);
return fromAPI(component);
return component;
}
public static Component join(Component delimiter, Collection<? extends ComponentLike> components) {

View File

@ -0,0 +1,72 @@
package com.discordsrv.common.config.main;
import com.discordsrv.common.config.main.generic.GameCommandFilterConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@ConfigSerializable
public class DiscordCommandConfig {
public ExecuteConfig execute = new ExecuteConfig();
@ConfigSerializable
public static class ExecuteConfig {
public ExecuteConfig() {
filters.add(
new GameCommandFilterConfig(
new ArrayList<>(),
false,
new ArrayList<>(Arrays.asList("say", "/gamemode(?: (?:survival|spectator)(?: .+)?)?/"))
)
);
}
public boolean enabled = true;
@Comment("If the command output should only be visible to the user who ran the command")
public boolean ephemeral = true;
@Comment("The mode for the command output, available options are:\n"
+ "- markdown: Regular Discord markdown\n"
+ "- ansi: A colored ansi code block\n"
+ "- plain: Plain text\n"
+ "- codeblock: Plain code block\n"
+ "- off: No command output")
public String outputMode = "markdown";
public OutputMode getOutputMode() {
switch (outputMode.toLowerCase(Locale.ROOT)) {
default:
case "markdown": return OutputMode.MARKDOWN;
case "ansi": return OutputMode.ANSI;
case "plain": return OutputMode.PLAIN;
case "codeblock": return OutputMode.PLAIN_BLOCK;
case "off": return OutputMode.OFF;
}
}
@Comment("At least one condition has to match to allow execution")
public List<GameCommandFilterConfig> filters = new ArrayList<>();
@Comment("If commands should be suggested while typing\n" +
"Suggestions go through the server's main thread (on servers with a main thread) to ensure compatability.")
public boolean suggest = true;
@Comment("If suggestions should be filtered based on the \"filters\" option")
public boolean filterSuggestions = true;
}
public enum OutputMode {
MARKDOWN,
ANSI,
PLAIN,
PLAIN_BLOCK,
OFF
}
}

View File

@ -29,7 +29,9 @@ import com.discordsrv.common.config.main.linking.LinkedAccountConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.*;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
@ConfigSerializable
public abstract class MainConfig implements Config {
@ -79,6 +81,9 @@ public abstract class MainConfig implements Config {
@Comment("In-game command configuration")
public GameCommandConfig gameCommand = new GameCommandConfig();
@Comment("Discord command configuration")
public DiscordCommandConfig discordCommand = new DiscordCommandConfig();
@Comment("Configuration for the %discord_invite% placeholder. The below options will be attempted in the order they are in")
public DiscordInviteConfig invite = new DiscordInviteConfig();

View File

@ -0,0 +1,116 @@
package com.discordsrv.common.config.main.generic;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.guild.DiscordRole;
import com.discordsrv.common.command.game.GameCommandExecutionHelper;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ConfigSerializable
public class GameCommandFilterConfig {
public GameCommandFilterConfig() {}
public GameCommandFilterConfig(List<Long> roleAndUserIds, boolean blacklist, List<String> commands) {
this.roleAndUserIds = roleAndUserIds;
this.blacklist = blacklist;
this.commands = commands;
}
@Comment("The role and user ids which this filter applies to")
public List<Long> roleAndUserIds = new ArrayList<>();
@Comment("true for blacklist (blocking commands), false for whitelist (allowing commands)")
public boolean blacklist = true;
@Comment("The commands and/or patterns that are allowed/blocked.\n" +
"The command needs to start with input, this will attempt to normalize command aliases where possible (for the main command)\n" +
"If the command start and ends with /, the input will be treated as a regular expression (regex) and it will pass if it matches the entire command")
public List<String> commands = new ArrayList<>();
public static boolean isCommandMatch(String configCommand, String command, boolean suggestions, GameCommandExecutionHelper helper) {
if (configCommand.startsWith("/") && configCommand.endsWith("/")) {
// Regex handling
Pattern pattern = Pattern.compile(configCommand.substring(1, configCommand.length() - 1));
Matcher matcher = pattern.matcher(command);
return matcher.matches() && matcher.start() == 0 && matcher.end() == command.length();
}
// Normal handling
configCommand = configCommand.toLowerCase(Locale.ROOT);
command = command.toLowerCase(Locale.ROOT);
List<String> parts = new ArrayList<>(Arrays.asList(configCommand.split(" ")));
String rootCommand = parts.remove(0);
Set<String> rootCommands = new LinkedHashSet<>();
rootCommands.add(rootCommand);
if (helper != null) {
rootCommands.addAll(helper.getAliases(rootCommand));
}
if (suggestions) {
// Allow suggesting the commands up to the allowed command
for (String rootCmd : rootCommands) {
if (command.matches("^" + Pattern.quote(rootCmd) + " ?$")) {
return true;
}
StringBuilder built = new StringBuilder(rootCmd);
for (String part : parts) {
built.append(" ").append(part);
if (command.matches("^" + Pattern.quote(built.toString()) + " ?$")) {
return true;
}
}
}
}
String arguments = String.join(" ", parts);
for (String rootCmd : rootCommands) {
String joined = rootCmd + (arguments.isEmpty() ? "" : " " + arguments);
// This part at the end prevents "command list" matching "command listsecrets"
if (command.matches("^" + Pattern.quote(joined) + "(?:$| .+)")) {
// Make sure it's the same command, the alias may be used by another command
return helper == null || helper.isSameCommand(rootCommand, rootCmd);
}
}
return false;
}
public boolean isAcceptableCommand(DiscordGuildMember member, DiscordUser user, String command, boolean suggestions, GameCommandExecutionHelper helper) {
long userId = user.getId();
List<Long> roleIds = new ArrayList<>();
if (member != null) {
for (DiscordRole role : member.getRoles()) {
roleIds.add(role.getId());
}
}
boolean match = false;
for (Long id : roleAndUserIds) {
if (id == userId || roleIds.contains(id)) {
match = true;
break;
}
}
if (!match) {
return true;
}
for (String configCommand : commands) {
if (isCommandMatch(configCommand, command, suggestions, helper) != blacklist) {
return true;
}
}
return false;
}
}

View File

@ -23,6 +23,7 @@ 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.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.command.SubCommandGroup;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.events.interaction.DiscordModalInteractionEvent;
@ -130,7 +131,7 @@ 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(
DiscordCommand command = discordSRV.discordAPI().getActiveCommand(
((CommandAutoCompleteInteractionEvent) event).isGuildCommand() ? event.getGuild() : null,
CommandType.CHAT_INPUT,
((CommandAutoCompleteInteractionEvent) event).getName()
@ -138,25 +139,30 @@ public class DiscordAPIEventModule extends AbstractModule<DiscordSRV> {
if (command == null) {
return;
}
command = mapCommand(
command,
((CommandAutoCompleteInteractionEvent) event).getSubcommandGroup(),
((CommandAutoCompleteInteractionEvent) event).getSubcommandName()
);
DiscordCommandAutoCompleteInteractionEvent autoComplete = new DiscordCommandAutoCompleteInteractionEvent(
(CommandAutoCompleteInteractionEvent) event, command.getId(), user, guildMember, channel);
discordSRV.eventBus().publish(autoComplete);
Consumer<DiscordCommandAutoCompleteInteractionEvent> autoCompleteHandler = command.getAutoCompleteHandler();
DiscordCommand.AutoCompleteHandler autoCompleteHandler = command.getAutoCompleteHandler();
if (autoCompleteHandler != null) {
autoCompleteHandler.accept(autoComplete);
autoCompleteHandler.autoComplete(autoComplete);
}
List<Command.Choice> choices = new ArrayList<>();
for (Map.Entry<String, Object> entry : autoComplete.getChoices().entrySet()) {
String key = entry.getKey();
String name = entry.getKey();
Object value = entry.getValue();
if (value instanceof String) {
choices.add(new Command.Choice(key, (String) value));
choices.add(new Command.Choice(name, (String) value));
} else if (value instanceof Double || value instanceof Float) {
choices.add(new Command.Choice(key, ((Number) value).doubleValue()));
choices.add(new Command.Choice(name, ((Number) value).doubleValue()));
} else {
choices.add(new Command.Choice(key, ((Number) value).longValue()));
choices.add(new Command.Choice(name, ((Number) value).longValue()));
}
}
((CommandAutoCompleteInteractionEvent) event).replyChoices(choices).queue();
@ -169,7 +175,7 @@ public class DiscordAPIEventModule extends AbstractModule<DiscordSRV> {
Guild guild = ((GenericCommandInteractionEvent) event).isGuildCommand() ? event.getGuild() : null;
String name = ((GenericCommandInteractionEvent) event).getName();
if (event instanceof MessageContextInteractionEvent) {
com.discordsrv.api.discord.entity.interaction.command.Command command = discordSRV.discordAPI()
DiscordCommand command = discordSRV.discordAPI()
.getActiveCommand(guild, CommandType.MESSAGE, name).orElse(null);
if (command == null) {
return;
@ -190,7 +196,7 @@ public class DiscordAPIEventModule extends AbstractModule<DiscordSRV> {
eventHandler.accept(interactionEvent);
}
} else if (event instanceof UserContextInteractionEvent) {
com.discordsrv.api.discord.entity.interaction.command.Command command = discordSRV.discordAPI()
DiscordCommand command = discordSRV.discordAPI()
.getActiveCommand(guild, CommandType.USER, name).orElse(null);
if (command == null) {
return;
@ -211,35 +217,16 @@ public class DiscordAPIEventModule extends AbstractModule<DiscordSRV> {
eventHandler.accept(interactionEvent);
}
} else if (event instanceof SlashCommandInteractionEvent) {
com.discordsrv.api.discord.entity.interaction.command.Command command = discordSRV.discordAPI()
DiscordCommand command = discordSRV.discordAPI()
.getActiveCommand(guild, CommandType.CHAT_INPUT, name).orElse(null);
if (command == null) {
return;
}
String subCommandGroupName = ((SlashCommandInteractionEvent) event).getSubcommandGroup();
String subCommandName = ((SlashCommandInteractionEvent) event).getSubcommandName();
if (subCommandGroupName != null) {
for (SubCommandGroup group : command.getSubCommandGroups()) {
if (group.getName().equals(subCommandGroupName)) {
for (com.discordsrv.api.discord.entity.interaction.command.Command subCommand : group.getCommands()) {
if (subCommand.getName().equals(subCommandName)) {
command = subCommand;
break;
}
}
break;
}
}
} else if (subCommandName != null) {
for (com.discordsrv.api.discord.entity.interaction.command.Command subCommand : command.getSubCommands()) {
if (subCommandName.equals(subCommand.getName())) {
command = subCommand;
break;
}
}
}
command = mapCommand(
command,
((SlashCommandInteractionEvent) event).getSubcommandGroup(),
((SlashCommandInteractionEvent) event).getSubcommandName()
);
DiscordChatInputInteractionEvent interactionEvent = new DiscordChatInputInteractionEvent(
(SlashCommandInteractionEvent) event,
@ -283,4 +270,28 @@ public class DiscordAPIEventModule extends AbstractModule<DiscordSRV> {
discordSRV.eventBus().publish(newEvent);
}
}
private DiscordCommand mapCommand(DiscordCommand command, String subCommandGroupName, String subCommandName) {
if (subCommandGroupName != null) {
for (SubCommandGroup group : command.getSubCommandGroups()) {
if (group.getName().equals(subCommandGroupName)) {
for (DiscordCommand subCommand : group.getCommands()) {
if (subCommand.getName().equals(subCommandName)) {
command = subCommand;
break;
}
}
break;
}
}
} else if (subCommandName != null) {
for (DiscordCommand subCommand : command.getSubCommands()) {
if (subCommandName.equals(subCommand.getName())) {
command = subCommand;
break;
}
}
}
return command;
}
}

View File

@ -25,8 +25,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.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.exception.NotReadyException;
import com.discordsrv.api.discord.exception.RestErrorResponseException;
import com.discordsrv.common.DiscordSRV;
@ -495,16 +495,16 @@ public class DiscordAPIImpl implements DiscordAPI {
}
@Override
public Command.RegistrationResult registerCommand(Command command) {
public DiscordCommand.RegistrationResult registerCommand(DiscordCommand command) {
return commandRegistry.register(command, false);
}
@Override
public void unregisterCommand(Command command) {
public void unregisterCommand(DiscordCommand command) {
commandRegistry.unregister(command);
}
public Optional<Command> getActiveCommand(@Nullable Guild guild, CommandType type, String name) {
public Optional<DiscordCommand> getActiveCommand(@Nullable Guild guild, CommandType type, String name) {
return Optional.ofNullable(commandRegistry.getActive(guild != null ? guild.getIdLong() : null, type, name));
}

View File

@ -19,7 +19,7 @@
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.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.command.CommandType;
import com.discordsrv.api.discord.events.interaction.command.CommandRegisterEvent;
import com.discordsrv.common.DiscordSRV;
@ -50,7 +50,7 @@ public class DiscordCommandRegistry {
CommandRegisterEvent event = new CommandRegisterEvent();
discordSRV.eventBus().publish(event);
List<Command> commands = event.getCommands();
List<DiscordCommand> commands = event.getCommands();
for (Map<CommandType, Registry> registryMap : registries.values()) {
registryMap.values().forEach(registry -> registry.removeIf(reg -> reg.isTemporary() && !commands.contains(reg.getCommand())));
}
@ -58,27 +58,27 @@ public class DiscordCommandRegistry {
commands.forEach(cmd -> register(cmd, true));
}
public Command.RegistrationResult register(Command command, boolean temporary) {
public DiscordCommand.RegistrationResult register(DiscordCommand command, boolean temporary) {
CommandType type = command.getType();
Long guildId = command.getGuildId();
Registry registry = registries
.computeIfAbsent(guildId != null ? guildId : GLOBAL_ID, key -> new EnumMap<>(CommandType.class))
.computeIfAbsent(type, key -> new Registry());
if (registry.contains(command)) {
return Command.RegistrationResult.ALREADY_REGISTERED;
return DiscordCommand.RegistrationResult.ALREADY_REGISTERED;
}
boolean first = registry.register(command, temporary);
if (!first) {
return Command.RegistrationResult.NAME_ALREADY_IN_USE;
return DiscordCommand.RegistrationResult.NAME_ALREADY_IN_USE;
}
if (registry.getInTimeOrder().indexOf(command) >= type.getMaximumCount()) {
return Command.RegistrationResult.TOO_MANY_COMMANDS;
return DiscordCommand.RegistrationResult.TOO_MANY_COMMANDS;
}
return Command.RegistrationResult.REGISTERED;
return DiscordCommand.RegistrationResult.REGISTERED;
}
public void unregister(Command command) {
public void unregister(DiscordCommand command) {
Long guildId = command.getGuildId();
Registry registry = registries
.computeIfAbsent(guildId != null ? guildId : GLOBAL_ID, key -> Collections.emptyMap())
@ -90,7 +90,7 @@ public class DiscordCommandRegistry {
}
@Nullable
public Command getActive(Long guildId, CommandType type, String name) {
public DiscordCommand getActive(Long guildId, CommandType type, String name) {
Registry registry = registries
.computeIfAbsent(guildId != null ? guildId : GLOBAL_ID, key -> Collections.emptyMap())
.get(type);
@ -114,23 +114,23 @@ public class DiscordCommandRegistry {
for (long guildId : ids) {
Map<CommandType, Registry> commandsByType = registries.getOrDefault(guildId, Collections.emptyMap());
Map<CommandType, Set<Command>> commandsToRegister = new EnumMap<>(CommandType.class);
Map<CommandType, Set<DiscordCommand>> 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<>();
List<DiscordCommand> commands = registry.getInTimeOrder();
Set<DiscordCommand> currentCommands = new LinkedHashSet<>();
int max = Math.min(commands.size(), entry.getKey().getMaximumCount());
for (int i = 0; i < max; i++) {
Command command = commands.get(i);
DiscordCommand command = commands.get(i);
currentCommands.add(command);
}
commandsToRegister.put(entry.getKey(), currentCommands);
Collection<Command> activeCommands = registry.activeCommands.values();
Collection<DiscordCommand> activeCommands = registry.activeCommands.values();
if (activeCommands.size() != currentCommands.size() || !currentCommands.containsAll(activeCommands)) {
updateNeeded = true;
}
@ -148,7 +148,7 @@ public class DiscordCommandRegistry {
action = guild.updateCommands();
}
List<Command> allCommands = new ArrayList<>();
List<DiscordCommand> allCommands = new ArrayList<>();
commandsToRegister.values().forEach(allCommands::addAll);
action.addCommands(allCommands.stream().map(JDAEntity::asJDA).collect(Collectors.toList()))
.queue(v -> {
@ -166,7 +166,7 @@ public class DiscordCommandRegistry {
private static class Registry {
private final Map<String, List<Registration>> registry = new ConcurrentHashMap<>();
private final Map<String, Command> activeCommands = new HashMap<>();
private final Map<String, DiscordCommand> activeCommands = new HashMap<>();
public void removeIf(Predicate<Registration> commandPredicate) {
List<String> removeKeys = new ArrayList<>();
@ -180,7 +180,7 @@ public class DiscordCommandRegistry {
removeKeys.forEach(registry::remove);
}
public boolean contains(@NotNull Command command) {
public boolean contains(@NotNull DiscordCommand command) {
List<Registration> commands = registry.get(command.getName());
if (commands == null) {
return false;
@ -189,14 +189,14 @@ public class DiscordCommandRegistry {
return commands.stream().anyMatch(reg -> reg.getCommand() == command);
}
public boolean register(@NotNull Command command, boolean temporary) {
public boolean register(@NotNull DiscordCommand command, boolean temporary) {
List<Registration> commands = registry.computeIfAbsent(command.getName(), key -> new CopyOnWriteArrayList<>());
boolean empty = commands.isEmpty();
commands.add(new Registration(command, temporary));
return empty;
}
public void unregister(@NotNull Command command) {
public void unregister(@NotNull DiscordCommand command) {
List<Registration> commands = registry.get(command.getName());
if (commands == null) {
return;
@ -208,16 +208,16 @@ public class DiscordCommandRegistry {
}
}
public void putActiveCommands(Set<Command> commands) {
public void putActiveCommands(Set<DiscordCommand> commands) {
synchronized (activeCommands) {
activeCommands.clear();
for (Command command : commands) {
for (DiscordCommand command : commands) {
activeCommands.put(command.getName(), command);
}
}
}
public List<Command> getInTimeOrder() {
public List<DiscordCommand> getInTimeOrder() {
List<Registration> registrations = registry.values().stream()
.map(list -> list.get(0))
.collect(Collectors.toList());
@ -229,7 +229,7 @@ public class DiscordCommandRegistry {
}
@Nullable
public Command getActive(String name) {
public DiscordCommand getActive(String name) {
synchronized (activeCommands) {
return activeCommands.get(name);
}
@ -238,17 +238,17 @@ public class DiscordCommandRegistry {
private static class Registration {
private final Command command;
private final DiscordCommand command;
private final long time;
private final boolean temporary;
public Registration(Command command, boolean temporary) {
public Registration(DiscordCommand command, boolean temporary) {
this.command = command;
this.time = System.currentTimeMillis();
this.temporary = temporary;
}
public Command getCommand() {
public DiscordCommand getCommand() {
return command;
}

View File

@ -0,0 +1,110 @@
package com.discordsrv.common.command.game;
import com.discordsrv.common.config.main.generic.GameCommandFilterConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class GameCommandFilterTest {
private final ExecutionHelper helper = new ExecutionHelper();
@Test
public void test1() {
Assertions.assertTrue(GameCommandFilterConfig.isCommandMatch("test", "test", false, helper));
}
@Test
public void test2() {
Assertions.assertFalse(GameCommandFilterConfig.isCommandMatch("test", "tester", false, helper));
}
@Test
public void argumentTest() {
Assertions.assertTrue(GameCommandFilterConfig.isCommandMatch("test arg", "test arg", false, helper));
}
@Test
public void suggestTest() {
Assertions.assertTrue(GameCommandFilterConfig.isCommandMatch("test arg", "test", true, helper));
}
@Test
public void extraTest() {
Assertions.assertTrue(GameCommandFilterConfig.isCommandMatch("test arg", "test arg extra arguments after that", false, helper));
}
@Test
public void argumentOverflowTest1() {
Assertions.assertFalse(GameCommandFilterConfig.isCommandMatch("test arg", "test argument", false, helper));
}
@Test
public void sameCommandTest1() {
Assertions.assertFalse(GameCommandFilterConfig.isCommandMatch("plugin1:test", "test", false, helper));
}
@Test
public void sameCommandTest2() {
Assertions.assertTrue(GameCommandFilterConfig.isCommandMatch("plugin2:test", "test", false, helper));
}
@Test
public void regexTest1() {
Assertions.assertTrue(GameCommandFilterConfig.isCommandMatch("/test/", "test", false, helper));
}
@Test
public void regexTest2() {
Assertions.assertFalse(GameCommandFilterConfig.isCommandMatch("/test/", "test extra", false, helper));
}
@Test
public void regexTest3() {
Assertions.assertTrue(GameCommandFilterConfig.isCommandMatch("/test( argument)?/", "test argument", false, helper));
}
@Test
public void regexTest4() {
Assertions.assertFalse(GameCommandFilterConfig.isCommandMatch("/test( argument)?/", "test fail", false, helper));
}
@Test
public void regexTest5() {
Assertions.assertTrue(GameCommandFilterConfig.isCommandMatch("/test( argument)?/", "test", true, helper));
}
public static class ExecutionHelper implements GameCommandExecutionHelper {
private final List<String> TEST1_USED = Collections.singletonList("plugin1:test");
private final List<String> TEST1 = Arrays.asList("test", "plugin1:test");
private final List<String> TEST2 = Arrays.asList("test", "plugin2:test");
private final List<String> TESTER = Arrays.asList("tester", "plugin2:tester");
@Override
public CompletableFuture<List<String>> suggestCommands(List<String> parts) {
return null;
}
@Override
public List<String> getAliases(String command) {
if (TEST1_USED.contains(command)) {
return TEST1;
} else if (TEST2.contains(command)) {
return TEST2;
} else if (TESTER.contains(command)) {
return TESTER;
}
return Collections.emptyList();
}
@Override
public boolean isSameCommand(String command1, String command2) {
return getAliases(command1) == getAliases(command2);
}
}
}

View File

@ -1,3 +1,4 @@
dependencies {
compileOnlyApi(libs.slf4j.api)
compileOnlyApi(libs.adventure.api)
}

View File

@ -0,0 +1,5 @@
package com.discordsrv.unrelocate.net.kyori.adventure.text;
@SuppressWarnings("NonExtendableApiUsage")
public interface Component extends net.kyori.adventure.text.Component {
}

View File

@ -18,8 +18,8 @@ dependencyResolutionManagement {
// Bukkit
version('bukkit_minimum', '1.8.8-R0.1-SNAPSHOT')
version('bukkit1_12', '1.12.2-R0.1-SNAPSHOT')
version('bukkit_latest', '1.19.4-R0.1-SNAPSHOT')
version('folia', '1.19.4-R0.1-SNAPSHOT')
version('bukkit_latest', '1.20.1-R0.1-SNAPSHOT')
version('folia', '1.20.1-R0.1-SNAPSHOT')
library('paperapi', 'io.papermc.paper', 'paper-api').versionRef('bukkit_latest')
library('spigotapi', 'org.spigotmc', 'spigot-api').versionRef('bukkit_minimum')
library('spigotapi-onetwelve', 'org.spigotmc', 'spigot-api').versionRef('bukkit1_12')