Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Discord Commands framework, and some basic commands as an example. #5

Open
wants to merge 10 commits into
base: dev/1.19.x
Choose a base branch
from
11 changes: 11 additions & 0 deletions src/main/java/tk/sciwhiz12/concord/ChatBot.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -56,6 +58,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;
Expand All @@ -65,6 +68,12 @@ public class ChatBot extends ListenerAdapter {
playerListener = new PlayerListener(this);
statusListener = new StatusListener(this);

// Initialize Discord-side commands
dispatcher = new CommandDispatcher();
discord.addEventListener(dispatcher);

ConcordDiscordCommand.initialize(dispatcher);

// Prevent any mentions not explicitly specified
MessageAction.setDefaultMentions(Collections.emptySet());
}
Expand All @@ -77,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"));
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/tk/sciwhiz12/concord/Concord.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/tk/sciwhiz12/concord/ConcordConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class ConcordConfig {

public static final ForgeConfigSpec.ConfigValue<String> TOKEN;
public static final ForgeConfigSpec.ConfigValue<String> GUILD_ID;
public static final ForgeConfigSpec.ConfigValue<String> MODERATOR_ROLE_ID;
public static final ForgeConfigSpec.ConfigValue<String> CHAT_CHANNEL_ID;
public static final ForgeConfigSpec.ConfigValue<String> REPORT_CHANNEL_ID;

Expand Down Expand Up @@ -110,6 +111,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();
}
Expand Down
118 changes: 118 additions & 0 deletions src/main/java/tk/sciwhiz12/concord/command/ConcordDiscordCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package tk.sciwhiz12.concord.command;

import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
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;
import tk.sciwhiz12.concord.command.discord.KickCommand;
import tk.sciwhiz12.concord.command.discord.WhitelistCommand;

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 <user> [reason]; remove a user from the server, optionally with the specified reason. See @link{KickCommand}.
* - ban <user> [reason]; ban a user from the server, optionally with the specified reason. See @link{BanCommand}
* - whitelist <remove|add> <user>; 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.
*
* @author Curle
*/
public class ConcordDiscordCommand {
private static JDA bot;
private static MinecraftServer server;

private static void tpsCommand(SlashCommandEvent tpsEvent) {
double meanTickTime = Mth.average(server.tickTimes) * 1.0E-6D;
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) * 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("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())
.build()
).setEphemeral(true).queue();
}

private static void helpCommand(SlashCommandEvent helpEvent) {
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) {
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("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);
dispatcher.registerSingle(WhitelistCommand.INSTANCE);
}

public static void postInit() {
bot = Concord.BOT.getDiscord();
server = Concord.BOT.getServer();
}
}
108 changes: 108 additions & 0 deletions src/main/java/tk/sciwhiz12/concord/command/discord/BanCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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.network.chat.Component;
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 <user> [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);

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 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();


// Short-circuit for integrated servers.
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;
}

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(
reasonMapping == null ?
Component.translatable("multiplayer.disconnect.banned") :
Component.literal(reasonMapping.getAsString())
);

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);
}
}
Loading