Skip to content
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