From f5b072bae3d7de7e2c8cb17b3558cb749b716ff4 Mon Sep 17 00:00:00 2001 From: Omri H Date: Sun, 18 Jan 2026 19:20:16 +0200 Subject: [PATCH 1/4] move existing code to java and remove all dependency on kotlin --- build.gradle.kts | 8 +- gradle.properties | 3 +- shell.nix | 17 ++ .../legitimoose/bot/LegitimooseBotClient.java | 225 ++++++++++++++++++ .../java/net/legitimoose/bot/Scraper.java | 125 ++++++++++ .../java/net/legitimoose/bot/World.java | 98 ++++++++ .../legitimoose/bot/discord/DiscordBot.java | 116 +++++++++ .../bot/discord/command/Command.java | 5 + .../bot/discord/command/FindCommand.java | 29 +++ .../bot/discord/command/ListCommand.java | 78 ++++++ .../bot/discord/command/MsgCommand.java | 36 +++ .../bot/discord/command/staff/Rejoin.java | 20 ++ .../bot/discord/command/staff/Restart.java | 20 ++ .../bot/discord/command/staff/Send.java | 33 +++ .../legitimoose/bot/LegitimooseBotClient.kt | 208 ---------------- .../kotlin/net/legitimoose/bot/Scraper.kt | 149 ------------ .../kotlin/net/legitimoose/bot/World.kt | 95 -------- .../net/legitimoose/bot/discord/Command.kt | 9 - .../net/legitimoose/bot/discord/DiscordBot.kt | 107 --------- .../legitimoose/bot/discord/FindCommand.kt | 22 -- .../legitimoose/bot/discord/ListCommand.kt | 60 ----- .../net/legitimoose/bot/discord/MsgCommand.kt | 28 --- .../bot/discord/ShoutCommand.kt.disabled | 22 -- .../legitimoose/bot/discord/staff/Rejoin.kt | 13 - .../legitimoose/bot/discord/staff/Restart.kt | 13 - .../net/legitimoose/bot/discord/staff/Send.kt | 24 -- .../net/legitimoose/bot/LegitimooseBot.java | 16 ++ .../net/legitimoose/bot/LegitimooseBot.kt | 13 - src/main/resources/fabric.mod.json | 10 +- 29 files changed, 822 insertions(+), 780 deletions(-) create mode 100644 shell.nix create mode 100644 src/client/java/net/legitimoose/bot/LegitimooseBotClient.java create mode 100644 src/client/java/net/legitimoose/bot/Scraper.java create mode 100644 src/client/java/net/legitimoose/bot/World.java create mode 100644 src/client/java/net/legitimoose/bot/discord/DiscordBot.java create mode 100644 src/client/java/net/legitimoose/bot/discord/command/Command.java create mode 100644 src/client/java/net/legitimoose/bot/discord/command/FindCommand.java create mode 100644 src/client/java/net/legitimoose/bot/discord/command/ListCommand.java create mode 100644 src/client/java/net/legitimoose/bot/discord/command/MsgCommand.java create mode 100644 src/client/java/net/legitimoose/bot/discord/command/staff/Rejoin.java create mode 100644 src/client/java/net/legitimoose/bot/discord/command/staff/Restart.java create mode 100644 src/client/java/net/legitimoose/bot/discord/command/staff/Send.java delete mode 100644 src/client/kotlin/net/legitimoose/bot/LegitimooseBotClient.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/Scraper.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/World.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/Command.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/DiscordBot.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/FindCommand.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/ListCommand.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/MsgCommand.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/ShoutCommand.kt.disabled delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/staff/Rejoin.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/staff/Restart.kt delete mode 100644 src/client/kotlin/net/legitimoose/bot/discord/staff/Send.kt create mode 100644 src/main/java/net/legitimoose/bot/LegitimooseBot.java delete mode 100644 src/main/kotlin/net/legitimoose/bot/LegitimooseBot.kt diff --git a/build.gradle.kts b/build.gradle.kts index 8bf6c97..76d8499 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,4 @@ plugins { - kotlin("jvm") version "2.2.21" id("fabric-loom") version "1.11.5" id("com.gradleup.shadow") version "9.0.2" } @@ -33,11 +32,8 @@ dependencies { modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}") modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}") - modImplementation( - "net.fabricmc:fabric-language-kotlin:${project.property("fabric_kotlin_version")}") - shadow(implementation("org.mongodb:mongodb-driver-kotlin-sync:5.5.1")!!) - shadow(implementation("org.mongodb:bson-kotlinx:5.5.1")!!) + shadow(implementation("org.mongodb:mongodb-driver-sync:5.5.1")!!) shadow(implementation("net.dv8tion:JDA:5.6.1") { exclude("opus-java") }) } @@ -75,5 +71,3 @@ tasks { } java { toolchain.languageVersion = JavaLanguageVersion.of(21) } - -kotlin { jvmToolchain(21) } diff --git a/gradle.properties b/gradle.properties index 649eb39..abc8146 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,10 +5,9 @@ org.gradle.jvmargs=-Xmx1G minecraft_version=1.21.10 loader_version=0.17.2 # Mod Properties -mod_version=1.0.1 +mod_version=2.0.0 maven_group=net.legitimoose archives_base_name=Legitimoose-Bot # Dependencies # check this on https://modmuss50.me/fabric.html fabric_version=0.138.3+1.21.10 -fabric_kotlin_version=1.13.7+kotlin.2.2.21 diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..c204f2b --- /dev/null +++ b/shell.nix @@ -0,0 +1,17 @@ +{ pkgs ? import {} }: +let + runtimeLibs = with pkgs; lib.makeLibraryPath [ + glfw + libGL + xorg.libX11 + xorg.libXcursor + xorg.libXext + xorg.libXrandr + xorg.libXxf86vm + libglvnd + ]; +in pkgs.mkShell { + shellHook = '' + export LD_LIBRARY_PATH=${runtimeLibs}:$LD_LIBRARY_PATH + ''; +} diff --git a/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java b/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java new file mode 100644 index 0000000..c05e86b --- /dev/null +++ b/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java @@ -0,0 +1,225 @@ +package net.legitimoose.bot; + +import com.mojang.brigadier.CommandDispatcher; +import net.dv8tion.jda.api.entities.Member; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.legitimoose.bot.discord.DiscordBot; +import net.legitimoose.bot.discord.DiscordWebhook; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.*; +import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import net.minecraft.commands.CommandBuildContext; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static net.legitimoose.bot.LegitimooseBot.CONFIG; +import static net.legitimoose.bot.LegitimooseBot.LOGGER; + +public class LegitimooseBotClient implements ClientModInitializer { + public static final Minecraft mc = Minecraft.getInstance(); + public static final Scraper scraper = new Scraper(); + + private final Pattern joinPattern = Pattern.compile("^\\[\\+]\\s*(?:[^|]+\\|\\s*)?(\\S+)"); + private final Pattern switchPattern = Pattern.compile("^\\[→]\\s*(?:[^|]+\\|\\s*)?(\\S+)"); + private final Pattern leavePattern = Pattern.compile("^\\[-]\\s*(?:[^|]+\\|\\s*)?(\\S+)"); + + private final Pattern chatPattern = Pattern.compile("^(?:\\[SHOUT]\\s*)?(?:[^|]+\\|\\s*)?([^:]+): (.*)"); + private final Pattern msgPattern = Pattern.compile("\\[(.*) -> me] @(.*) (.*)"); + + static volatile private long lastJoinTimestamp = 0L; + private static long REJOIN_COOLDOWN_MS = 5_000L; + + Timer timer = new Timer(); + + @Override + public void onInitializeClient() { + timer.schedule(new TimerTask() { + @Override + public void run() { + System.exit(67); + } + }, TimeUnit.HOURS.toMillis(24), TimeUnit.HOURS.toMillis(24)); + + ClientCommandRegistrationCallback.EVENT.register((dispatcher, commandBuildContext) -> dispatcher.register( + ClientCommandManager.literal("scrape").executes((context -> { + new Thread(scraper::scrape).start(); + return 1; + })))); + + new Thread(DiscordBot::run).start(); + + ClientTickEvents.END_CLIENT_TICK.register((minecraft) -> rejoin(false)); + + new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(10); + } catch (InterruptedException e) { + LOGGER.warn(e.getMessage()); + } + rejoin(false); + }).start(); + + new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + LOGGER.warn(e.getMessage()); + } + while (true) { + scraper.scrape(); + try { + TimeUnit.MINUTES.sleep((long) CONFIG.getOrDefault("waitMinutesBetweenScrapes", 10)); + } catch (InterruptedException e) { + LOGGER.warn(e.getMessage()); + } + } + }).start(); + + new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + LOGGER.warn(e.getMessage()); + } + while (true) { + try { + mc.player + .connection + . + sendChat("
I am a bot that syncs lobby chat to a community Discord
To prevent messages being sent to discord, prefix your messages with ::
You can check out our work at https://legiti.dev/"); + TimeUnit.MINUTES.sleep(20); + } catch (InterruptedException e) { + LOGGER.warn(e.getMessage()); + } + } + }).start(); + + ClientReceiveMessageEvents.GAME.register(((message, overlay) -> { + new Thread(() -> { + String msg = message.getString(); + String username = ""; + String cleanMessage = msg; + + Matcher joinMatcher = joinPattern.matcher(msg); + Matcher switchMatcher = switchPattern.matcher(msg); + Matcher leaveMatcher = leavePattern.matcher(msg); + Matcher chatMatcher = chatPattern.matcher(msg); + Matcher msgMatcher = msgPattern.matcher(msg); + + DiscordWebhook webhook = new DiscordWebhook(CONFIG.getOrDefault("webhookUrl", "")); + if (joinMatcher.find()) { + username = joinMatcher.group(1); + cleanMessage = String.format("**%s** joined the server.", username); + webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); + webhook.setContent(cleanMessage.replace("@", "")); + try { + webhook.execute(0x57F287); + } catch (IOException | URISyntaxException e) { + LOGGER.warn(e.getMessage()); + } + return; + } else if (switchMatcher.find()) { + username = switchMatcher.group(1); + cleanMessage = String.format("**%s** switched servers.", username); + webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); + webhook.setContent(cleanMessage.replace("@", "")); + try { + webhook.execute(0xF2F257); + } catch (IOException | URISyntaxException e) { + LOGGER.warn(e.getMessage()); + } + return; + } else if (leaveMatcher.find()) { + username = leaveMatcher.group(1); + cleanMessage = String.format("**%s** left the server.", username); + webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); + webhook.setContent(cleanMessage.replace("@", "")); + try { + webhook.execute(0xF25757); + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + return; + } else if (chatMatcher.find()) { + username = chatMatcher.group(1); + cleanMessage = chatMatcher.group(2); + if (msg.startsWith("[SHOUT]")) { + webhook.setUsername("[SHOUT] $username"); + } else { + webhook.setUsername(username); + } + webhook.setAvatarUrl(String.format("https://mc-heads.net/avatar/%s", username)); + } else if (msgMatcher.find()) { + String username1 = msgMatcher.group(1); + String username2 = msgMatcher.group(2); + String msg1 = msgMatcher.group(3); + Member member = + DiscordBot.jda + .getGuildById(1311574348989071440L) + .findMembers(s -> s.getUser().getName().equals(username2)) + .get().get(0); + member.getUser() + .openPrivateChannel() + .flatMap(channel -> channel.sendMessage(String.format("%s: %s", username1, msg1))) + .queue(); + return; + } + + if (username == mc.player.getName().getString()) return; + + if (!username.isEmpty() && + !cleanMessage.startsWith(CONFIG.getOrDefault("secretPrefix", "::")) + ) { + webhook.setContent(cleanMessage.replace("@", "")); + try { + webhook.execute(); + } catch (IOException | URISyntaxException e) { + LOGGER.warn(e.getMessage()); + } + } + }).start(); + })); + } + + public static void rejoin(boolean force) { + if (FabricLoader.getInstance().isDevelopmentEnvironment()) return; + Screen screen = mc.screen; + if (screen instanceof DisconnectedScreen || + screen instanceof JoinMultiplayerScreen || + screen instanceof TitleScreen || + screen instanceof AccessibilityOnboardingScreen || + (mc.getConnection() != null && force) + ) { + Minecraft.getInstance().schedule(() -> { + long now = System.currentTimeMillis(); + if (now - lastJoinTimestamp >= REJOIN_COOLDOWN_MS) { + lastJoinTimestamp = now; + LOGGER.info("Attempting to reconnect to server"); + ServerData info = new ServerData("Server", "legitimoose.com", ServerData.Type.OTHER); + ConnectScreen.startConnecting( + new JoinMultiplayerScreen(null), + mc, + ServerAddress.parseString("legitimoose.com"), + info, + false, + null + ); + } + }); + } + } +} diff --git a/src/client/java/net/legitimoose/bot/Scraper.java b/src/client/java/net/legitimoose/bot/Scraper.java new file mode 100644 index 0000000..9047f2c --- /dev/null +++ b/src/client/java/net/legitimoose/bot/Scraper.java @@ -0,0 +1,125 @@ +package net.legitimoose.bot; + +import com.mojang.serialization.JsonOps; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import net.legitimoose.bot.discord.DiscordWebhook; +import net.minecraft.client.Minecraft; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ItemStack; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.concurrent.TimeUnit; + +import static net.legitimoose.bot.LegitimooseBot.CONFIG; +import static net.legitimoose.bot.LegitimooseBot.LOGGER; + +public class Scraper { + private final MongoClient mongoClient = MongoClients.create(CONFIG.getOrDefault("mongoUri", "")); + private final DiscordWebhook errorWebhook = new DiscordWebhook(CONFIG.getOrDefault("errorWebhookUrl", "")); + + public final MongoDatabase db = mongoClient.getDatabase("legitimooseapi"); + + private void waitSeconds(long time) { + try { + TimeUnit.SECONDS.sleep(time); + } catch (InterruptedException e) { + LOGGER.warn("Failed to wait {} seconds:", time); + LOGGER.warn(e.getMessage()); + } + } + + private void error(String message, Exception exception) throws IOException, URISyntaxException { + LOGGER.error(message); + LOGGER.error(exception.getMessage()); + errorWebhook.setContent("$message\n${exception.message}"); + errorWebhook.execute(); + } + + public void scrape() { + Minecraft client = Minecraft.getInstance(); + client.player.connection.sendCommand("worlds"); + + waitSeconds(1); + int max_pages = Integer.parseInt(client.screen.getTitle().getSiblings().get(0).getString().substring(3)); + LOGGER.info("Last page is: {}", max_pages); + for (int i = 0; i <= max_pages; i++) { + Container inv = client.player.containerMenu.getSlot(0).container; + for (int j = 0; j <= 26; j++) { + if (client.player.containerMenu.containerId == 0) + return; // should check if player closed the inventory not sure though + ItemStack itemStack = inv.getItem(j); + // last page & air: break, last world was already hit. + if (i == max_pages && itemStack.toString().substring(2) == "minecraft:air") break; + CompoundTag customData = itemStack.get(DataComponents.CUSTOM_DATA).copyTag(); + CompoundTag publicBukkitValues = (CompoundTag) customData.get("PublicBukkitValues"); + int jam_id = (!publicBukkitValues.get("datapackserverpaper:jam_id").asString().get().isEmpty()) ? Integer.parseInt(publicBukkitValues.get("datapackserverpaper:jam_id").asString().get()) : null; + World world = + new World( + publicBukkitValues.get("datapackserverpaper:creation_date").asString().get(), + Integer.parseInt(publicBukkitValues + .get("datapackserverpaper:creation_date_unix_seconds") + .asString() + .get()), + Boolean.parseBoolean(publicBukkitValues + .get("datapackserverpaper:enforce_whitelist") + .asString() + .get()), + Boolean.parseBoolean(publicBukkitValues + .get("datapackserverpaper:locked") + .asString() + .get()), + publicBukkitValues.get("datapackserverpaper:owner").asString().get(), + Integer.parseInt(publicBukkitValues + .get("datapackserverpaper:player_count") + .asString() + .get()), + publicBukkitValues + .get("datapackserverpaper:resource_pack_url") + .asString() + .get(), + publicBukkitValues.get("datapackserverpaper:uuid").asString().get(), + publicBukkitValues.get("datapackserverpaper:version").asString().get(), + Integer.parseInt(publicBukkitValues.get("datapackserverpaper:visits").asString().get()), + Integer.parseInt(publicBukkitValues.get("datapackserverpaper:votes").asString().get()), + Boolean.parseBoolean(publicBukkitValues + .get("datapackserverpaper:whitelist_on_version_change") + .asString() + .get()), + itemStack.get(DataComponents.CUSTOM_NAME).getString(), + itemStack.get(DataComponents.LORE).lines().get(0).getString(), + Boolean.parseBoolean(publicBukkitValues.get("datapackserverpaper:jam_world").asString().get()), + jam_id, + ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, itemStack.get(DataComponents.CUSTOM_NAME)) + .result() + .get() + .toString(), + ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, itemStack.get(DataComponents.LORE).lines().get(0)) + .result() + .get() + .toString(), + itemStack.toString().substring(2), + System.currentTimeMillis() / 1000L + ); + LOGGER.info("Scraped World $j ${world.world_uuid}: ${world.name}"); + world.upload(db); + } + // finally, click on next page button + LOGGER.info("Scraped page #$i"); + Minecraft.getInstance() + .gameMode + .handleInventoryMouseClick( + client.player.containerMenu.containerId, 32, 0, ClickType.PICKUP, client.player + ); + waitSeconds(5); // wait five seconds to give legmos time to load + } + client.player.closeContainer(); + LOGGER.info("Finished Scraping"); + } +} diff --git a/src/client/java/net/legitimoose/bot/World.java b/src/client/java/net/legitimoose/bot/World.java new file mode 100644 index 0000000..28a549a --- /dev/null +++ b/src/client/java/net/legitimoose/bot/World.java @@ -0,0 +1,98 @@ +package net.legitimoose.bot; + +import com.mongodb.MongoClientException; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.Updates; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.bson.types.ObjectId; + +import static com.mongodb.client.model.Filters.eq; +import static com.mongodb.client.model.Filters.lt; + +import static net.legitimoose.bot.LegitimooseBot.LOGGER; + +public record World( + String creation_date, + int creation_date_unix_seconds, + boolean enforce_whitelist, + boolean locked, + String owner_uuid, + int player_count, + String resource_pack_url, + String world_uuid, + String version, + int visits, + int votes, + boolean whitelist_on_version_change, + String name, + String description, + boolean jam_world, + int jam_id, + String raw_name, + String raw_description, + String icon, + long last_scraped +) { + public void upload(MongoDatabase db) { + MongoCollection coll = db.getCollection("worlds"); + Document doc; + try { + doc = coll.find(eq("world_uuid", this.world_uuid)).first(); + } catch (MongoClientException e) { + doc = null; + } + coll.deleteMany(lt("last_scraped", System.currentTimeMillis() / 1000L - 86400)); + if (doc != null) { + LOGGER.info("updating world"); + Bson updates = + Updates.combine( + Updates.set("enforce_whitelist", this.enforce_whitelist), + Updates.set("locked", this.locked), + Updates.set("owner_uuid", this.owner_uuid), + Updates.set("player_count", this.player_count), + Updates.set("resource_pack_url", this.resource_pack_url), + Updates.set("version", this.version), + Updates.set("visits", this.visits), + Updates.set("votes", this.votes), + Updates.set("whitelist_on_version_change", this.whitelist_on_version_change), + Updates.set("name", this.name), + Updates.set("description", this.description), + Updates.set("jam_world", this.jam_world), + Updates.set("jam_id", this.jam_id), + Updates.set("raw_name", this.raw_name), + Updates.set("raw_description", this.raw_description), + Updates.set("icon", this.icon), + Updates.set("last_scraped", this.last_scraped)); + coll.updateOne(eq("world_uuid", this.world_uuid), updates, new UpdateOptions()); + LOGGER.info("Updated world"); + return; + } + coll.insertOne( + new Document() + .append("_id", new ObjectId()) + .append("creation_date", this.creation_date) + .append("creation_date_unix_seconds", this.creation_date_unix_seconds) + .append("enforce_whitelist", this.enforce_whitelist) + .append("locked", this.locked) + .append("owner_uuid", this.owner_uuid) + .append("player_count", this.player_count) + .append("resource_pack_url", this.resource_pack_url) + .append("world_uuid", this.world_uuid) + .append("version", this.version) + .append("visits", this.visits) + .append("votes", this.votes) + .append("whitelist_on_version_change", this.whitelist_on_version_change) + .append("name", this.name) + .append("description", this.description) + .append("jam_world", this.jam_world) + .append("jam_id", this.jam_id) + .append("raw_name", this.raw_name) + .append("raw_description", this.raw_description) + .append("icon", this.icon) + .append("last_scraped", this.last_scraped)); + LOGGER.info("Created world"); + } +} \ No newline at end of file diff --git a/src/client/java/net/legitimoose/bot/discord/DiscordBot.java b/src/client/java/net/legitimoose/bot/discord/DiscordBot.java new file mode 100644 index 0000000..46f516e --- /dev/null +++ b/src/client/java/net/legitimoose/bot/discord/DiscordBot.java @@ -0,0 +1,116 @@ +package net.legitimoose.bot.discord; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.events.guild.GuildReadyEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.legitimoose.bot.LegitimooseBot; +import net.legitimoose.bot.discord.command.FindCommand; +import net.legitimoose.bot.discord.command.ListCommand; +import net.legitimoose.bot.discord.command.MsgCommand; +import net.legitimoose.bot.discord.command.staff.Rejoin; +import net.legitimoose.bot.discord.command.staff.Restart; +import net.legitimoose.bot.discord.command.staff.Send; +import net.minecraft.client.Minecraft; + +import static net.legitimoose.bot.LegitimooseBot.CONFIG; +import static net.legitimoose.bot.LegitimooseBot.LOGGER; + +public class DiscordBot extends ListenerAdapter { + public static JDA jda; + + public static void run() { + jda = + JDABuilder.createDefault(CONFIG.getOrDefault("discordToken", "")) + .enableIntents(GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_MEMBERS) + .build(); + + jda.addEventListener(new DiscordBot()); + jda.updateCommands() + .addCommands( + Commands.slash("list", "List online players in the server") + .addOption( + OptionType.BOOLEAN, + "lobby", + "True if you only want to see online players in the lobby"), + Commands.slash("find", "Find which world a player is in") + .addOption( + OptionType.STRING, + "player", + "The username of the player you want to find", + true), + Commands.slash("msg", "Message an ingame player") + .addOption( + OptionType.STRING, + "player", + "The username of the player you want to message", + true) + .addOption(OptionType.STRING, "message", "The message you want to send", true)) + .queue(); + } + + @Override + public void onGuildReady(GuildReadyEvent event) { + if (!event.getGuild().getId().equals(CONFIG.getOrDefault("discordGuildId", "1311574348989071440"))) return; + event.getGuild() + .updateCommands() + .addCommands( + Commands.slash("rejoin", "Rejoin server") + .setDefaultPermissions( + DefaultMemberPermissions.enabledFor(Permission.MANAGE_SERVER)), + Commands.slash("restart", "Restart bot") + .setDefaultPermissions( + DefaultMemberPermissions.enabledFor(Permission.MANAGE_SERVER)), + Commands.slash("send", "Send message") + .setDefaultPermissions( + DefaultMemberPermissions.enabledFor(Permission.MANAGE_SERVER)) + .addOption(OptionType.STRING, "message", "The message to send", true)) + .queue(); + } + + @Override + public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { + switch (event.getName()) { + case "list" -> { + boolean lobby; + if (event.getOption("lobby") != null) { + lobby = event.getOption("lobby").getAsBoolean(); + } else { + lobby = false; + } + new ListCommand(event, lobby).onCommandReceived(); + } + case "find" -> new FindCommand(event, event.getOption("player").getAsString()).onCommandReceived(); + case "msg" -> + new MsgCommand(event, event.getOption("message").getAsString(), event.getOption("player").getAsString()).onCommandReceived(); + case "rejoin" -> new Rejoin(event).onCommandReceived(); + case "restart" -> new Restart(event).onCommandReceived(); + case "send" -> new Send(event, event.getOption("message").getAsString()).onCommandReceived(); + } + } + + @Override + public void onMessageReceived(MessageReceivedEvent event) { + if (event.isWebhookMessage() || event.getAuthor().isBot()) return; + String discordNick = event.getMember().getEffectiveName().replace("§", "?"); + String message = + "
ᴅɪsᴄᴏʀᴅ $discordNick: " + + event.getMessage().getContentStripped().replace("\n", "
").replace("§", "?"); + if (!event.getMessage().getAttachments().isEmpty()) { + message += " [Attachment Included]"; + } + if (message.length() >= 200) return; + if (CONFIG.getOrDefault("channelId", "").isEmpty()) + LOGGER.error("Discord channel ID is not set in config!"); + if (event.getChannel().getId().equals(CONFIG.getOrDefault("channelId", ""))) { + Minecraft.getInstance().player.connection.sendChat(message); + } + } +} diff --git a/src/client/java/net/legitimoose/bot/discord/command/Command.java b/src/client/java/net/legitimoose/bot/discord/command/Command.java new file mode 100644 index 0000000..56ad82a --- /dev/null +++ b/src/client/java/net/legitimoose/bot/discord/command/Command.java @@ -0,0 +1,5 @@ +package net.legitimoose.bot.discord.command; + +public interface Command { + void onCommandReceived(); +} diff --git a/src/client/java/net/legitimoose/bot/discord/command/FindCommand.java b/src/client/java/net/legitimoose/bot/discord/command/FindCommand.java new file mode 100644 index 0000000..32e6a78 --- /dev/null +++ b/src/client/java/net/legitimoose/bot/discord/command/FindCommand.java @@ -0,0 +1,29 @@ +package net.legitimoose.bot.discord.command; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.legitimoose.bot.LegitimooseBot; +import net.legitimoose.bot.LegitimooseBotClient; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; + + +public class FindCommand implements Command { + final SlashCommandInteractionEvent event; + final String player; + + public FindCommand(SlashCommandInteractionEvent event, String player) { + this.event = event; + this.player = player; + } + + @Override + public void onCommandReceived() { + if (player.length() >= 200) { + event.reply("player name too long, sorry!").setEphemeral(true).queue(); + return; + } + Minecraft.getInstance().player.connection.sendCommand("find " + player.replace("§", "?")); + event.reply(LegitimooseBotClient.mc.gui.getChat().getRecentChat().get(0).replace(" Click HERE to join.", "").trim()).queue(); + } +} diff --git a/src/client/java/net/legitimoose/bot/discord/command/ListCommand.java b/src/client/java/net/legitimoose/bot/discord/command/ListCommand.java new file mode 100644 index 0000000..1a03509 --- /dev/null +++ b/src/client/java/net/legitimoose/bot/discord/command/ListCommand.java @@ -0,0 +1,78 @@ +package net.legitimoose.bot.discord.command; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mongodb.client.MongoCollection; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.legitimoose.bot.LegitimooseBotClient; +import net.legitimoose.bot.Scraper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.PlayerInfo; +import org.bson.Document; +import org.bson.types.ObjectId; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static net.legitimoose.bot.LegitimooseBotClient.scraper; +import static net.legitimoose.bot.LegitimooseBot.LOGGER; + +public class ListCommand implements Command { + final SlashCommandInteractionEvent event; + final boolean lobby; + + public ListCommand(SlashCommandInteractionEvent event, boolean lobby) { + this.event = event; + this.lobby = lobby; + } + + @Override + public void onCommandReceived() { + if (lobby) { + Collection playerList = + Minecraft.getInstance().getConnection().getOnlinePlayers(); + StringBuilder players = new StringBuilder(); + for (PlayerInfo player : playerList) { + players.append(player.getTabListDisplayName().getString()).append('\n'); + } + event.reply(players.toString()).queue(); + } else { + MongoCollection coll = scraper.db.getCollection("stats"); + + event + .deferReply() + .queue(); // It *does* send a packet to the mc server, so keeping this is safer... + + // Please ignore the nulls. Only the 'input' is actually used + CommandContext context = new CommandContextBuilder(null, null, null, 1).build("/find "); + + CompletableFuture pendingParse = + Minecraft.getInstance() + .player + .connection + .getSuggestionsProvider() + .customSuggestion(context); + + pendingParse.thenRun(() -> { + if (!pendingParse.isDone()) { + LOGGER.warn("Pending parse is not done! (somehow??)"); + return; + } + List mcSuggestions = pendingParse.join().getList(); + StringBuilder suggestions = new StringBuilder(); + for (Suggestion suggestion : mcSuggestions) { + suggestions.append(suggestion.getText() + '\n'); + } + event.getHook() + .sendMessage( + String.format("There are %s player(s) online:\n```\n%s```", mcSuggestions.size(), suggestions.toString())) + .queue(); + + coll.insertOne(new Document().append("_id", new ObjectId()).append("player_count", mcSuggestions.size())); + }); + } + } +} diff --git a/src/client/java/net/legitimoose/bot/discord/command/MsgCommand.java b/src/client/java/net/legitimoose/bot/discord/command/MsgCommand.java new file mode 100644 index 0000000..f8c365d --- /dev/null +++ b/src/client/java/net/legitimoose/bot/discord/command/MsgCommand.java @@ -0,0 +1,36 @@ +package net.legitimoose.bot.discord.command; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.minecraft.client.Minecraft; + +public class MsgCommand implements Command { + final SlashCommandInteractionEvent event; + final String message; + final String player; + + public MsgCommand(SlashCommandInteractionEvent event, String message, String player) { + this.event = event; + this.message = message; + this.player = player; + } + + @Override + public void onCommandReceived() { + if ((message.length() + player.length()) >= 200) { + event.reply("Failed to send, message and/or player name too long!").setEphemeral(true).queue(); + return; + } + + Minecraft.getInstance() + .player + .connection + .sendCommand( + "msg " + + player.replace("§", "?") + + " [ᴅɪsᴄᴏʀᴅ] " + + event.getMember().getEffectiveName() + + ": " + + message.replace("\n", "
").replace("§", "?")); + event.reply("Sent `" + message.trim() + "` to " + player).setEphemeral(true).queue(); + } +} diff --git a/src/client/java/net/legitimoose/bot/discord/command/staff/Rejoin.java b/src/client/java/net/legitimoose/bot/discord/command/staff/Rejoin.java new file mode 100644 index 0000000..26db2ac --- /dev/null +++ b/src/client/java/net/legitimoose/bot/discord/command/staff/Rejoin.java @@ -0,0 +1,20 @@ +package net.legitimoose.bot.discord.command.staff; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.legitimoose.bot.LegitimooseBotClient; +import net.legitimoose.bot.discord.command.Command; + +public class Rejoin implements Command { + final SlashCommandInteractionEvent event; + + public Rejoin(SlashCommandInteractionEvent event) { + this.event = event; + } + + @Override + public void onCommandReceived() { + event.deferReply(true).queue(); + event.getHook().sendMessage("Rejoining server...").queue(); + LegitimooseBotClient.rejoin(true); + } +} diff --git a/src/client/java/net/legitimoose/bot/discord/command/staff/Restart.java b/src/client/java/net/legitimoose/bot/discord/command/staff/Restart.java new file mode 100644 index 0000000..e112fa6 --- /dev/null +++ b/src/client/java/net/legitimoose/bot/discord/command/staff/Restart.java @@ -0,0 +1,20 @@ +package net.legitimoose.bot.discord.command.staff; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.legitimoose.bot.discord.command.Command; + +public class Restart implements Command { + final SlashCommandInteractionEvent event; + + public Restart(SlashCommandInteractionEvent event) { + this.event = event; + } + + + @Override + public void onCommandReceived() { + event.deferReply(true).queue(); + event.getHook().sendMessage("Restarting bot...").complete(); + System.exit(0); + } +} diff --git a/src/client/java/net/legitimoose/bot/discord/command/staff/Send.java b/src/client/java/net/legitimoose/bot/discord/command/staff/Send.java new file mode 100644 index 0000000..5c1565c --- /dev/null +++ b/src/client/java/net/legitimoose/bot/discord/command/staff/Send.java @@ -0,0 +1,33 @@ +package net.legitimoose.bot.discord.command.staff; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.legitimoose.bot.LegitimooseBotClient; +import net.legitimoose.bot.discord.command.Command; + +public class Send implements Command { + final SlashCommandInteractionEvent event; + final String message; + + public Send(SlashCommandInteractionEvent event, String message) { + this.event = event; + this.message = message; + } + + @Override + public void onCommandReceived() { + event.deferReply(true).queue(); + if (message.isEmpty()) { + event.getHook().sendMessage("Please provide a message to send.").queue(); + return; + } + LegitimooseBotClient.mc.schedule(() -> { + if (message.startsWith("/")) { + LegitimooseBotClient.mc.getConnection().sendCommand(message.substring(1)); + event.getHook().sendMessage(String.format("Command sent: `%s`", message)).queue(); + return; + } + LegitimooseBotClient.mc.getConnection().sendChat(message); + event.getHook().sendMessage(String.format("Message sent: `%s`", message)).queue(); + }); + } +} diff --git a/src/client/kotlin/net/legitimoose/bot/LegitimooseBotClient.kt b/src/client/kotlin/net/legitimoose/bot/LegitimooseBotClient.kt deleted file mode 100644 index 7240723..0000000 --- a/src/client/kotlin/net/legitimoose/bot/LegitimooseBotClient.kt +++ /dev/null @@ -1,208 +0,0 @@ -package net.legitimoose.bot - -import java.util.Timer -import java.util.concurrent.TimeUnit -import java.util.regex.Pattern -import kotlin.concurrent.schedule -import kotlin.concurrent.thread -import kotlin.concurrent.timer -import kotlin.system.exitProcess -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager -import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents -import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents -import net.fabricmc.loader.api.FabricLoader -import net.legitimoose.bot.LegitimooseBot.config -import net.legitimoose.bot.LegitimooseBot.logger -import net.legitimoose.bot.discord.DiscordBot -import net.legitimoose.bot.discord.DiscordWebhook -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.screens.AccessibilityOnboardingScreen -import net.minecraft.client.gui.screens.ConnectScreen -import net.minecraft.client.gui.screens.DisconnectedScreen -import net.minecraft.client.gui.screens.TitleScreen -import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen -import net.minecraft.client.multiplayer.ServerData -import net.minecraft.client.multiplayer.resolver.ServerAddress -import net.minecraft.network.chat.Component - -object LegitimooseBotClient { - val mc - get() = Minecraft.getInstance() - - private val joinPattern: Pattern = Pattern.compile("""^\[\+]\s*(?:[^|]+\|\s*)?(\S+)""") - private val switchPattern: Pattern = Pattern.compile("""^\[→]\s*(?:[^|]+\|\s*)?(\S+)""") - private val leavePattern: Pattern = Pattern.compile("""^\[-]\s*(?:[^|]+\|\s*)?(\S+)""") - - private val chatPattern: Pattern = Pattern.compile("""^(?:\[SHOUT]\s*)?(?:[^|]+\|\s*)?([^:]+): (.*)""") - private val msgPattern: Pattern = Pattern.compile("""\[(.*) -> me] @(.*) (.*)""") - - @Volatile - private var lastJoinTimestamp: Long = 0L - private const val REJOIN_COOLDOWN_MS = 5_000L - - private val timer = Timer() - - fun init() { - timer.schedule(TimeUnit.HOURS.toMillis(24), TimeUnit.HOURS.toMillis(24)) { exitProcess(67) } - ClientCommandRegistrationCallback.EVENT.register { dispatcher, _ -> - dispatcher.register( - ClientCommandManager.literal("scrape").executes { - thread { Scraper.scrape() } - 1 - }) - } - - thread { DiscordBot.runBot() } - - ClientTickEvents.END_CLIENT_TICK.register { rejoin() } - - thread { - try { - TimeUnit.SECONDS.sleep(10) - } catch (e: InterruptedException) { - logger.warn(e.message) - } - logger.info("Trying to join for the first time") - rejoin() - } - - thread { - try { - TimeUnit.SECONDS.sleep(5) - } catch (e: InterruptedException) { - logger.warn(e.message) - } - while (true) { - try { - Scraper.scrape() - } catch (_: Exception) { - } - try { - TimeUnit.MINUTES.sleep(config.getOrDefault("waitMinutesBetweenScrapes", 10).toLong()) - } catch (e: InterruptedException) { - logger.warn(e.message) - } - } - } - - thread { - try { - TimeUnit.SECONDS.sleep(5) - } catch (e: InterruptedException) { - logger.warn(e.message) - } - while (true) { - try { - mc.player - ?.connection - ?.sendChat("
I am a bot that syncs lobby chat to a community Discord
To prevent messages being sent to discord, prefix your messages with ::
You can check out our work at https://legiti.dev/") - TimeUnit.MINUTES.sleep(20) - } catch (e: InterruptedException) { - logger.warn(e.message) - } - } - } - - ClientReceiveMessageEvents.GAME.register { message: Component, _: Boolean -> - thread { - val msg = message.string - var username = "" - var cleanMessage = msg - - val joinMatcher = joinPattern.matcher(msg) - val switchMatcher = switchPattern.matcher(msg) - val leaveMatcher = leavePattern.matcher(msg) - val chatMatcher = chatPattern.matcher(msg) - val msgMatcher = msgPattern.matcher(msg) - - val webhook = DiscordWebhook(config.getOrDefault("webhookUrl", "")) - if (joinMatcher.find()) { - username = joinMatcher.group(1) - cleanMessage = "**$username** joined the server." - webhook.setEmbedThumbnail("https://mc-heads.net/head/$username/50/left") - webhook.setContent(cleanMessage.replace("@", "")) - webhook.execute(0x57F287) - return@thread - } - else if (switchMatcher.find()) { - username = switchMatcher.group(1) - cleanMessage = "**$username** switched servers." - webhook.setEmbedThumbnail("https://mc-heads.net/head/$username/50/left") - webhook.setContent(cleanMessage.replace("@", "")) - webhook.execute(0xF2F257) - return@thread - } - else if (leaveMatcher.find()) { - username = leaveMatcher.group(1) - cleanMessage = "**$username** left the server." - webhook.setEmbedThumbnail("https://mc-heads.net/head/$username/50/left") - webhook.setContent(cleanMessage.replace("@", "")) - webhook.execute(0xF25757) - return@thread - } - else if (chatMatcher.find()) { - username = chatMatcher.group(1) - cleanMessage = chatMatcher.group(2) - if (msg.startsWith("[SHOUT]")) { - webhook.setUsername("[SHOUT] $username") - } else { - webhook.setUsername(username) - } - webhook.setAvatarUrl("https://mc-heads.net/avatar/$username") - } else if (msgMatcher.find()) { - val username1 = msgMatcher.group(1) - val username2 = msgMatcher.group(2) - val msg1 = msgMatcher.group(3) - val member = - DiscordBot.jda - .getGuildById(1311574348989071440L)!! - .findMembers { s -> s.user.name == username2 } - .get()[0] - member.user - .openPrivateChannel() - .flatMap { channel -> channel.sendMessage("$username1: $msg1") } - .queue() - return@thread - } - - if (username == mc.player?.name?.string) return@thread - - if (username.isNotEmpty() && - !cleanMessage.startsWith(config.getOrDefault("secretPrefix", "::")) - ) { - webhook.setContent(cleanMessage.replace("@", "")) - webhook.execute() - } - } - } - } - - fun rejoin(force: Boolean = false) { - if (FabricLoader.getInstance().isDevelopmentEnvironment) return - val screen = mc.screen - if (screen is DisconnectedScreen || - screen is JoinMultiplayerScreen || - screen is TitleScreen || - screen is AccessibilityOnboardingScreen || - (mc.connection != null && force) - ) { - Minecraft.getInstance().schedule { - val now = System.currentTimeMillis() - if (now - lastJoinTimestamp >= REJOIN_COOLDOWN_MS) { - lastJoinTimestamp = now - logger.info("Attempting to reconnect to server") - val info = ServerData("Server", "legitimoose.com", ServerData.Type.OTHER) - ConnectScreen.startConnecting( - JoinMultiplayerScreen(null), - mc, - ServerAddress.parseString("legitimoose.com"), - info, - false, - null - ) - } - } - } - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/Scraper.kt b/src/client/kotlin/net/legitimoose/bot/Scraper.kt deleted file mode 100644 index 462e22a..0000000 --- a/src/client/kotlin/net/legitimoose/bot/Scraper.kt +++ /dev/null @@ -1,149 +0,0 @@ -package net.legitimoose.bot - -import com.mojang.serialization.JsonOps -import com.mongodb.kotlin.client.MongoClient -import java.util.concurrent.TimeUnit -import net.legitimoose.bot.LegitimooseBot.config -import net.legitimoose.bot.LegitimooseBot.logger -import net.legitimoose.bot.discord.DiscordWebhook -import net.minecraft.client.Minecraft -import net.minecraft.core.component.DataComponents -import net.minecraft.nbt.CompoundTag -import net.minecraft.network.chat.ComponentSerialization -import net.minecraft.world.inventory.ClickType - -object Scraper { - private val mongoClient = MongoClient.create(config.getOrDefault("mongoUri", "")) - private val errorWebhook = DiscordWebhook(config.getOrDefault("errorWebhookUrl", "")) - val db = mongoClient.getDatabase("legitimooseapi") - - private fun waitSeconds(time: Long) { - try { - TimeUnit.SECONDS.sleep(time) - } catch (e: InterruptedException) { - logger.warn("Failed to wait {} seconds:", time) - logger.warn(e.message) - } - } - - private fun error(message: String?, exception: Exception) { - logger.error(message) - logger.error(exception.message) - errorWebhook.setContent("$message\n${exception.message}") - errorWebhook.execute() - } - - fun scrape() { - val client = Minecraft.getInstance() - client.player!!.connection.sendCommand("worlds") - - waitSeconds(1) - val max_pages: Int - try { - max_pages = client.screen!!.title.siblings[0].string.substring(3).toInt() - } catch (e: NumberFormatException) { - error("Cannot start scraping: failed to parse integer amount of worlds!", e) - return - } - logger.info("Last page is: {}", max_pages) - for (i in 1..max_pages) { - val inv = client.player!!.containerMenu.getSlot(0).container - for (j in 0..26) { - if (client.player!!.containerMenu.containerId == 0) - return // should check if player closed the inventory not sure though - val itemStack = inv.getItem(j) - // last page & air: break, last world was already hit. - if (i == max_pages && itemStack.toString().substring(2) == "minecraft:air") break - var customData: CompoundTag - try { - customData = itemStack.get(DataComponents.CUSTOM_DATA)!!.copyTag() - } catch (e: NullPointerException) { - error("could not scrape world $j in page $i", e) - continue - } - val publicBukkitValues = customData.get("PublicBukkitValues") as CompoundTag - val jam_id = if(!publicBukkitValues.get("datapackserverpaper:jam_id")!!.asString().get().isEmpty()) publicBukkitValues.get("datapackserverpaper:jam_id")!!.asString().get().toInt() else null; - val world = - World( - creation_date = - publicBukkitValues.get("datapackserverpaper:creation_date")!!.asString().get(), - creation_date_unix_seconds = - publicBukkitValues - .get("datapackserverpaper:creation_date_unix_seconds")!! - .asString() - .get() - .toInt(), - enforce_whitelist = - publicBukkitValues - .get("datapackserverpaper:enforce_whitelist")!! - .asString() - .get() - .toBoolean(), - locked = - publicBukkitValues - .get("datapackserverpaper:locked")!! - .asString() - .get() - .toBoolean(), - owner_uuid = publicBukkitValues.get("datapackserverpaper:owner")!!.asString().get(), - player_count = - publicBukkitValues - .get("datapackserverpaper:player_count")!! - .asString() - .get() - .toInt(), - resource_pack_url = - publicBukkitValues - .get("datapackserverpaper:resource_pack_url")!! - .asString() - .get(), - world_uuid = publicBukkitValues.get("datapackserverpaper:uuid")!!.asString().get(), - version = publicBukkitValues.get("datapackserverpaper:version")!!.asString().get(), - visits = - publicBukkitValues.get("datapackserverpaper:visits")!!.asString().get().toInt(), - votes = - publicBukkitValues.get("datapackserverpaper:votes")!!.asString().get().toInt(), - whitelist_on_version_change = - publicBukkitValues - .get("datapackserverpaper:whitelist_on_version_change")!! - .asString() - .get() - .toBoolean(), - name = itemStack.get(DataComponents.CUSTOM_NAME)!!.string, - description = itemStack.get(DataComponents.LORE)!!.lines[0].string, - jam_world = - publicBukkitValues.get("datapackserverpaper:jam_world")!!.asString().get().toBoolean(), - jam_id, - raw_name = - ComponentSerialization.CODEC.encodeStart( - JsonOps.INSTANCE, itemStack.get(DataComponents.CUSTOM_NAME)) - .result() - .get() - .toString(), - raw_description = - ComponentSerialization.CODEC.encodeStart( - JsonOps.INSTANCE, itemStack.get(DataComponents.LORE)!!.lines[0]) - .result() - .get() - .toString(), - icon = itemStack.toString().substring(2), - last_scraped = System.currentTimeMillis() / 1000L) - logger.info("Scraped World $j ${world.world_uuid}: ${world.name}") - try { - world.upload(db) - } catch (e: Exception) { - error("could not upload world ${world.world_uuid}: ${world.name} to db") - } - } - // finally, click on next page button - logger.info("Scraped page #$i") - Minecraft.getInstance() - .gameMode!! - .handleInventoryMouseClick( - client.player!!.containerMenu.containerId, 32, 0, ClickType.PICKUP, client.player!!) - waitSeconds(3) // wait three seconds to give legmos time to load - } - client.player!!.closeContainer() - logger.info("Finished Scraping") - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/World.kt b/src/client/kotlin/net/legitimoose/bot/World.kt deleted file mode 100644 index 475553b..0000000 --- a/src/client/kotlin/net/legitimoose/bot/World.kt +++ /dev/null @@ -1,95 +0,0 @@ -package net.legitimoose.bot - -import com.mongodb.MongoClientException -import com.mongodb.client.model.Filters.eq -import com.mongodb.client.model.Filters.lt -import com.mongodb.client.model.UpdateOptions -import com.mongodb.client.model.Updates -import com.mongodb.kotlin.client.MongoCollection -import com.mongodb.kotlin.client.MongoDatabase -import net.legitimoose.bot.LegitimooseBot.logger -import org.bson.Document -import org.bson.types.ObjectId - -data class World( - val creation_date: String, - val creation_date_unix_seconds: Int, - val enforce_whitelist: Boolean, - val locked: Boolean, - val owner_uuid: String, - val player_count: Int, - val resource_pack_url: String, - val world_uuid: String, - val version: String, - val visits: Int, - val votes: Int, - val whitelist_on_version_change: Boolean, - val name: String, - val description: String, - val jam_world: Boolean, - val jam_id: Int?, - val raw_name: String, - val raw_description: String, - val icon: String, - val last_scraped: Long -) { - fun upload(db: MongoDatabase) { - val coll: MongoCollection = db.getCollection("worlds") - var doc: Document? - try { - doc = coll.find(eq("world_uuid", this.world_uuid)).first() - } catch (e: MongoClientException) { - doc = null - } - coll.deleteMany(lt("last_scraped", System.currentTimeMillis() / 1000L - 86400)) - if (doc != null) { - logger.info("updating world") - val updates = - Updates.combine( - Updates.set("enforce_whitelist", this.enforce_whitelist), - Updates.set("locked", this.locked), - Updates.set("owner_uuid", this.owner_uuid), - Updates.set("player_count", this.player_count), - Updates.set("resource_pack_url", this.resource_pack_url), - Updates.set("version", this.version), - Updates.set("visits", this.visits), - Updates.set("votes", this.votes), - Updates.set("whitelist_on_version_change", this.whitelist_on_version_change), - Updates.set("name", this.name), - Updates.set("description", this.description), - Updates.set("jam_world", this.jam_world), - Updates.set("jam_id", this.jam_id), - Updates.set("raw_name", this.raw_name), - Updates.set("raw_description", this.raw_description), - Updates.set("icon", this.icon), - Updates.set("last_scraped", this.last_scraped)) - coll.updateOne(eq("world_uuid", this.world_uuid), updates, UpdateOptions()) - logger.info("Updated world") - return - } - coll.insertOne( - Document() - .append("_id", ObjectId()) - .append("creation_date", this.creation_date) - .append("creation_date_unix_seconds", this.creation_date_unix_seconds) - .append("enforce_whitelist", this.enforce_whitelist) - .append("locked", this.locked) - .append("owner_uuid", this.owner_uuid) - .append("player_count", this.player_count) - .append("resource_pack_url", this.resource_pack_url) - .append("world_uuid", this.world_uuid) - .append("version", this.version) - .append("visits", this.visits) - .append("votes", this.votes) - .append("whitelist_on_version_change", this.whitelist_on_version_change) - .append("name", this.name) - .append("description", this.description) - .append("jam_world", this.jam_world) - .append("jam_id", this.jam_id) - .append("raw_name", this.raw_name) - .append("raw_description", this.raw_description) - .append("icon", this.icon) - .append("last_scraped", this.last_scraped)) - logger.info("Created world") - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/Command.kt b/src/client/kotlin/net/legitimoose/bot/discord/Command.kt deleted file mode 100644 index 32e82e0..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/Command.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.legitimoose.bot.discord - -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent - -interface Command { - val event: SlashCommandInteractionEvent - - fun onCommandReceived() -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/DiscordBot.kt b/src/client/kotlin/net/legitimoose/bot/discord/DiscordBot.kt deleted file mode 100644 index d49f11c..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/DiscordBot.kt +++ /dev/null @@ -1,107 +0,0 @@ -package net.legitimoose.bot.discord - -import net.dv8tion.jda.api.JDA -import net.dv8tion.jda.api.JDABuilder -import net.dv8tion.jda.api.Permission -import net.dv8tion.jda.api.events.guild.GuildReadyEvent -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.dv8tion.jda.api.events.message.MessageReceivedEvent -import net.dv8tion.jda.api.hooks.ListenerAdapter -import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions -import net.dv8tion.jda.api.interactions.commands.OptionType -import net.dv8tion.jda.api.interactions.commands.build.Commands -import net.dv8tion.jda.api.requests.GatewayIntent -import net.legitimoose.bot.LegitimooseBot -import net.legitimoose.bot.LegitimooseBot.config -import net.legitimoose.bot.discord.staff.Rejoin -import net.legitimoose.bot.discord.staff.Restart -import net.legitimoose.bot.discord.staff.Send -import net.minecraft.client.Minecraft - -class DiscordBot : ListenerAdapter() { - companion object { - lateinit var jda: JDA - - fun runBot() { - jda = - JDABuilder.createDefault(config.getOrDefault("discordToken", "")) - .enableIntents(GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_MEMBERS) - .build() - - jda.addEventListener(DiscordBot()) - jda.updateCommands() - .addCommands( - Commands.slash("list", "List online players in the server") - .addOption( - OptionType.BOOLEAN, - "lobby", - "True if you only want to see online players in the lobby"), - Commands.slash("find", "Find which world a player is in") - .addOption( - OptionType.STRING, - "player", - "The username of the player you want to find", - true), - Commands.slash("msg", "Message an ingame player") - .addOption( - OptionType.STRING, - "player", - "The username of the player you want to message", - true) - .addOption(OptionType.STRING, "message", "The message you want to send", true)) - .queue() - } - } - - override fun onGuildReady(event: GuildReadyEvent) { - if (event.guild.id != config.getOrDefault("discordGuildId", "1311574348989071440")) return - event.guild - .updateCommands() - .addCommands( - Commands.slash("rejoin", "Rejoin server") - .setDefaultPermissions( - DefaultMemberPermissions.enabledFor(Permission.MANAGE_SERVER)), - Commands.slash("restart", "Restart bot") - .setDefaultPermissions( - DefaultMemberPermissions.enabledFor(Permission.MANAGE_SERVER)), - Commands.slash("send", "Send message") - .setDefaultPermissions( - DefaultMemberPermissions.enabledFor(Permission.MANAGE_SERVER)) - .addOption(OptionType.STRING, "message", "The message to send", true)) - .queue() - } - - override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) { - when (event.name) { - "list" -> ListCommand(event, event.getOption("lobby")?.asBoolean).onCommandReceived() - "find" -> FindCommand(event, event.getOption("player")!!.asString).onCommandReceived() - "msg" -> - MsgCommand( - event, - event.getOption("message")!!.asString, - event.getOption("player")!!.asString) - .onCommandReceived() - - "rejoin" -> Rejoin(event).onCommandReceived() - "restart" -> Restart(event).onCommandReceived() - "send" -> Send(event, event.getOption("message")!!.asString).onCommandReceived() - } - } - - override fun onMessageReceived(event: MessageReceivedEvent) { - if (event.isWebhookMessage || event.author.isBot) return - val discordNick = event.member!!.effectiveName.replace("§", "?") - var message = - "
ᴅɪsᴄᴏʀᴅ $discordNick: " + - event.message.contentStripped.replace("\n", "
").replace("§", "?") - if (event.message.attachments.size != 0) { - message += " [Attachment Included]" - } - if (message.length >= 200) return - if (config.getOrDefault("channelId", "").isEmpty()) - LegitimooseBot.logger.error("Discord channel ID is not set in config!") - if (event.channel.id == config.getOrDefault("channelId", "")) { - Minecraft.getInstance().player?.connection?.sendChat(message) - } - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/FindCommand.kt b/src/client/kotlin/net/legitimoose/bot/discord/FindCommand.kt deleted file mode 100644 index d3b131d..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/FindCommand.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.legitimoose.bot.discord - -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents -import net.minecraft.client.Minecraft -import net.minecraft.network.chat.Component - -class FindCommand(override val event: SlashCommandInteractionEvent, val player: String) : Command { - override fun onCommandReceived() { - if (player.length >= 200) { - event.reply("player name too long, sorry!").setEphemeral(true).queue() - return - } - Minecraft.getInstance().player?.connection?.sendCommand("find " + player.replace("§", "?")) - val bool: BooleanArray = booleanArrayOf(true) - ClientReceiveMessageEvents.GAME.register { message: Component, _: Boolean -> - if (!bool[0]) return@register - event.reply(message.string.replace(" Click HERE to join.", "").trim()).queue() - bool[0] = false - } - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/ListCommand.kt b/src/client/kotlin/net/legitimoose/bot/discord/ListCommand.kt deleted file mode 100644 index bee8df0..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/ListCommand.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.legitimoose.bot.discord - -import com.mojang.brigadier.context.CommandContextBuilder -import com.mongodb.kotlin.client.MongoCollection -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.legitimoose.bot.LegitimooseBot.logger -import net.legitimoose.bot.Scraper -import net.minecraft.client.Minecraft -import net.minecraft.client.multiplayer.PlayerInfo -import org.bson.Document -import org.bson.types.ObjectId - -class ListCommand(override val event: SlashCommandInteractionEvent, val lobby: Boolean?) : Command { - override fun onCommandReceived() { - if (lobby != null && lobby) { - val playerList: MutableCollection? = - Minecraft.getInstance().connection?.onlinePlayers - val players = StringBuilder() - for (player in playerList!!) { - players.append(player.tabListDisplayName!!.string).append('\n') - } - event.reply(players.toString()).queue() - } else { - val coll: MongoCollection = Scraper.db.getCollection("stats") - - event - .deferReply() - .queue() // It *does* send a packet to the mc server, so keeping this is safer... - - // Please ignore the nulls. Only the 'input' is actually used - val context = CommandContextBuilder(null, null, null, 1).build("/find ") - - val pendingParse = - Minecraft.getInstance() - .player - ?.connection - ?.getSuggestionsProvider() - ?.customSuggestion(context) ?: return - - pendingParse.thenRun { - if (!pendingParse.isDone) { - logger.warn("Pending parse is not done! (somehow??)") - return@thenRun - } - val mcSuggestions = pendingParse.join().list - val suggestions = StringBuilder() - for (suggestion in mcSuggestions) { - suggestions.append(suggestion.text + '\n') - } - event.hook - .sendMessage( - "There are ${mcSuggestions.size} player(s) online:\n```\n${suggestions.toString()}```") - .queue() - - coll.insertOne( - Document().append("_id", ObjectId()).append("player_count", mcSuggestions.size)) - } - } - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/MsgCommand.kt b/src/client/kotlin/net/legitimoose/bot/discord/MsgCommand.kt deleted file mode 100644 index fb11cc4..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/MsgCommand.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.legitimoose.bot.discord - -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.minecraft.client.Minecraft - -class MsgCommand( - override val event: SlashCommandInteractionEvent, - val message: String, - val player: String -) : Command { - override fun onCommandReceived() { - if ((message.length + player.length) >= 200) { - event.reply("Failed to send, message and/or player name too long!").setEphemeral(true).queue() - return - } - Minecraft.getInstance() - .player - ?.connection - ?.sendCommand( - "msg " + - player.replace("§", "?") + - " [ᴅɪsᴄᴏʀᴅ] " + - event.member!!.effectiveName + - ": " + - message.replace("\n", "
").replace("§", "?")) - event.reply("Sent `" + message.trim() + "` to " + player).setEphemeral(true).queue() - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/ShoutCommand.kt.disabled b/src/client/kotlin/net/legitimoose/bot/discord/ShoutCommand.kt.disabled deleted file mode 100644 index 56ccd39..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/ShoutCommand.kt.disabled +++ /dev/null @@ -1,22 +0,0 @@ -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.minecraft.client.Minecraft - -class ShoutCommand(override val event: SlashCommandInteractionEvent, val message: String) : - Command { - override fun onCommandReceived() { - if (message.length >= 200) { - event.reply("Failed to send, message too long!").setEphemeral(true).queue() - return - } - Minecraft.getInstance() - .player - ?.connection - ?.sendCommand( - "shout [ᴅɪsᴄᴏʀᴅ] " + - event.member!!.effectiveName + - ": " + - message.replace("\n", "
").replace("§", "?") - ) - event.reply("Successfully shouted `${message.trim()}`").setEphemeral(true).queue() - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/staff/Rejoin.kt b/src/client/kotlin/net/legitimoose/bot/discord/staff/Rejoin.kt deleted file mode 100644 index 6929fc4..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/staff/Rejoin.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.legitimoose.bot.discord.staff - -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.legitimoose.bot.LegitimooseBotClient -import net.legitimoose.bot.discord.Command - -class Rejoin(override val event: SlashCommandInteractionEvent) : Command { - override fun onCommandReceived() { - event.deferReply(true).queue() - event.hook.sendMessage("Rejoining server...").queue() - LegitimooseBotClient.rejoin(force = true) - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/staff/Restart.kt b/src/client/kotlin/net/legitimoose/bot/discord/staff/Restart.kt deleted file mode 100644 index 5e22799..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/staff/Restart.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.legitimoose.bot.discord.staff - -import kotlin.system.exitProcess -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.legitimoose.bot.discord.Command - -class Restart(override val event: SlashCommandInteractionEvent) : Command { - override fun onCommandReceived() { - event.deferReply(true).queue() - event.hook.sendMessage("Restarting bot...").complete() - exitProcess(0) - } -} diff --git a/src/client/kotlin/net/legitimoose/bot/discord/staff/Send.kt b/src/client/kotlin/net/legitimoose/bot/discord/staff/Send.kt deleted file mode 100644 index 3d2cd49..0000000 --- a/src/client/kotlin/net/legitimoose/bot/discord/staff/Send.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.legitimoose.bot.discord.staff - -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.legitimoose.bot.LegitimooseBotClient -import net.legitimoose.bot.discord.Command - -class Send(override val event: SlashCommandInteractionEvent, val message: String) : Command { - override fun onCommandReceived() { - event.deferReply(true).queue() - if (message.isEmpty()) { - event.hook.sendMessage("Please provide a message to send.").queue() - return - } - LegitimooseBotClient.mc.schedule { - if (message.startsWith("/")) { - LegitimooseBotClient.mc.connection?.sendCommand(message.substring(1)) - event.hook.sendMessage("Command sent: `$message`").queue() - return@schedule - } - LegitimooseBotClient.mc.connection?.sendChat(message) - event.hook.sendMessage("Message sent: `$message`").queue() - } - } -} diff --git a/src/main/java/net/legitimoose/bot/LegitimooseBot.java b/src/main/java/net/legitimoose/bot/LegitimooseBot.java new file mode 100644 index 0000000..56d8218 --- /dev/null +++ b/src/main/java/net/legitimoose/bot/LegitimooseBot.java @@ -0,0 +1,16 @@ +package net.legitimoose.bot; + +import net.fabricmc.api.ModInitializer; +import net.legitimoose.bot.config.SimpleConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LegitimooseBot implements ModInitializer { + public static final Logger LOGGER = LoggerFactory.getLogger("legitimoose-bot"); + public static final SimpleConfig CONFIG = SimpleConfig.of("legitimoosebot-config").request(); + + @Override + public void onInitialize() { + + } +} diff --git a/src/main/kotlin/net/legitimoose/bot/LegitimooseBot.kt b/src/main/kotlin/net/legitimoose/bot/LegitimooseBot.kt deleted file mode 100644 index 808b184..0000000 --- a/src/main/kotlin/net/legitimoose/bot/LegitimooseBot.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.legitimoose.bot - -import net.legitimoose.bot.config.SimpleConfig -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -object LegitimooseBot { - private const val MOD_ID = "legitimoose-bot" - val logger: Logger = LoggerFactory.getLogger(MOD_ID) - val config: SimpleConfig = SimpleConfig.of("legitimoosebot-config").request() - - fun init() {} -} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 149614d..ee3faf9 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -10,16 +10,10 @@ "environment": "client", "entrypoints": { "client": [ - { - "adapter": "kotlin", - "value": "net.legitimoose.bot.LegitimooseBotClient::init" - } + "net.legitimoose.bot.LegitimooseBotClient" ], "main": [ - { - "adapter": "kotlin", - "value": "net.legitimoose.bot.LegitimooseBot::init" - } + "net.legitimoose.bot.LegitimooseBot" ] }, "mixins": [ From 9a63bf976b39bc4772d9eda4e0c78ea06a69998d Mon Sep 17 00:00:00 2001 From: Omri H Date: Sun, 18 Jan 2026 19:38:14 +0200 Subject: [PATCH 2/4] minor changes and cleanup --- .../legitimoose/bot/LegitimooseBotClient.java | 168 +++++++++--------- 1 file changed, 82 insertions(+), 86 deletions(-) diff --git a/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java b/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java index c05e86b..302bda6 100644 --- a/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java +++ b/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java @@ -1,11 +1,9 @@ package net.legitimoose.bot; -import com.mojang.brigadier.CommandDispatcher; import net.dv8tion.jda.api.entities.Member; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.loader.api.FabricLoader; @@ -16,7 +14,6 @@ import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.resolver.ServerAddress; -import net.minecraft.commands.CommandBuildContext; import java.io.IOException; import java.net.URISyntaxException; @@ -41,7 +38,7 @@ public class LegitimooseBotClient implements ClientModInitializer { private final Pattern msgPattern = Pattern.compile("\\[(.*) -> me] @(.*) (.*)"); static volatile private long lastJoinTimestamp = 0L; - private static long REJOIN_COOLDOWN_MS = 5_000L; + private static final long REJOIN_COOLDOWN_MS = 5_000L; Timer timer = new Timer(); @@ -108,91 +105,90 @@ public void run() { } }).start(); - ClientReceiveMessageEvents.GAME.register(((message, overlay) -> { - new Thread(() -> { - String msg = message.getString(); - String username = ""; - String cleanMessage = msg; - - Matcher joinMatcher = joinPattern.matcher(msg); - Matcher switchMatcher = switchPattern.matcher(msg); - Matcher leaveMatcher = leavePattern.matcher(msg); - Matcher chatMatcher = chatPattern.matcher(msg); - Matcher msgMatcher = msgPattern.matcher(msg); - - DiscordWebhook webhook = new DiscordWebhook(CONFIG.getOrDefault("webhookUrl", "")); - if (joinMatcher.find()) { - username = joinMatcher.group(1); - cleanMessage = String.format("**%s** joined the server.", username); - webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); - webhook.setContent(cleanMessage.replace("@", "")); - try { - webhook.execute(0x57F287); - } catch (IOException | URISyntaxException e) { - LOGGER.warn(e.getMessage()); + ClientReceiveMessageEvents.GAME.register(((message, overlay) -> + new Thread(() -> { + String msg = message.getString(); + String username = ""; + String cleanMessage = msg; + + Matcher joinMatcher = joinPattern.matcher(msg); + Matcher switchMatcher = switchPattern.matcher(msg); + Matcher leaveMatcher = leavePattern.matcher(msg); + Matcher chatMatcher = chatPattern.matcher(msg); + Matcher msgMatcher = msgPattern.matcher(msg); + + DiscordWebhook webhook = new DiscordWebhook(CONFIG.getOrDefault("webhookUrl", "")); + if (joinMatcher.find()) { + username = joinMatcher.group(1); + cleanMessage = String.format("**%s** joined the server.", username); + webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); + webhook.setContent(cleanMessage.replace("@", "")); + try { + webhook.execute(0x57F287); + } catch (IOException | URISyntaxException e) { + LOGGER.warn(e.getMessage()); + } + return; + } else if (switchMatcher.find()) { + username = switchMatcher.group(1); + cleanMessage = String.format("**%s** switched servers.", username); + webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); + webhook.setContent(cleanMessage.replace("@", "")); + try { + webhook.execute(0xF2F257); + } catch (IOException | URISyntaxException e) { + LOGGER.warn(e.getMessage()); + } + return; + } else if (leaveMatcher.find()) { + username = leaveMatcher.group(1); + cleanMessage = String.format("**%s** left the server.", username); + webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); + webhook.setContent(cleanMessage.replace("@", "")); + try { + webhook.execute(0xF25757); + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + return; + } else if (chatMatcher.find()) { + username = chatMatcher.group(1); + cleanMessage = chatMatcher.group(2); + if (msg.startsWith("[SHOUT]")) { + webhook.setUsername("[SHOUT] $username"); + } else { + webhook.setUsername(username); + } + webhook.setAvatarUrl(String.format("https://mc-heads.net/avatar/%s", username)); + } else if (msgMatcher.find()) { + String username1 = msgMatcher.group(1); + String username2 = msgMatcher.group(2); + String msg1 = msgMatcher.group(3); + Member member = + DiscordBot.jda + .getGuildById(CONFIG.getOrDefault("discordGuildId", "1311574348989071440")) + .findMembers(s -> s.getUser().getName().equals(username2)) + .get().getFirst(); + member.getUser() + .openPrivateChannel() + .flatMap(channel -> channel.sendMessage(String.format("%s: %s", username1, msg1))) + .queue(); + return; } - return; - } else if (switchMatcher.find()) { - username = switchMatcher.group(1); - cleanMessage = String.format("**%s** switched servers.", username); - webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); - webhook.setContent(cleanMessage.replace("@", "")); - try { - webhook.execute(0xF2F257); - } catch (IOException | URISyntaxException e) { - LOGGER.warn(e.getMessage()); - } - return; - } else if (leaveMatcher.find()) { - username = leaveMatcher.group(1); - cleanMessage = String.format("**%s** left the server.", username); - webhook.setEmbedThumbnail(String.format("https://mc-heads.net/head/%s/50/left", username)); - webhook.setContent(cleanMessage.replace("@", "")); - try { - webhook.execute(0xF25757); - } catch (IOException | URISyntaxException e) { - throw new RuntimeException(e); - } - return; - } else if (chatMatcher.find()) { - username = chatMatcher.group(1); - cleanMessage = chatMatcher.group(2); - if (msg.startsWith("[SHOUT]")) { - webhook.setUsername("[SHOUT] $username"); - } else { - webhook.setUsername(username); - } - webhook.setAvatarUrl(String.format("https://mc-heads.net/avatar/%s", username)); - } else if (msgMatcher.find()) { - String username1 = msgMatcher.group(1); - String username2 = msgMatcher.group(2); - String msg1 = msgMatcher.group(3); - Member member = - DiscordBot.jda - .getGuildById(1311574348989071440L) - .findMembers(s -> s.getUser().getName().equals(username2)) - .get().get(0); - member.getUser() - .openPrivateChannel() - .flatMap(channel -> channel.sendMessage(String.format("%s: %s", username1, msg1))) - .queue(); - return; - } - - if (username == mc.player.getName().getString()) return; - if (!username.isEmpty() && - !cleanMessage.startsWith(CONFIG.getOrDefault("secretPrefix", "::")) - ) { - webhook.setContent(cleanMessage.replace("@", "")); - try { - webhook.execute(); - } catch (IOException | URISyntaxException e) { - LOGGER.warn(e.getMessage()); + if (username.equals(mc.player.getName().getString())) return; + + if (!username.isEmpty() && + !cleanMessage.startsWith(CONFIG.getOrDefault("secretPrefix", "::")) + ) { + webhook.setContent(cleanMessage.replace("@", "")); + try { + webhook.execute(); + } catch (IOException | URISyntaxException e) { + LOGGER.warn(e.getMessage()); + } } - } - }).start(); - })); + }).start())); } public static void rejoin(boolean force) { From c9a48bf2b2efa19e1d27e97d85bc0cb7dc65f220 Mon Sep 17 00:00:00 2001 From: Omri H Date: Sun, 18 Jan 2026 20:02:10 +0200 Subject: [PATCH 3/4] more fixes --- build.gradle.kts | 24 ++++--------------- .../legitimoosebot-config.properties.example | 4 +++- .../legitimoose/bot/LegitimooseBotClient.java | 2 +- .../java/net/legitimoose/bot/Scraper.java | 14 +++++++---- .../java/net/legitimoose/bot/World.java | 2 +- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 76d8499..656a81b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("fabric-loom") version "1.11.5" - id("com.gradleup.shadow") version "9.0.2" } version = project.property("mod_version") as String @@ -23,6 +22,7 @@ loom { repositories { mavenCentral() maven("https://maven.isxander.dev/releases") + maven("https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1") } dependencies { @@ -33,8 +33,10 @@ dependencies { modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}") - shadow(implementation("org.mongodb:mongodb-driver-sync:5.5.1")!!) - shadow(implementation("net.dv8tion:JDA:5.6.1") { exclude("opus-java") }) + include(implementation("org.mongodb:mongodb-driver-sync:5.5.1")!!) + include(implementation("net.dv8tion:JDA:5.6.1") { exclude("opus-java") }) + + modRuntimeOnly("me.djtheredstoner:DevAuth-fabric:1.2.2") } tasks.processResources { @@ -54,20 +56,4 @@ tasks.processResources { tasks.withType { options.encoding = "UTF-8" } -tasks { - shadowJar { - from(sourceSets["main"].output) - from(sourceSets["client"].output) - configurations = listOf(project.configurations.shadow.get()) - archiveClassifier = "shadowed-only" - // minimize() - } - remapJar { - dependsOn(shadowJar) - mustRunAfter(shadowJar) - inputFile = file(shadowJar.get().archiveFile) - archiveClassifier = "" - } -} - java { toolchain.languageVersion = JavaLanguageVersion.of(21) } diff --git a/run/config/legitimoosebot-config.properties.example b/run/config/legitimoosebot-config.properties.example index ab87dc5..46e5099 100644 --- a/run/config/legitimoosebot-config.properties.example +++ b/run/config/legitimoosebot-config.properties.example @@ -3,4 +3,6 @@ webhookUrl= errorWebhookUrl= mongoUri=mongodb://127.0.0.1:27017/ secretPrefix=:: -channelId= \ No newline at end of file +channelId= +discordGuildId= +waitMinutesBetweenScrapes= \ No newline at end of file diff --git a/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java b/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java index 302bda6..3cd7022 100644 --- a/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java +++ b/src/client/java/net/legitimoose/bot/LegitimooseBotClient.java @@ -79,7 +79,7 @@ public void run() { while (true) { scraper.scrape(); try { - TimeUnit.MINUTES.sleep((long) CONFIG.getOrDefault("waitMinutesBetweenScrapes", 10)); + TimeUnit.MINUTES.sleep((long) CONFIG.getOrDefault("waitMinutesBetweenScrapes", 20)); } catch (InterruptedException e) { LOGGER.warn(e.getMessage()); } diff --git a/src/client/java/net/legitimoose/bot/Scraper.java b/src/client/java/net/legitimoose/bot/Scraper.java index 9047f2c..9d831d2 100644 --- a/src/client/java/net/legitimoose/bot/Scraper.java +++ b/src/client/java/net/legitimoose/bot/Scraper.java @@ -38,7 +38,7 @@ private void waitSeconds(long time) { private void error(String message, Exception exception) throws IOException, URISyntaxException { LOGGER.error(message); LOGGER.error(exception.getMessage()); - errorWebhook.setContent("$message\n${exception.message}"); + errorWebhook.setContent(String.format("%s\n%s", message, exception.getMessage())); errorWebhook.execute(); } @@ -59,7 +59,13 @@ public void scrape() { if (i == max_pages && itemStack.toString().substring(2) == "minecraft:air") break; CompoundTag customData = itemStack.get(DataComponents.CUSTOM_DATA).copyTag(); CompoundTag publicBukkitValues = (CompoundTag) customData.get("PublicBukkitValues"); - int jam_id = (!publicBukkitValues.get("datapackserverpaper:jam_id").asString().get().isEmpty()) ? Integer.parseInt(publicBukkitValues.get("datapackserverpaper:jam_id").asString().get()) : null; + Integer jam_id; + if (!publicBukkitValues.get("datapackserverpaper:jam_id").asString().get().isEmpty()) { + jam_id = Integer.parseInt(publicBukkitValues.get("datapackserverpaper:jam_id").asString().get()); + } else { + jam_id = null; + } + //int jam_id = !publicBukkitValues.get("datapackserverpaper:jam_id").asString().get().isEmpty() ? Integer.parseInt(publicBukkitValues.get("datapackserverpaper:jam_id").asString().get()) : null; World world = new World( publicBukkitValues.get("datapackserverpaper:creation_date").asString().get(), @@ -107,11 +113,11 @@ public void scrape() { itemStack.toString().substring(2), System.currentTimeMillis() / 1000L ); - LOGGER.info("Scraped World $j ${world.world_uuid}: ${world.name}"); + LOGGER.info("Scraped World {} {}: {}", j, world.world_uuid(), world.name()); world.upload(db); } // finally, click on next page button - LOGGER.info("Scraped page #$i"); + LOGGER.info("Scraped page #{}", i); Minecraft.getInstance() .gameMode .handleInventoryMouseClick( diff --git a/src/client/java/net/legitimoose/bot/World.java b/src/client/java/net/legitimoose/bot/World.java index 28a549a..f4fbb45 100644 --- a/src/client/java/net/legitimoose/bot/World.java +++ b/src/client/java/net/legitimoose/bot/World.java @@ -30,7 +30,7 @@ public record World( String name, String description, boolean jam_world, - int jam_id, + Integer jam_id, String raw_name, String raw_description, String icon, From a1d4421084b3f52e26ff642b4ca4081b7d379282 Mon Sep 17 00:00:00 2001 From: Omri H Date: Sun, 18 Jan 2026 20:05:28 +0200 Subject: [PATCH 4/4] enable configuration cache for faster builds --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index abc8146..bb54192 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -# Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx1G +org.gradle.configuration-cache=true # Fabric Properties # check these on https://modmuss50.me/fabric.html minecraft_version=1.21.10