package dev.plutorocks; import net.minecraft.client.MinecraftClient; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; public class IrcClient { private final String host; private final int port; private final String channel; private final String nick; private volatile boolean running = false; private volatile boolean connected = false; private volatile boolean registered = false; private Socket socket; private BufferedWriter writer; public IrcClient(String host, int port, String channel, String nick) { this.host = host; this.port = port; this.channel = channel.startsWith("#") ? channel : "#" + channel; this.nick = nick; } /** * start the IRC thread. */ public void connect() { if (running) return; running = true; MinecraftIRC.LOGGER.info("[IRC] Connecting to {}:{} as {} in {}", host, port, nick, channel); Thread thread = new Thread(this::runLoop, "MinecraftIRC-Thread"); thread.setDaemon(true); thread.start(); } public boolean isConnected() { return connected; } public void disconnect() { running = false; MinecraftIRC.LOGGER.info("[IRC] Disconnect requested"); try { if (socket != null && !socket.isClosed()) { socket.close(); } } catch (IOException e) { MinecraftIRC.LOGGER.warn("[IRC] Error closing socket", e); } connected = false; } private void runLoop() { MinecraftIRC.LOGGER.info("[IRC] runLoop started"); registered = false; try (Socket sock = new Socket(host, port); BufferedReader reader = new BufferedReader( new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8)); BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8))) { this.socket = sock; this.writer = writer; this.connected = true; MinecraftIRC.LOGGER.info("[IRC] Connected TCP to {}:{}", host, port); sendRaw("NICK " + nick); sendRaw("USER " + nick + " 0 * :" + nick); String line; while (running && (line = reader.readLine()) != null) { MinecraftIRC.LOGGER.info("[IRC RAW] {}", line); handleLine(line); } if (running) { MinecraftIRC.LOGGER.info("[IRC] Server closed connection while running"); sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Disconnected from server. Use /irc connect to reconnect.") .formatted(Formatting.GRAY)) ); } else { MinecraftIRC.LOGGER.info("[IRC] runLoop exiting normally (running == false)"); } } catch (IOException e) { MinecraftIRC.LOGGER.warn("[IRC] IOException in runLoop", e); sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Connection error: " + e.getMessage() + " (use /irc connect to try again)") .formatted(Formatting.GRAY)) ); } finally { connected = false; running = false; registered = false; this.writer = null; this.socket = null; MinecraftIRC.LOGGER.info("[IRC] runLoop finished (connected=false, running=false)"); } } private void handleLine(String line) { if (line.startsWith("ERROR")) { sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Server ERROR: " + line) .formatted(Formatting.GRAY)) ); return; } if (line.startsWith(":")) { String[] parts = line.split(" "); if (parts.length >= 2) { String code = parts[1]; if ("001".equals(code)) { registered = true; MinecraftIRC.LOGGER.info("[IRC] Registration complete (001), joining {}", channel); sendRaw("JOIN " + channel); sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Connected to " + host + " " + channel + " as " + nick) .formatted(Formatting.GRAY)) ); return; } if ("432".equals(code)) { sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Nick " + nick + " is invalid on this server (e.g. too long).") .formatted(Formatting.GRAY)) ); MinecraftIRC.LOGGER.warn("[IRC] Nick {} rejected with 432", nick); running = false; return; } if ("433".equals(code)) { sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Nick " + nick + " is already in use on this server.") .formatted(Formatting.GRAY)) ); MinecraftIRC.LOGGER.warn("[IRC] Nick {} already in use (433)", nick); running = false; return; } if ("451".equals(code)) { sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Server says connection is not registered (451).") .formatted(Formatting.GRAY)) ); MinecraftIRC.LOGGER.warn("[IRC] Received 451 (connection not registered)"); return; } } } if (line.startsWith("PING")) { String payload = line.length() > 5 ? line.substring(5) : ""; sendRaw("PONG " + payload); return; } if (!line.contains(" PRIVMSG ")) { return; } int prefixEnd = line.indexOf(' '); if (!line.startsWith(":") || prefixEnd <= 1) { return; } String prefix = line.substring(1, prefixEnd); String nick = prefix; int bang = prefix.indexOf('!'); if (bang != -1) { nick = prefix.substring(0, bang); } String[] split = line.split(" :", 2); if (split.length < 2) { return; } String trailing = split[1]; String commandPart = split[0]; String[] cmdParts = commandPart.split(" "); if (cmdParts.length < 3) { return; } String target = cmdParts[2]; if (!target.equalsIgnoreCase(this.channel) && !target.equalsIgnoreCase(this.nick)) { return; } MutableText prefixText = Text.literal("[IRC] ") .formatted(Formatting.AQUA); MutableText nickText = Text.literal("<" + nick + "> ") .formatted(Formatting.WHITE); MutableText msgText = Text.literal(trailing) .formatted(Formatting.WHITE); sendClientChat(prefixText.append(nickText).append(msgText)); } /** * sends a message to the configured channel */ public void sendChannelMessage(String message) { if (!connected) { MinecraftIRC.LOGGER.info("[IRC] Ignoring sendChannelMessage while not connected: {}", message); return; } if (!registered) { MinecraftIRC.LOGGER.info("[IRC] Ignoring sendChannelMessage while not registered: {}", message); sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Cannot send message yet; IRC connection is not fully registered.") .formatted(Formatting.GRAY)) ); return; } sendRaw("PRIVMSG " + channel + " :" + message); } /** * low-level raw IRC send */ private synchronized void sendRaw(String line) { if (writer == null) { MinecraftIRC.LOGGER.info("[IRC] sendRaw called but writer is null: {}", line); return; } try { MinecraftIRC.LOGGER.debug("[IRC SEND] {}", line); writer.write(line); writer.write("\r\n"); writer.flush(); } catch (IOException e) { MinecraftIRC.LOGGER.warn("[IRC] IOException in sendRaw", e); sendClientChat( Text.literal("[IRC] ").formatted(Formatting.AQUA) .append(Text.literal("Send error: " + e.getMessage()) .formatted(Formatting.GRAY)) ); disconnect(); } } private void sendClientChat(Text text) { MinecraftClient client = MinecraftClient.getInstance(); if (client == null) return; client.execute(() -> { if (client.inGameHud != null) { client.inGameHud.getChatHud().addMessage(text); } }); } }