From fd8aa46d7dd8ec745a1e66ba42948d977255a86e Mon Sep 17 00:00:00 2001 From: Curle Date: Mon, 2 May 2022 03:10:42 +0100 Subject: [PATCH 01/10] Discord Commands Framework --- .../java/tk/sciwhiz12/concord/ChatBot.java | 5 + .../command/discord/CommandDispatcher.java | 149 ++++++++++++++++++ .../concord/command/discord/SlashCommand.java | 84 ++++++++++ 3 files changed, 238 insertions(+) create mode 100644 src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java create mode 100644 src/main/java/tk/sciwhiz12/concord/command/discord/SlashCommand.java diff --git a/src/main/java/tk/sciwhiz12/concord/ChatBot.java b/src/main/java/tk/sciwhiz12/concord/ChatBot.java index b3218f5f..8db43667 100644 --- a/src/main/java/tk/sciwhiz12/concord/ChatBot.java +++ b/src/main/java/tk/sciwhiz12/concord/ChatBot.java @@ -56,6 +56,7 @@ public class ChatBot extends ListenerAdapter { private final MessageListener msgListener; private final PlayerListener playerListener; private final StatusListener statusListener; + private final CommandDispatcher dispatcher; ChatBot(JDA discord, MinecraftServer server) { this.discord = discord; @@ -65,6 +66,10 @@ public class ChatBot extends ListenerAdapter { playerListener = new PlayerListener(this); statusListener = new StatusListener(this); + // Initialize Discord-side commands + dispatcher = new CommandDispatcher(); + discord.addEventListener(dispatcher); + // Prevent any mentions not explicitly specified MessageAction.setDefaultMentions(Collections.emptySet()); } diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java b/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java new file mode 100644 index 00000000..769d639a --- /dev/null +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java @@ -0,0 +1,149 @@ +package tk.sciwhiz12.concord.command.discord; + +import com.mojang.brigadier.Command; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.events.ReadyEvent; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import tk.sciwhiz12.concord.ConcordConfig; + +import java.util.*; +import java.util.function.Consumer; + +/** + * Dispatches slash commands to the appropriate classes. + * Serves as a wrapper around many individual ListenerAdapters. + * + * Register your commands to this dispatcher with CommandDispatcher.register(...). + * If the command is not already, it will be automatically upserted to all valid guilds. + * You may set .testGuild(...) if you wish to only upsert to a single guild, but this is global. + * + * When the command is invoked, the parameter to the register call will be invoked, and passed down the SlashCommandEvent. + * + * Information such as parameters, help info, description and others will be automatically taken from the command parameter. + * + * @author Curle + */ +public class CommandDispatcher extends ListenerAdapter { + + // The list of valid commands, to be upserted and listened for. + private List commands; + + // The Map that powers the command listener + private Map commandsByName; + + // Whether this Dispatcher should only listen on a single guild, in a testing configuration. + private boolean testMode; + + // The Guild to upsert commands to, if testMode is enabled + @Nullable + private Guild testGuild; + + /** + * Create a new Command Dispatcher, pre-programmed with its' list of commands. + * This should be the preferred way of creating a dispatcher. + * @param commands the list of commands to listen for. + */ + public CommandDispatcher(SlashCommand... commands) { + this.commands = Arrays.stream(commands).toList(); + + this.commandsByName = new HashMap<>(); + for (SlashCommand command : this.commands) { + this.commandsByName.put(command.getName(), command); + } + + this.testMode = false; + this.testGuild = null; + } + + /** + * Create an empty CommandDispatcher. + * Only useful if you want to primarily add commands via lambda using addSingle. + * Please try not to use this too much. + */ + public CommandDispatcher() { + this.commands = new ArrayList<>(); + this.commandsByName = new HashMap<>(); + this.testMode = false; + this.testGuild = null; + } + + /** + * A condensed way of adding a command, that doesn't require creating a new class. + * Provide the necessary information along with a Consumer and the rest will be handled for you. + * @param commandName the name of the command to add + * @param commandDescription description of the command to add + * @param help extra information to be shown in the help command + * @param consumer the action to perform when the command is invoked + * @return the modified CommandDispatcher + */ + public CommandDispatcher registerSingle(String commandName, String commandDescription, String help, Consumer consumer) { + var command = new SlashCommand() { + { + setName(commandName); + setDescription(commandDescription); + setHelpString(help); + } + + @Override + public void execute(SlashCommandEvent event) { + consumer.accept(event); + } + + @Override + public CommandCreateAction setup(CommandCreateAction action) { + return action; + } + }; + + this.commands.add(command); + this.commandsByName.put(commandName, command); + + return this; + } + + /** + * Set this Dispatcher to a test mode, which will only upsert commands to the specified guild. + * This effectively allows for immediate usage of specified commands, rather than the 1 hour delay in regular mode. + * However, it is limited to a single guild in this mode, so do not use it for production usage. + * @param guild the guild to upsert commands to. + * @return The updated CommandDispatcher, for chaining + */ + public CommandDispatcher testGuild(Guild guild) { + this.testMode = true; + this.testGuild = guild; + + return this; + } + + @Override + public void onReady(@NotNull ReadyEvent event) { + if (!ConcordConfig.GUILD_ID.get().isEmpty()) + this.testGuild(event.getJDA().getGuildById(ConcordConfig.GUILD_ID.get())); + + for (SlashCommand command : this.commands) { + // If in test mode, upsert to the configured Test Guild, otherwise to the whole JDA instance. + var action = this.testMode ? + this.testGuild.upsertCommand(command.getName(), command.getDescription()) : + event.getJDA().upsertCommand(command.getName(), command.getDescription()); + // Let the command set up extra information + command.setup(action); + // Queue the upsert + action.queue(); + } + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + SlashCommand command = this.commandsByName.get(event.getName()); + + // Sanity check + if (command == null) throw new IllegalStateException("Attempted to invoke command " + event.getName() + " but it does not exist"); + + // Dispatch to the registered command + command.execute(event); + } +} diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/SlashCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/SlashCommand.java new file mode 100644 index 00000000..5bdd166b --- /dev/null +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/SlashCommand.java @@ -0,0 +1,84 @@ +package tk.sciwhiz12.concord.command.discord; + +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; + +/** + *

Represents a primary Slash Command.

+ * + * A command has optional additions: + *
    + *
  • Sub-commands
  • + *
  • Options
  • + *
+ * A command also has required information: + *
    + *
  • Name
  • + *
  • Description
  • + *
  • Help information
  • + *
+ * + *

Pass this command to the CommandDispatcher for upsert.

+ * + *

When this command is invoked by a user, all information will be passed to the execute method via the Event parameter.

+ * + * @author Curle + */ +public abstract class SlashCommand { + // The primary command string + private String name; + + // Extra information shown to the user in the command list. + private String description; + + // Information shown in the help command, about what this command does. + private String helpString; + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setHelpString(String helpString) { + this.helpString = helpString; + } + + /** + * @return the name of this command + */ + public String getName() { + return name; + } + + /** + * @return extra information about this command + */ + public String getDescription() { + return description; + } + + /** + * @return a detailed description for the help command. + */ + public String getHelpString() { + return helpString; + } + + /** + * Called when a user invokes this command. + * All options and extra information is provided via the event parameter. + * @param event the SlashCommandEvent for extra information about the command invocation + */ + public abstract void execute(SlashCommandEvent event); + + /** + * Add extra things to the command - such as sub-commands and options. + * @param action the action that represents this command + * @return the modified action to submit + */ + public abstract CommandCreateAction setup(CommandCreateAction action); + +} From 7382c617a5ff80c0b6ef8f6456c8d949acbdfee7 Mon Sep 17 00:00:00 2001 From: Curle Date: Mon, 2 May 2022 03:11:21 +0100 Subject: [PATCH 02/10] Begin implementing Concord commands. TPS is buggy. --- .../java/tk/sciwhiz12/concord/ChatBot.java | 4 + .../java/tk/sciwhiz12/concord/Concord.java | 4 +- .../command/ConcordDiscordCommand.java | 91 +++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java diff --git a/src/main/java/tk/sciwhiz12/concord/ChatBot.java b/src/main/java/tk/sciwhiz12/concord/ChatBot.java index 8db43667..3887ad69 100644 --- a/src/main/java/tk/sciwhiz12/concord/ChatBot.java +++ b/src/main/java/tk/sciwhiz12/concord/ChatBot.java @@ -37,6 +37,8 @@ import net.minecraftforge.common.MinecraftForge; import org.slf4j.Marker; import org.slf4j.MarkerFactory; +import tk.sciwhiz12.concord.command.ConcordDiscordCommand; +import tk.sciwhiz12.concord.command.discord.CommandDispatcher; import tk.sciwhiz12.concord.msg.MessageListener; import tk.sciwhiz12.concord.msg.Messaging; import tk.sciwhiz12.concord.msg.PlayerListener; @@ -70,6 +72,8 @@ public class ChatBot extends ListenerAdapter { dispatcher = new CommandDispatcher(); discord.addEventListener(dispatcher); + ConcordDiscordCommand.initialize(dispatcher); + // Prevent any mentions not explicitly specified MessageAction.setDefaultMentions(Collections.emptySet()); } diff --git a/src/main/java/tk/sciwhiz12/concord/Concord.java b/src/main/java/tk/sciwhiz12/concord/Concord.java index 54aa31d2..04297576 100644 --- a/src/main/java/tk/sciwhiz12/concord/Concord.java +++ b/src/main/java/tk/sciwhiz12/concord/Concord.java @@ -45,6 +45,7 @@ import tk.sciwhiz12.concord.command.ConcordCommand; import tk.sciwhiz12.concord.command.EmoteCommandHook; import tk.sciwhiz12.concord.command.ReportCommand; +import tk.sciwhiz12.concord.command.ConcordDiscordCommand; import tk.sciwhiz12.concord.command.SayCommandHook; import tk.sciwhiz12.concord.msg.Messaging; import tk.sciwhiz12.concord.util.Messages; @@ -76,7 +77,7 @@ public Concord() { MinecraftForge.EVENT_BUS.addListener(EmoteCommandHook::onRegisterCommands); } - + public void onServerStarting(ServerStartingEvent event) { if (!event.getServer().isDedicatedServer() && !ConcordConfig.ENABLE_INTEGRATED.get()) { LOGGER.info("Discord integration for integrated servers is disabled in server config."); @@ -146,6 +147,7 @@ public static void enable(MinecraftServer server) { try { final JDA jda = jdaBuilder.build(); BOT = new ChatBot(jda, server); + ConcordDiscordCommand.postInit(); } catch (LoginException e) { LOGGER.error("Error while trying to login to Discord; integration will not be enabled.", e); } diff --git a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java new file mode 100644 index 00000000..905aa15e --- /dev/null +++ b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java @@ -0,0 +1,91 @@ +package tk.sciwhiz12.concord.command; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.minecraft.core.Registry; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.level.dimension.DimensionType; +import org.lwjgl.system.CallbackI; +import tk.sciwhiz12.concord.Concord; +import tk.sciwhiz12.concord.command.discord.CommandDispatcher; + +import java.awt.*; +import java.time.Instant; + +/** + * The Discord command hub. + * + * Has several sub-commands, some of which are usable by everyone: + * - list; lists players on the connected server. + * - tps; displays ticks per second in a format similar to /forge tps. + * - help; displays help autogenerated from the registered commands. + * Some commands are only usable by administrators: + * - kick [reason]; remove a user from the server, optionally with the specified reason. + * - ban [reason]; ban a user from the server, optionally with the specified reason. + * - whitelist ; add or remove a user from the server's whitelist. + * - stop; stop and shutdown the connected server. Disabled on singleplayer worlds. + * + * @author Curle + */ +public class ConcordDiscordCommand { + private static JDA bot; + private static MinecraftServer server; + + private static void listCommand(SlashCommandEvent listEvent) { + listEvent.replyEmbeds(new EmbedBuilder() + .setTitle("Concord Integrations") + .setDescription("There are currently " + server.getPlayerCount() + " people online.") + .addField("Online Players", String.join("\n", server.getPlayerNames()), false) + .setTimestamp(Instant.now()) + .setColor(Color.CYAN) + .build() + ).setEphemeral(true).queue(); + } + + private static void tpsCommand(SlashCommandEvent tpsEvent) { + double meanTickTime = Mth.average(server.tickTimes); + double meanTPS = Math.min(1000.0/meanTickTime, 20); + + StringBuilder builder = new StringBuilder(); + + for (ServerLevel dim : server.getAllLevels()) { + long[] times = server.getTickTime(dim.dimension()); + + if (times == null) + times = new long[]{0}; + + double worldTickTime = Mth.average(times); + double worldTPS = Math.min(1000.0 / worldTickTime, 20); + + builder.append(dim.dimension().location()).append(": Mean tick time: ").append(worldTickTime).append(" ms. Mean TPS: ").append(worldTPS).append("\n"); + } + + tpsEvent.replyEmbeds(new EmbedBuilder() + .setTitle("TPS Performance Report") + .setDescription("Overall performance: Mean tick time: " + meanTickTime + " ms. Mean TPS: " + meanTPS) + .addField("Performance per dimension", builder.toString(), false) + .setColor(Color.ORANGE) + .setTimestamp(Instant.now()) + .build() + ).setEphemeral(true).queue(); + } + + private static void helpCommand(SlashCommandEvent helpEvent) { + // TODO + } + + public static void initialize(CommandDispatcher dispatcher) { + dispatcher.registerSingle("list", "List all online users.", "Show a count of online users, and their names.", ConcordDiscordCommand::listCommand); + dispatcher.registerSingle("tps", "Show the performance of the server.", "Display a breakdown of server performance, in current, average and separated by dimension.", ConcordDiscordCommand::tpsCommand); + dispatcher.registerSingle("help", "Show detailed information about every single available command.", "Show the help information you are currently reading.", ConcordDiscordCommand::helpCommand); + } + + public static void postInit() { + bot = Concord.BOT.getDiscord(); + server = Concord.BOT.getServer(); + } +} From a7665baa9cab0dc479d735b8a9e1a9d284ade50d Mon Sep 17 00:00:00 2001 From: Curle Date: Mon, 2 May 2022 03:11:38 +0100 Subject: [PATCH 03/10] Begin implementing Concord commands. TPS is buggy. --- .../tk/sciwhiz12/concord/command/ConcordDiscordCommand.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java index 905aa15e..87d7224b 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java @@ -2,14 +2,10 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -import net.minecraft.core.Registry; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.Mth; -import net.minecraft.world.level.dimension.DimensionType; -import org.lwjgl.system.CallbackI; import tk.sciwhiz12.concord.Concord; import tk.sciwhiz12.concord.command.discord.CommandDispatcher; From 1b873a52efd704ea1cf0e72ea90f208848a498a0 Mon Sep 17 00:00:00 2001 From: Curle Date: Mon, 2 May 2022 18:50:21 +0100 Subject: [PATCH 04/10] Fix TPS command. --- .../concord/command/ConcordDiscordCommand.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java index 87d7224b..0db5f68c 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java @@ -43,7 +43,7 @@ private static void listCommand(SlashCommandEvent listEvent) { } private static void tpsCommand(SlashCommandEvent tpsEvent) { - double meanTickTime = Mth.average(server.tickTimes); + double meanTickTime = Mth.average(server.tickTimes) * 1.0E-6D;; double meanTPS = Math.min(1000.0/meanTickTime, 20); StringBuilder builder = new StringBuilder(); @@ -54,15 +54,16 @@ private static void tpsCommand(SlashCommandEvent tpsEvent) { if (times == null) times = new long[]{0}; - double worldTickTime = Mth.average(times); + double worldTickTime = Mth.average(times) * 1.0E-6D;; double worldTPS = Math.min(1000.0 / worldTickTime, 20); builder.append(dim.dimension().location()).append(": Mean tick time: ").append(worldTickTime).append(" ms. Mean TPS: ").append(worldTPS).append("\n"); } tpsEvent.replyEmbeds(new EmbedBuilder() - .setTitle("TPS Performance Report") - .setDescription("Overall performance: Mean tick time: " + meanTickTime + " ms. Mean TPS: " + meanTPS) + .setTitle("Concord integrations") + .setDescription("TPS Performance Report") + .addField("Overall performance", "Mean tick time: " + meanTickTime + " ms. Mean TPS: " + meanTPS, false) .addField("Performance per dimension", builder.toString(), false) .setColor(Color.ORANGE) .setTimestamp(Instant.now()) @@ -71,6 +72,7 @@ private static void tpsCommand(SlashCommandEvent tpsEvent) { } private static void helpCommand(SlashCommandEvent helpEvent) { + helpEvent.reply("TODO").queue(); // TODO } From 6e6e7c36337e4ffbfd25126809a85252458f251a Mon Sep 17 00:00:00 2001 From: Curle Date: Mon, 2 May 2022 19:48:06 +0100 Subject: [PATCH 05/10] Add Ban Command, some fixes to Kick --- .../command/ConcordDiscordCommand.java | 8 +- .../concord/command/discord/BanCommand.java | 75 +++++++++++++++++++ .../command/discord/CommandDispatcher.java | 12 +++ .../concord/command/discord/KickCommand.java | 74 ++++++++++++++++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java create mode 100644 src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java diff --git a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java index 0db5f68c..ceeb9a65 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java @@ -7,7 +7,9 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.util.Mth; import tk.sciwhiz12.concord.Concord; +import tk.sciwhiz12.concord.command.discord.BanCommand; import tk.sciwhiz12.concord.command.discord.CommandDispatcher; +import tk.sciwhiz12.concord.command.discord.KickCommand; import java.awt.*; import java.time.Instant; @@ -20,9 +22,10 @@ * - tps; displays ticks per second in a format similar to /forge tps. * - help; displays help autogenerated from the registered commands. * Some commands are only usable by administrators: - * - kick [reason]; remove a user from the server, optionally with the specified reason. + * - kick [reason]; remove a user from the server, optionally with the specified reason. See @link{KickCommand}. * - ban [reason]; ban a user from the server, optionally with the specified reason. * - whitelist ; add or remove a user from the server's whitelist. + * The above commands are implemented separately, due to requirements of the option system. * - stop; stop and shutdown the connected server. Disabled on singleplayer worlds. * * @author Curle @@ -80,6 +83,9 @@ public static void initialize(CommandDispatcher dispatcher) { dispatcher.registerSingle("list", "List all online users.", "Show a count of online users, and their names.", ConcordDiscordCommand::listCommand); dispatcher.registerSingle("tps", "Show the performance of the server.", "Display a breakdown of server performance, in current, average and separated by dimension.", ConcordDiscordCommand::tpsCommand); dispatcher.registerSingle("help", "Show detailed information about every single available command.", "Show the help information you are currently reading.", ConcordDiscordCommand::helpCommand); + + dispatcher.registerSingle(KickCommand.INSTANCE); + dispatcher.registerSingle(BanCommand.INSTANCE); } public static void postInit() { diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java new file mode 100644 index 00000000..efee0c79 --- /dev/null +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java @@ -0,0 +1,75 @@ +package tk.sciwhiz12.concord.command.discord; + +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; +import net.minecraft.client.server.IntegratedServer; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.server.players.UserBanListEntry; +import tk.sciwhiz12.concord.Concord; +import tk.sciwhiz12.concord.ConcordConfig; + +import java.util.Date; +import java.util.List; + +public class BanCommand extends SlashCommand { + private static final OptionData USER_OPTION = new OptionData(OptionType.STRING, "user", "The name of the user to ban from the server", true); + private static final OptionData REASON_OPTION = new OptionData(OptionType.STRING, "reason", "The reason for the user to be banned.", false); + + public static BanCommand INSTANCE = new BanCommand(); + + public BanCommand() { + setName("ban"); + setDescription("Ban a player from your Minecraft server"); + setHelpString("Remove a player from the server, and prevent them from joining again, optionally with a reason. The reason is for moderation purposes, and is not shown to the user."); + } + + @Override + public void execute(SlashCommandEvent event) { + var user = event.getOption(USER_OPTION.getName()).getAsString(); + var server = Concord.BOT.getServer(); + + + // Short-circuit for integrated servers. + if (!ConcordConfig.ENABLE_INTEGRATED.get() && server instanceof IntegratedServer) { + event.reply("Sorry, but this command is disabled on Integrated Servers. Check the enable_integrated option in the Concord Config.").setEphemeral(true).queue(); + return; + } + + var reasonMapping = event.getOption(REASON_OPTION.getName()); + + // The Reason Option is optional, so default to "Reason Not Specified" if it isn't. + var reason = ""; + if (reasonMapping == null) + reason = "Reason Not Specified"; + else + reason = reasonMapping.getAsString(); + + + if (List.of(server.getPlayerNames()).contains(user)) { + var player = server.getPlayerList().getPlayerByName(user); + var profile = player.getGameProfile(); + + // If they're not already banned.. + if (!server.getPlayerList().getBans().isBanned(profile)) { + // Prevent them from rejoining + UserBanListEntry userbanlistentry = new UserBanListEntry(profile, (Date) null, "Discord User " + event.getMember().getEffectiveName(), (Date) null, reason); + server.getPlayerList().getBans().add(userbanlistentry); + // Kick them + player.connection.disconnect(new TranslatableComponent("multiplayer.disconnect.banned")); + + event.reply("User " + user + " banned successfully.").queue(); + return; + } + event.reply("The user " + user + " is already banned on this server.").setEphemeral(true).queue(); + return; + } + event.reply("The user " + user + " is not connected to the server.").setEphemeral(true).queue(); + } + + @Override + public CommandCreateAction setup(CommandCreateAction action) { + return action.addOptions(USER_OPTION, REASON_OPTION); + } +} diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java b/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java index 769d639a..ee78c5fa 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java @@ -105,6 +105,18 @@ public CommandCreateAction setup(CommandCreateAction action) { return this; } + /** + * A short way to add a single command to the dispatcher. + * This is not the recommended way to do this - use the variadic constructor instead. + * @param command the command to add + * @return the new Dispatcher, for chaining. + */ + public CommandDispatcher registerSingle(SlashCommand command) { + this.commands.add(command); + this.commandsByName.put(command.getName(), command); + return this; + } + /** * Set this Dispatcher to a test mode, which will only upsert commands to the specified guild. * This effectively allows for immediate usage of specified commands, rather than the 1 hour delay in regular mode. diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java new file mode 100644 index 00000000..4c585441 --- /dev/null +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java @@ -0,0 +1,74 @@ +package tk.sciwhiz12.concord.command.discord; + +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; +import net.minecraft.client.server.IntegratedServer; +import net.minecraft.network.chat.TextComponent; +import tk.sciwhiz12.concord.Concord; +import tk.sciwhiz12.concord.ConcordConfig; + +import java.util.List; + +/** + * This command takes the form: + * /kick [reason] + * + * It removes a user from the server, optionally with the specified reason. + * + * @author Curle + */ +public class KickCommand extends SlashCommand { + private static final OptionData USER_OPTION = new OptionData(OptionType.STRING, "user", "The username of the Minecraft user to kick from the server", true); + private static final OptionData REASON_OPTION = new OptionData(OptionType.STRING, "reason", "Why the user is being kicked from the server.", false); + + // Static instance. + public static KickCommand INSTANCE = new KickCommand(); + + public KickCommand() { + setName("kick"); + setDescription("Kick a user from your Minecraft server"); + setHelpString("Remove a user from the server, optionally with a reason. The reason will be shown to the user in the disconnection screen."); + } + + @Override + public void execute(SlashCommandEvent event) { + var user = event.getOption(USER_OPTION.getName()).getAsString(); + var server = Concord.BOT.getServer(); + + // Short-circuit for integrated servers. + if (!ConcordConfig.ENABLE_INTEGRATED.get() && server instanceof IntegratedServer) { + event.reply("Sorry, but this command is disabled on Integrated Servers. Check the enable_integrated option in the Concord Config.").setEphemeral(true).queue(); + return; + } + + var reasonMapping = event.getOption(REASON_OPTION.getName()); + + // The Reason Option is optional, so default to "Reason Not Specified" if it isn't. + var reason = ""; + if (reasonMapping == null) + reason = "Reason Not Specified"; + else + reason = reasonMapping.getAsString(); + + // Check whether the user is online + if (List.of(server.getPlayerNames()).contains(user)) { + var player = server.getPlayerList().getPlayerByName(user); + // If they are, kick them with the message. + player.connection.disconnect(new TextComponent(reason)); + + // Reply to the user. + event.reply("User " + user + " kicked successfully.").queue(); + return; + } + + // Reply with a failure message. + event.reply("The user " + user + " is not connected to the server.").queue(); + } + + @Override + public CommandCreateAction setup(CommandCreateAction action) { + return action.addOptions(USER_OPTION, REASON_OPTION); + } +} From 2ee925e1527b5cd11d2a9ebea6f91fe7c1f8be4e Mon Sep 17 00:00:00 2001 From: Curle Date: Mon, 2 May 2022 21:03:05 +0100 Subject: [PATCH 06/10] Add Whitelist command, make some adjustments to kick & ban --- .../command/ConcordDiscordCommand.java | 2 + .../concord/command/discord/BanCommand.java | 14 ++- .../concord/command/discord/KickCommand.java | 4 +- .../command/discord/WhitelistCommand.java | 95 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/main/java/tk/sciwhiz12/concord/command/discord/WhitelistCommand.java diff --git a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java index ceeb9a65..1ab81687 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java @@ -10,6 +10,7 @@ import tk.sciwhiz12.concord.command.discord.BanCommand; import tk.sciwhiz12.concord.command.discord.CommandDispatcher; import tk.sciwhiz12.concord.command.discord.KickCommand; +import tk.sciwhiz12.concord.command.discord.WhitelistCommand; import java.awt.*; import java.time.Instant; @@ -86,6 +87,7 @@ public static void initialize(CommandDispatcher dispatcher) { dispatcher.registerSingle(KickCommand.INSTANCE); dispatcher.registerSingle(BanCommand.INSTANCE); + dispatcher.registerSingle(WhitelistCommand.INSTANCE); } public static void postInit() { diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java index efee0c79..c4364a6a 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java @@ -7,12 +7,24 @@ import net.minecraft.client.server.IntegratedServer; import net.minecraft.network.chat.TranslatableComponent; import net.minecraft.server.players.UserBanListEntry; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLLoader; import tk.sciwhiz12.concord.Concord; import tk.sciwhiz12.concord.ConcordConfig; import java.util.Date; import java.util.List; + +/** + * This command takes the form: + * /ban [reason] + * + * It removes a user from the server and prevents them from joining again (known as a ban, or kickban), + * optionally with the specified reason. + * + * @author Curle + */ public class BanCommand extends SlashCommand { private static final OptionData USER_OPTION = new OptionData(OptionType.STRING, "user", "The name of the user to ban from the server", true); private static final OptionData REASON_OPTION = new OptionData(OptionType.STRING, "reason", "The reason for the user to be banned.", false); @@ -32,7 +44,7 @@ public void execute(SlashCommandEvent event) { // Short-circuit for integrated servers. - if (!ConcordConfig.ENABLE_INTEGRATED.get() && server instanceof IntegratedServer) { + if (!ConcordConfig.ENABLE_INTEGRATED.get() && FMLLoader.getDist() == Dist.CLIENT) { event.reply("Sorry, but this command is disabled on Integrated Servers. Check the enable_integrated option in the Concord Config.").setEphemeral(true).queue(); return; } diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java index 4c585441..71964015 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java @@ -6,6 +6,8 @@ import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; import net.minecraft.client.server.IntegratedServer; import net.minecraft.network.chat.TextComponent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLLoader; import tk.sciwhiz12.concord.Concord; import tk.sciwhiz12.concord.ConcordConfig; @@ -38,7 +40,7 @@ public void execute(SlashCommandEvent event) { var server = Concord.BOT.getServer(); // Short-circuit for integrated servers. - if (!ConcordConfig.ENABLE_INTEGRATED.get() && server instanceof IntegratedServer) { + if (!ConcordConfig.ENABLE_INTEGRATED.get() && FMLLoader.getDist() == Dist.CLIENT) { event.reply("Sorry, but this command is disabled on Integrated Servers. Check the enable_integrated option in the Concord Config.").setEphemeral(true).queue(); return; } diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/WhitelistCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/WhitelistCommand.java new file mode 100644 index 00000000..ed711d62 --- /dev/null +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/WhitelistCommand.java @@ -0,0 +1,95 @@ +package tk.sciwhiz12.concord.command.discord; + +import com.mojang.authlib.GameProfile; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; +import net.minecraft.client.server.IntegratedServer; +import net.minecraft.server.players.UserWhiteListEntry; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLLoader; +import tk.sciwhiz12.concord.Concord; + +import java.util.Optional; + + +/** + * This command takes the form: + * /whitelist + * + * Depending on the second term, it will add or remove the specified user from the server whitelist. + * This command is disabled on integrated servers, even if enable_integrated is specified. + * + * @author Curle + */ +public class WhitelistCommand extends SlashCommand { + private static final OptionData USER_OPTION = new OptionData(OptionType.STRING, "user", "The user to change", true); + private static final SubcommandData ADD_SUBCOMMAND = new SubcommandData("add", "Add a user to the whitelist").addOptions(USER_OPTION); + private static final SubcommandData REMOVE_SUBCOMMAND = new SubcommandData("remove", "Remove a user from the whitelist").addOptions(USER_OPTION); + + public static WhitelistCommand INSTANCE = new WhitelistCommand(); + + public WhitelistCommand() { + setName("whitelist"); + setDescription("Add or remove a player from the server's whitelist."); + setHelpString("Contains two subcommands; add and remove. Each takes a user argument and will add or remove the player from the whitelist respectively."); + } + + @Override + public void execute(SlashCommandEvent event) { + var server = Concord.BOT.getServer(); + + // Short circuit for singleplayer worlds + if (FMLLoader.getDist() == Dist.CLIENT) { + event.reply("Sorry, but this command is disabled on Integrated Servers").setEphemeral(true).queue(); + return; + } + + // Figure out which subcommand we're running + var subcommand = event.getSubcommandName(); + switch (subcommand) { + case "add": + var player = event.getOption(USER_OPTION.getName()).getAsString(); + var whitelist = server.getPlayerList().getWhiteList(); + Optional optional = server.getProfileCache().get(player); + var profile = optional.orElseThrow(); + + if (!whitelist.isWhiteListed(profile)) { + UserWhiteListEntry userwhitelistentry = new UserWhiteListEntry(profile); + whitelist.add(userwhitelistentry); + + event.reply("User " + player + " successfully added to the whitelist.").setEphemeral(true).queue(); + return; + } + + event.reply("User " + player + " is already whitelisted.").setEphemeral(true).queue(); + return; + case "remove": + player = event.getOption(USER_OPTION.getName()).getAsString(); + whitelist = server.getPlayerList().getWhiteList(); + optional = server.getProfileCache().get(player); + profile = optional.orElseThrow(); + + if (whitelist.isWhiteListed(profile)) { + whitelist.remove(profile); + + event.reply("User " + player + " successfully removed from the whitelist.").setEphemeral(true).queue(); + return; + } + + event.reply("User " + player + " is not whitelisted.").setEphemeral(true).queue(); + return; + } + + // No recognized subcommand. Fall through to a safe default. + event.reply("Unrecognized subcommand.").setEphemeral(true).queue(); + + } + + @Override + public CommandCreateAction setup(CommandCreateAction action) { + return action.addSubcommands(ADD_SUBCOMMAND).addSubcommands(REMOVE_SUBCOMMAND); + } +} From 88faa48064c8a07ba83e5c534f2df02d4121dc54 Mon Sep 17 00:00:00 2001 From: Curle Date: Mon, 2 May 2022 21:10:55 +0100 Subject: [PATCH 07/10] Implement Stop --- .../concord/command/ConcordDiscordCommand.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java index 1ab81687..7d7385e8 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java @@ -6,6 +6,8 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.loading.FMLLoader; import tk.sciwhiz12.concord.Concord; import tk.sciwhiz12.concord.command.discord.BanCommand; import tk.sciwhiz12.concord.command.discord.CommandDispatcher; @@ -24,8 +26,8 @@ * - help; displays help autogenerated from the registered commands. * Some commands are only usable by administrators: * - kick [reason]; remove a user from the server, optionally with the specified reason. See @link{KickCommand}. - * - ban [reason]; ban a user from the server, optionally with the specified reason. - * - whitelist ; add or remove a user from the server's whitelist. + * - ban [reason]; ban a user from the server, optionally with the specified reason. See @link{BanCommand} + * - whitelist ; add or remove a user from the server's whitelist. See @link{WhitelistCommand} * The above commands are implemented separately, due to requirements of the option system. * - stop; stop and shutdown the connected server. Disabled on singleplayer worlds. * @@ -80,9 +82,21 @@ private static void helpCommand(SlashCommandEvent helpEvent) { // TODO } + private static void stopCommand(SlashCommandEvent stopEvent) { + // Short-circuit if on integrated server + if(FMLLoader.getDist() == Dist.CLIENT) { + stopEvent.reply("Sorry! This command is disabled on Integrated servers.").setEphemeral(true).queue(); + return; + } + + stopEvent.reply("Shutting the server down..").queue(); + Concord.BOT.getServer().halt(false); + } + public static void initialize(CommandDispatcher dispatcher) { dispatcher.registerSingle("list", "List all online users.", "Show a count of online users, and their names.", ConcordDiscordCommand::listCommand); dispatcher.registerSingle("tps", "Show the performance of the server.", "Display a breakdown of server performance, in current, average and separated by dimension.", ConcordDiscordCommand::tpsCommand); + dispatcher.registerSingle("stop", "Shut down your Minecraft server.", "Immediately schedule the shutdown of the Minecraft server, akin to /stop from in-game.", ConcordDiscordCommand::stopCommand); dispatcher.registerSingle("help", "Show detailed information about every single available command.", "Show the help information you are currently reading.", ConcordDiscordCommand::helpCommand); dispatcher.registerSingle(KickCommand.INSTANCE); From 1fa8d826f8fa77b9601f1dce6c71165df5a772ab Mon Sep 17 00:00:00 2001 From: Curle Date: Mon, 2 May 2022 22:05:24 +0100 Subject: [PATCH 08/10] Implement role-gating as a safeguard while the bot is being set up. --- .../tk/sciwhiz12/concord/ConcordConfig.java | 6 +++ .../command/ConcordDiscordCommand.java | 46 +++++++++---------- .../concord/command/discord/BanCommand.java | 19 +++++++- .../concord/command/discord/KickCommand.java | 20 +++++++- .../command/discord/WhitelistCommand.java | 20 +++++++- 5 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/main/java/tk/sciwhiz12/concord/ConcordConfig.java b/src/main/java/tk/sciwhiz12/concord/ConcordConfig.java index 7d93ff2d..e4fa4c3a 100644 --- a/src/main/java/tk/sciwhiz12/concord/ConcordConfig.java +++ b/src/main/java/tk/sciwhiz12/concord/ConcordConfig.java @@ -39,6 +39,8 @@ public class ConcordConfig { public static final ForgeConfigSpec.ConfigValue TOKEN; public static final ForgeConfigSpec.ConfigValue GUILD_ID; + public static final ForgeConfigSpec.ConfigValue CHANNEL_ID; + public static final ForgeConfigSpec.ConfigValue MODERATOR_ROLE_ID; public static final ForgeConfigSpec.ConfigValue CHAT_CHANNEL_ID; public static final ForgeConfigSpec.ConfigValue REPORT_CHANNEL_ID; @@ -110,6 +112,10 @@ public static void register() { REPORT_CHANNEL_ID = builder.comment("The snowflake ID of the channel where this bot will post reports from in-game users.", "If empty, reports will be disabled.") .define("report_channel_id", ""); + MODERATOR_ROLE_ID = builder.comment("The snowflake ID of the role that will be treated as a moderator role.", + "This role will be able to use Concord's Moderation slash commands on Discord - /kick, /ban, etc.", + "This should not be treated as an alternative to proper Discord permissions configuration, but exists as a safeguard so that random users may not ban you while you're setting up.") + .define("moderator_role_id", ""); builder.pop(); } diff --git a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java index 7d7385e8..7ded4a9c 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java @@ -37,17 +37,6 @@ public class ConcordDiscordCommand { private static JDA bot; private static MinecraftServer server; - private static void listCommand(SlashCommandEvent listEvent) { - listEvent.replyEmbeds(new EmbedBuilder() - .setTitle("Concord Integrations") - .setDescription("There are currently " + server.getPlayerCount() + " people online.") - .addField("Online Players", String.join("\n", server.getPlayerNames()), false) - .setTimestamp(Instant.now()) - .setColor(Color.CYAN) - .build() - ).setEphemeral(true).queue(); - } - private static void tpsCommand(SlashCommandEvent tpsEvent) { double meanTickTime = Mth.average(server.tickTimes) * 1.0E-6D;; double meanTPS = Math.min(1000.0/meanTickTime, 20); @@ -82,22 +71,31 @@ private static void helpCommand(SlashCommandEvent helpEvent) { // TODO } - private static void stopCommand(SlashCommandEvent stopEvent) { - // Short-circuit if on integrated server - if(FMLLoader.getDist() == Dist.CLIENT) { - stopEvent.reply("Sorry! This command is disabled on Integrated servers.").setEphemeral(true).queue(); - return; - } - - stopEvent.reply("Shutting the server down..").queue(); - Concord.BOT.getServer().halt(false); - } - public static void initialize(CommandDispatcher dispatcher) { - dispatcher.registerSingle("list", "List all online users.", "Show a count of online users, and their names.", ConcordDiscordCommand::listCommand); + dispatcher.registerSingle("list", "List all online users.", "Show a count of online users, and their names.", (listEvent) -> { + listEvent.replyEmbeds(new EmbedBuilder() + .setTitle("Concord Integrations") + .setDescription("There are currently " + server.getPlayerCount() + " people online.") + .addField("Online Players", String.join("\n", server.getPlayerNames()), false) + .setTimestamp(Instant.now()) + .setColor(Color.CYAN) + .build() + ).setEphemeral(true).queue(); + }); + dispatcher.registerSingle("tps", "Show the performance of the server.", "Display a breakdown of server performance, in current, average and separated by dimension.", ConcordDiscordCommand::tpsCommand); - dispatcher.registerSingle("stop", "Shut down your Minecraft server.", "Immediately schedule the shutdown of the Minecraft server, akin to /stop from in-game.", ConcordDiscordCommand::stopCommand); dispatcher.registerSingle("help", "Show detailed information about every single available command.", "Show the help information you are currently reading.", ConcordDiscordCommand::helpCommand); + dispatcher.registerSingle("stop", "Shut down your Minecraft server.", "Immediately schedule the shutdown of the Minecraft server, akin to /stop from in-game.", (event) -> { + // Short-circuit if on integrated server + if(FMLLoader.getDist() == Dist.CLIENT) { + event.reply("Sorry! This command is disabled on Integrated servers.").setEphemeral(true).queue(); + return; + } + + event.reply("Shutting the server down..").queue(); + Concord.BOT.getServer().halt(false); + }); + dispatcher.registerSingle(KickCommand.INSTANCE); dispatcher.registerSingle(BanCommand.INSTANCE); diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java index c4364a6a..32edb999 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java @@ -4,7 +4,6 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; -import net.minecraft.client.server.IntegratedServer; import net.minecraft.network.chat.TranslatableComponent; import net.minecraft.server.players.UserBanListEntry; import net.minecraftforge.api.distmarker.Dist; @@ -39,6 +38,24 @@ public BanCommand() { @Override public void execute(SlashCommandEvent event) { + var roleConfig = ConcordConfig.MODERATOR_ROLE_ID.get(); + if (!roleConfig.isEmpty()) { + var role = Concord.BOT.getDiscord().getRoleById(roleConfig); + // If no role, then it's non-empty and invalid; disable the command + if (role == null) { + event.reply("Sorry, but this command is disabled by configuration. Check the moderator_role_id option in the config.").setEphemeral(true).queue(); + return; + } else { + // If the member doesn't have the moderator role, then deny them the ability to use the command. + if (!event.getMember().getRoles().contains(role)) { + event.reply("Sorry, but you don't have permission to use this command.").setEphemeral(true).queue(); + return; + } + // Fall-through; member has the role, so they can use the command. + } + // Fall-through; the role is empty, so all permissions are handled by Discord. + } + var user = event.getOption(USER_OPTION.getName()).getAsString(); var server = Concord.BOT.getServer(); diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java index 71964015..649b66c1 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java @@ -4,7 +4,6 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; -import net.minecraft.client.server.IntegratedServer; import net.minecraft.network.chat.TextComponent; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.loading.FMLLoader; @@ -36,6 +35,25 @@ public KickCommand() { @Override public void execute(SlashCommandEvent event) { + // Check permissions. + var roleConfig = ConcordConfig.MODERATOR_ROLE_ID.get(); + if (!roleConfig.isEmpty()) { + var role = Concord.BOT.getDiscord().getRoleById(roleConfig); + // If no role, then it's non-empty and invalid; disable the command + if (role == null) { + event.reply("Sorry, but this command is disabled by configuration. Check the moderator_role_id option in the config.").setEphemeral(true).queue(); + return; + } else { + // If the member doesn't have the moderator role, then deny them the ability to use the command. + if (!event.getMember().getRoles().contains(role)) { + event.reply("Sorry, but you don't have permission to use this command.").setEphemeral(true).queue(); + return; + } + // Fall-through; member has the role, so they can use the command. + } + // Fall-through; the role is empty, so all permissions are handled by Discord. + } + var user = event.getOption(USER_OPTION.getName()).getAsString(); var server = Concord.BOT.getServer(); diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/WhitelistCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/WhitelistCommand.java index ed711d62..ce3b525a 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/WhitelistCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/WhitelistCommand.java @@ -6,11 +6,11 @@ import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; -import net.minecraft.client.server.IntegratedServer; import net.minecraft.server.players.UserWhiteListEntry; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.loading.FMLLoader; import tk.sciwhiz12.concord.Concord; +import tk.sciwhiz12.concord.ConcordConfig; import java.util.Optional; @@ -39,6 +39,24 @@ public WhitelistCommand() { @Override public void execute(SlashCommandEvent event) { + var roleConfig = ConcordConfig.MODERATOR_ROLE_ID.get(); + if (!roleConfig.isEmpty()) { + var role = Concord.BOT.getDiscord().getRoleById(roleConfig); + // If no role, then it's non-empty and invalid; disable the command + if (role == null) { + event.reply("Sorry, but this command is disabled by configuration. Check the moderator_role_id option in the config.").setEphemeral(true).queue(); + return; + } else { + // If the member doesn't have the moderator role, then deny them the ability to use the command. + if (!event.getMember().getRoles().contains(role)) { + event.reply("Sorry, but you don't have permission to use this command.").setEphemeral(true).queue(); + return; + } + // Fall-through; member has the role, so they can use the command. + } + // Fall-through; the role is empty, so all permissions are handled by Discord. + } + var server = Concord.BOT.getServer(); // Short circuit for singleplayer worlds From f3172dbd1a832570eed9ed139317995290acf843 Mon Sep 17 00:00:00 2001 From: Curle Date: Sun, 4 Sep 2022 15:40:04 +0100 Subject: [PATCH 09/10] 1.19, add help command. --- src/main/java/tk/sciwhiz12/concord/ChatBot.java | 2 ++ .../tk/sciwhiz12/concord/ConcordConfig.java | 1 - .../concord/command/ConcordDiscordCommand.java | 17 +++++++++++++---- .../concord/command/discord/BanCommand.java | 4 ++-- .../command/discord/CommandDispatcher.java | 12 ++++++++++-- .../concord/command/discord/KickCommand.java | 4 ++-- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/main/java/tk/sciwhiz12/concord/ChatBot.java b/src/main/java/tk/sciwhiz12/concord/ChatBot.java index 3887ad69..81e85a25 100644 --- a/src/main/java/tk/sciwhiz12/concord/ChatBot.java +++ b/src/main/java/tk/sciwhiz12/concord/ChatBot.java @@ -86,6 +86,8 @@ public MinecraftServer getServer() { return server; } + public CommandDispatcher getDispatcher() { return dispatcher; } + @Override public void onReady(ReadyEvent event) { discord.getPresence().setPresence(OnlineStatus.ONLINE, Activity.playing("some Minecraft")); diff --git a/src/main/java/tk/sciwhiz12/concord/ConcordConfig.java b/src/main/java/tk/sciwhiz12/concord/ConcordConfig.java index e4fa4c3a..aba40d77 100644 --- a/src/main/java/tk/sciwhiz12/concord/ConcordConfig.java +++ b/src/main/java/tk/sciwhiz12/concord/ConcordConfig.java @@ -39,7 +39,6 @@ public class ConcordConfig { public static final ForgeConfigSpec.ConfigValue TOKEN; public static final ForgeConfigSpec.ConfigValue GUILD_ID; - public static final ForgeConfigSpec.ConfigValue CHANNEL_ID; public static final ForgeConfigSpec.ConfigValue MODERATOR_ROLE_ID; public static final ForgeConfigSpec.ConfigValue CHAT_CHANNEL_ID; public static final ForgeConfigSpec.ConfigValue REPORT_CHANNEL_ID; diff --git a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java index 7ded4a9c..c9dc10f6 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java @@ -38,7 +38,7 @@ public class ConcordDiscordCommand { private static MinecraftServer server; private static void tpsCommand(SlashCommandEvent tpsEvent) { - double meanTickTime = Mth.average(server.tickTimes) * 1.0E-6D;; + double meanTickTime = Mth.average(server.tickTimes) * 1.0E-6D; double meanTPS = Math.min(1000.0/meanTickTime, 20); StringBuilder builder = new StringBuilder(); @@ -49,7 +49,7 @@ private static void tpsCommand(SlashCommandEvent tpsEvent) { if (times == null) times = new long[]{0}; - double worldTickTime = Mth.average(times) * 1.0E-6D;; + double worldTickTime = Mth.average(times) * 1.0E-6D; double worldTPS = Math.min(1000.0 / worldTickTime, 20); builder.append(dim.dimension().location()).append(": Mean tick time: ").append(worldTickTime).append(" ms. Mean TPS: ").append(worldTPS).append("\n"); @@ -67,8 +67,17 @@ private static void tpsCommand(SlashCommandEvent tpsEvent) { } private static void helpCommand(SlashCommandEvent helpEvent) { - helpEvent.reply("TODO").queue(); - // TODO + var dispatcher = Concord.BOT.getDispatcher(); + var commands = dispatcher.getCommands(); + + var builder = new EmbedBuilder().setTitle("Concord Commands") + .setDescription("There are " + commands.size() + " registered commands."); + + for (var command : commands) { + builder.addField(command.getName(), command.getHelpString(), true); + } + + helpEvent.replyEmbeds(builder.setTimestamp(Instant.now()).setColor(Color.GREEN).build()).setEphemeral(false).queue(); } public static void initialize(CommandDispatcher dispatcher) { diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java index 32edb999..48e91c40 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java @@ -4,7 +4,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; -import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.network.chat.Component; import net.minecraft.server.players.UserBanListEntry; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.loading.FMLLoader; @@ -86,7 +86,7 @@ public void execute(SlashCommandEvent event) { UserBanListEntry userbanlistentry = new UserBanListEntry(profile, (Date) null, "Discord User " + event.getMember().getEffectiveName(), (Date) null, reason); server.getPlayerList().getBans().add(userbanlistentry); // Kick them - player.connection.disconnect(new TranslatableComponent("multiplayer.disconnect.banned")); + player.connection.disconnect(Component.translatable("multiplayer.disconnect.banned")); event.reply("User " + user + " banned successfully.").queue(); return; diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java b/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java index ee78c5fa..b11dcd6e 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/CommandDispatcher.java @@ -30,10 +30,10 @@ public class CommandDispatcher extends ListenerAdapter { // The list of valid commands, to be upserted and listened for. - private List commands; + private final List commands; // The Map that powers the command listener - private Map commandsByName; + private final Map commandsByName; // Whether this Dispatcher should only listen on a single guild, in a testing configuration. private boolean testMode; @@ -71,6 +71,14 @@ public CommandDispatcher() { this.testGuild = null; } + /** + * Get a list of all registered commands. + * Exposed primarily for the help command to be able to read details on all other commands. + */ + public List getCommands() { + return commands; + } + /** * A condensed way of adding a command, that doesn't require creating a new class. * Provide the necessary information along with a Consumer and the rest will be handled for you. diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java index 649b66c1..7dbd5948 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java @@ -4,7 +4,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.requests.restaction.CommandCreateAction; -import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.Component; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.loading.FMLLoader; import tk.sciwhiz12.concord.Concord; @@ -76,7 +76,7 @@ public void execute(SlashCommandEvent event) { if (List.of(server.getPlayerNames()).contains(user)) { var player = server.getPlayerList().getPlayerByName(user); // If they are, kick them with the message. - player.connection.disconnect(new TextComponent(reason)); + player.connection.disconnect(Component.literal(reason)); // Reply to the user. event.reply("User " + user + " kicked successfully.").queue(); From 994636edc7345f7b80a3afba52fb79779314f3d3 Mon Sep 17 00:00:00 2001 From: Curle Date: Sun, 4 Sep 2022 15:55:13 +0100 Subject: [PATCH 10/10] Update ban + kick message --- .../concord/command/discord/BanCommand.java | 6 +++++- .../concord/command/discord/KickCommand.java | 13 +++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java index 48e91c40..20f04398 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java @@ -86,7 +86,11 @@ public void execute(SlashCommandEvent event) { UserBanListEntry userbanlistentry = new UserBanListEntry(profile, (Date) null, "Discord User " + event.getMember().getEffectiveName(), (Date) null, reason); server.getPlayerList().getBans().add(userbanlistentry); // Kick them - player.connection.disconnect(Component.translatable("multiplayer.disconnect.banned")); + player.connection.disconnect( + reasonMapping == null ? + Component.translatable("multiplayer.disconnect.banned") : + Component.literal(reasonMapping.getAsString()) + ); event.reply("User " + user + " banned successfully.").queue(); return; diff --git a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java index 7dbd5948..55983a6f 100644 --- a/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java +++ b/src/main/java/tk/sciwhiz12/concord/command/discord/KickCommand.java @@ -65,18 +65,15 @@ public void execute(SlashCommandEvent event) { var reasonMapping = event.getOption(REASON_OPTION.getName()); - // The Reason Option is optional, so default to "Reason Not Specified" if it isn't. - var reason = ""; - if (reasonMapping == null) - reason = "Reason Not Specified"; - else - reason = reasonMapping.getAsString(); - // Check whether the user is online if (List.of(server.getPlayerNames()).contains(user)) { var player = server.getPlayerList().getPlayerByName(user); // If they are, kick them with the message. - player.connection.disconnect(Component.literal(reason)); + player.connection.disconnect( + reasonMapping == null ? + Component.translatable("multiplayer.disconnect.kicked") : + Component.literal(reasonMapping.getAsString()) + ); // Reply to the user. event.reply("User " + user + " kicked successfully.").queue();