Add running commands and sending command output for execute command

This commit is contained in:
Vankka 2023-07-09 13:16:18 +03:00
parent 1207990461
commit 512741bd92
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
17 changed files with 188 additions and 94 deletions

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

@ -36,7 +36,7 @@ public class BukkitGameCommandExecutionHelper implements GameCommandExecutionHel
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.scheduler().runOnMainThread(discordSRV.server().getConsoleSender(), () -> {
try {
for (String cmd : PaperCommandMap.getKnownCommands(discordSRV.server())) {
if (commandName == null || cmd.startsWith(commandName)) {
@ -63,9 +63,10 @@ public class BukkitGameCommandExecutionHelper implements GameCommandExecutionHel
CompletableFuture<List<String>> future = new CompletableFuture<>();
String finalPrefix = prefix;
discordSRV.scheduler().runOnMainThread(() -> {
CommandSender commandSender = discordSRV.server().getConsoleSender();
discordSRV.scheduler().runOnMainThread(commandSender, () -> {
try {
CommandSender commandSender = discordSRV.server().getConsoleSender();
List<String> completions = command.tabComplete(commandSender, commandName, parts.toArray(new String[0]));
List<String> suggestions = new ArrayList<>();

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

@ -13,15 +13,13 @@ 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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent>, DiscordCommand.AutoCompleteHandler {
@ -88,7 +86,10 @@ public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent
return;
}
discordSRV.logger().error("> " + command);
boolean ephemeral = config.ephemeral;
event.asJDA().reply("Executing command `" + command + "`")
.setEphemeral(ephemeral)
.queue(ih -> new ExecutionContext(discordSRV, ih, config.getOutputMode(), ephemeral).run(command));
}
@Override
@ -171,4 +172,84 @@ public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent
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

@ -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

@ -7,6 +7,7 @@ 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 {
@ -28,6 +29,28 @@ public class DiscordCommandConfig {
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<>();
@ -38,4 +61,12 @@ public class DiscordCommandConfig {
@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

@ -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 {
}