From 5cac01b1523e3f0b533030229054c118355941cf Mon Sep 17 00:00:00 2001 From: ChomeNS <95471003+ChomeNS@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:24:51 +0700 Subject: [PATCH] support for IRC it took uh mabe 4 hours (wednesday 18:00 - 21:00 and thursday (today) 8:00) no OP check yet but may add because commands need them --- .../chipmunk/chayapak/chomens_bot/Bot.java | 1 + .../chayapak/chomens_bot/Configuration.java | 14 ++ .../command/IRCCommandContext.java | 48 +++++ .../chayapak/chomens_bot/data/IRCMessage.java | 10 + .../chomens_bot/irc/IRCMessageLoop.java | 128 +++++++++++ .../chomens_bot/irc/MessageBuffer.java | 29 +++ .../chomens_bot/irc/MessageParser.java | 70 ++++++ .../chomens_bot/plugins/ConsolePlugin.java | 5 +- .../chomens_bot/plugins/DiscordPlugin.java | 2 +- .../chomens_bot/plugins/IRCPlugin.java | 204 ++++++++++++++++++ src/main/resources/default-config.yml | 13 ++ 11 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 src/main/java/land/chipmunk/chayapak/chomens_bot/command/IRCCommandContext.java create mode 100644 src/main/java/land/chipmunk/chayapak/chomens_bot/data/IRCMessage.java create mode 100644 src/main/java/land/chipmunk/chayapak/chomens_bot/irc/IRCMessageLoop.java create mode 100644 src/main/java/land/chipmunk/chayapak/chomens_bot/irc/MessageBuffer.java create mode 100644 src/main/java/land/chipmunk/chayapak/chomens_bot/irc/MessageParser.java create mode 100644 src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/IRCPlugin.java diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/Bot.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/Bot.java index 1756bc7..a0cca3b 100644 --- a/src/main/java/land/chipmunk/chayapak/chomens_bot/Bot.java +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/Bot.java @@ -48,6 +48,7 @@ public class Bot { public ConsolePlugin console; public LoggerPlugin logger; // in ConsolePlugin public DiscordPlugin discord; // same for this one too + public IRCPlugin irc; // AND same for this one too public TickPlugin tick; public ChatPlugin chat; diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/Configuration.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/Configuration.java index f90937d..0fdd647 100644 --- a/src/main/java/land/chipmunk/chayapak/chomens_bot/Configuration.java +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/Configuration.java @@ -23,6 +23,7 @@ public class Configuration { public Core core = new Core(); public Discord discord = new Discord(); + public IRC irc = new IRC(); public Music music = new Music(); public ColorPalette colorPalette = new ColorPalette(); @@ -100,6 +101,19 @@ public class Configuration { public String inviteLink = "https://discord.gg/xdgCkUyaA4"; } + public static class IRC { + public boolean enabled = true; + public String prefix = "!"; + public String host; + public int port; + public String nickname = "chomens-bot"; + public String username = "chomens-bot"; + public String realName = "chomens-bot"; + public String hostName = "null"; + public String serverName = "null"; + public Map servers = new HashMap<>(); + } + public static class Music { public URLRatelimit urlRatelimit = new URLRatelimit(); diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/command/IRCCommandContext.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/command/IRCCommandContext.java new file mode 100644 index 0000000..4a7c0f1 --- /dev/null +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/command/IRCCommandContext.java @@ -0,0 +1,48 @@ +package land.chipmunk.chayapak.chomens_bot.command; + +import com.github.steveice10.mc.auth.data.GameProfile; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import land.chipmunk.chayapak.chomens_bot.Bot; +import land.chipmunk.chayapak.chomens_bot.data.PlayerEntry; +import land.chipmunk.chayapak.chomens_bot.util.ComponentUtilities; +import net.kyori.adventure.text.Component; + +import java.util.UUID; + +public class IRCCommandContext extends CommandContext { + private final Bot bot; + private final String nickName; + + public IRCCommandContext (Bot bot, String prefix, String nickName) { + super( + bot, + prefix, + new PlayerEntry( + new GameProfile( + UUID.nameUUIDFromBytes(("OfflinePlayer:" + nickName).getBytes()), + nickName + ), + GameMode.SURVIVAL, + -69420, + Component.text(nickName), + 0L, + null, + new byte[0], + true + ), + false + ); + this.bot = bot; + this.nickName = nickName; + } + + @Override + public void sendOutput (Component component) { + bot.irc.sendMessage(bot, ComponentUtilities.stringify(component)); + } + + @Override + public Component displayName () { + return Component.text(nickName); + } +} diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/data/IRCMessage.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/data/IRCMessage.java new file mode 100644 index 0000000..c14043a --- /dev/null +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/data/IRCMessage.java @@ -0,0 +1,10 @@ +package land.chipmunk.chayapak.chomens_bot.data; + +// https://gist.github.com/kaecy/286f8ad334aec3fcb588516feb727772#file-message-java +public class IRCMessage { + public String origin; + public String nickName; + public String command; + public String channel; + public String content; +} diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/IRCMessageLoop.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/IRCMessageLoop.java new file mode 100644 index 0000000..ec4a4b6 --- /dev/null +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/IRCMessageLoop.java @@ -0,0 +1,128 @@ +package land.chipmunk.chayapak.chomens_bot.irc; + +import land.chipmunk.chayapak.chomens_bot.data.IRCMessage; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// https://gist.github.com/kaecy/286f8ad334aec3fcb588516feb727772#file-ircmessageloop-java +public abstract class IRCMessageLoop implements Runnable { + private Socket socket; + private OutputStream out; + + public List channelList; + + protected boolean initialSetupStatus = false; + + public IRCMessageLoop (String host, int port) { + channelList = new ArrayList<>(); + try { + socket = new Socket(host, port); + out = socket.getOutputStream(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void send(String text) { + byte[] bytes = (text + "\r\n").getBytes(); + + try { + out.write(bytes); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void nick(String nickname) { + final String msg = "NICK " + nickname; + send(msg); + } + + public void user(String username, String hostname, String serverName, String realName) { + final String msg = "USER " + username + " " + hostname + " " + serverName + " :" + realName; + send(msg); + } + + public void join(String channel) { + if (!initialSetupStatus) { + channelList.add(channel); + return; + } + + final String msg = "JOIN " + channel; + send(msg); + } + + public void part(String channel) { + final String msg = "PART " + channel; + send(msg); + } + + public void channelMessage (String channel, String message) { + final String msg = "PRIVMSG " + channel + " :" + message; + send(msg); + } + + public void pong (String server) { + final String msg = "PONG " + server; + send(msg); + } + + public void quit (String reason) { + final String msg = "QUIT :Quit: " + reason; + send(msg); + } + + protected abstract void onMessage (IRCMessage message); + + private void initialSetup() { + initialSetupStatus = true; + + // now join the channels. you need to wait for message 001 before you join a channel. + for (String channel: channelList) { + join(channel); + } + } + + private void processMessage(String ircMessage) { + final IRCMessage message = MessageParser.message(ircMessage); + + switch (message.command) { + case "privmsg" -> onMessage(message); + case "001" -> initialSetup(); + case "ping" -> pong(message.content); + } + } + + public void run() { + final InputStream stream; + + try { + stream = socket.getInputStream(); + final MessageBuffer messageBuffer = new MessageBuffer(); + final byte[] buffer = new byte[512]; + + int count; + + while (true) { + count = stream.read(buffer); + if (count == -1) break; + messageBuffer.append(Arrays.copyOfRange(buffer, 0, count)); + while (messageBuffer.hasCompleteMessage()) { + String ircMessage = messageBuffer.getNextMessage(); + + processMessage(ircMessage); + } + } + } catch (IOException e) { + quit("Internal Error: " + e); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/MessageBuffer.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/MessageBuffer.java new file mode 100644 index 0000000..0d50fc2 --- /dev/null +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/MessageBuffer.java @@ -0,0 +1,29 @@ +package land.chipmunk.chayapak.chomens_bot.irc; + +public class MessageBuffer { + private String buffer; + + public MessageBuffer() { + buffer = ""; + } + + public void append (byte[] bytes) { + buffer += new String(bytes); + } + + public boolean hasCompleteMessage() { + return buffer.contains("\r\n"); + } + + public String getNextMessage() { + int index = buffer.indexOf("\r\n"); + String message = ""; + + if (index > -1) { + message = buffer.substring(0, index); + buffer = buffer.substring(index + 2); + } + + return message; + } +} diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/MessageParser.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/MessageParser.java new file mode 100644 index 0000000..fd68032 --- /dev/null +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/irc/MessageParser.java @@ -0,0 +1,70 @@ +package land.chipmunk.chayapak.chomens_bot.irc; + +import land.chipmunk.chayapak.chomens_bot.data.IRCMessage; + +// https://gist.github.com/kaecy/286f8ad334aec3fcb588516feb727772#file-messageparser-java +public class MessageParser { + public static IRCMessage message (String ircMessage) { + final IRCMessage message = new IRCMessage(); + + int spIndex; + + if (ircMessage.startsWith(":")) { + spIndex = ircMessage.indexOf(' '); + if (spIndex > -1) { + message.origin = ircMessage.substring(1, spIndex); + ircMessage = ircMessage.substring(spIndex + 1); + + int uIndex = message.origin.indexOf('!'); + if (uIndex > -1) { + message.nickName = message.origin.substring(0, uIndex); + + message.origin = message.origin.substring(uIndex + 1); + } + } + } + + spIndex = ircMessage.indexOf(' '); + if (spIndex == -1) { + message.command = "null"; + return message; + } + + message.command = ircMessage.substring(0, spIndex).toLowerCase(); + ircMessage = ircMessage.substring(spIndex + 1); + + // parse privmsg params + if (message.command.equals("privmsg")) { + spIndex = ircMessage.indexOf(' '); + message.channel = ircMessage.substring(0, spIndex); + ircMessage = ircMessage.substring(spIndex + 1); + + if (ircMessage.startsWith(":")) { + message.content = ircMessage.substring(1); + } else { + message.content = ircMessage; + } + } + + // parse quit/join + if (message.command.equals("quit") || message.command.equals("join")) { + if (ircMessage.startsWith(":")) { + message.content = ircMessage.substring(1); + } else { + message.content = ircMessage; + } + } + + // parse ping params + if (message.command.equals("ping")) { + spIndex = ircMessage.indexOf(' '); + if (spIndex > -1) { + message.content = ircMessage.substring(0, spIndex); + } else { + message.content = ircMessage; + } + } + + return message; + } +} diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/ConsolePlugin.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/ConsolePlugin.java index bbe7a53..d7b98ae 100644 --- a/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/ConsolePlugin.java +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/ConsolePlugin.java @@ -25,7 +25,7 @@ public class ConsolePlugin implements Completer { private static final List listeners = new ArrayList<>(); - public ConsolePlugin (List allBots, Configuration discordConfig, JDA jda) { + public ConsolePlugin (List allBots, Configuration config, JDA jda) { this.allBots = allBots; this.reader = LineReaderBuilder .builder() @@ -42,7 +42,8 @@ public class ConsolePlugin implements Completer { bot.logger = new LoggerPlugin(bot); } - new DiscordPlugin(discordConfig, jda); + new DiscordPlugin(config, jda); + if (config.irc.enabled) new IRCPlugin(config); final String prompt = "> "; diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/DiscordPlugin.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/DiscordPlugin.java index 3f19734..1f089e2 100644 --- a/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/DiscordPlugin.java +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/DiscordPlugin.java @@ -314,7 +314,7 @@ public class DiscordPlugin { } StringBuilder logMessage = logMessages.get(channelId); if (logMessage.length() < 2000) { - if (logMessage.length() > 0) { + if (!logMessage.isEmpty()) { logMessage.append('\n'); } logMessage.append(message); diff --git a/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/IRCPlugin.java b/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/IRCPlugin.java new file mode 100644 index 0000000..bf6472f --- /dev/null +++ b/src/main/java/land/chipmunk/chayapak/chomens_bot/plugins/IRCPlugin.java @@ -0,0 +1,204 @@ +package land.chipmunk.chayapak.chomens_bot.plugins; + +import com.github.steveice10.packetlib.event.session.ConnectedEvent; +import land.chipmunk.chayapak.chomens_bot.Bot; +import land.chipmunk.chayapak.chomens_bot.Configuration; +import land.chipmunk.chayapak.chomens_bot.Main; +import land.chipmunk.chayapak.chomens_bot.command.IRCCommandContext; +import land.chipmunk.chayapak.chomens_bot.data.IRCMessage; +import land.chipmunk.chayapak.chomens_bot.data.chat.PlayerMessage; +import land.chipmunk.chayapak.chomens_bot.irc.IRCMessageLoop; +import land.chipmunk.chayapak.chomens_bot.util.ComponentUtilities; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class IRCPlugin extends IRCMessageLoop { + private final Configuration.IRC ircConfig; + + private final Map servers; + + private final Map> messageQueue = new HashMap<>(); + + public IRCPlugin (Configuration config) { + super(config.irc.host, config.irc.port); + + this.ircConfig = config.irc; + + this.servers = ircConfig.servers; + + nick(ircConfig.nickname); + user(ircConfig.username, ircConfig.hostName, ircConfig.serverName, ircConfig.realName); + + for (Map.Entry entry : ircConfig.servers.entrySet()) join(entry.getValue()); + + Main.executorService.submit(this); + + for (Bot bot : Main.bots) { + bot.addListener(new Bot.Listener() { + @Override + public void connected(ConnectedEvent event) { + IRCPlugin.this.connected(bot); + } + + @Override + public void loadedPlugins() { + bot.chat.addListener(new ChatPlugin.Listener() { + @Override + public void playerMessageReceived(PlayerMessage message) { + IRCPlugin.this.playerMessageReceived(bot, message); + } + }); + } + }); + + bot.irc = this; + } + + Main.executor.scheduleAtFixedRate(this::queueTick, 0, 500, TimeUnit.MILLISECONDS); + } + + @Override + protected void onMessage (IRCMessage message) { + Bot serverBot = null; + + String channel = null; + + for (Map.Entry entry : servers.entrySet()) { + if (entry.getValue().equals(message.channel)) { + serverBot = Main.bots.stream() + .filter(eachBot -> entry.getKey().equals(eachBot.host + ":" + eachBot.port)) + .toArray(Bot[]::new)[0]; + + channel = entry.getValue(); + + break; + } + } + + if (serverBot == null) return; + + final String commandPrefix = ircConfig.prefix; + + if (message.content.startsWith(commandPrefix)) { + final String noPrefix = message.content.substring(commandPrefix.length()); + + final IRCCommandContext context = new IRCCommandContext(serverBot, commandPrefix, message.nickName); + + final Component output = serverBot.commandHandler.executeCommand(noPrefix, context, null); + + if (output != null) context.sendOutput(output); + + return; + } + + final Component prefix = Component + .text(channel) + .hoverEvent( + HoverEvent.showText( + Component + .empty() + .append(Component.text("on ")) + .append(Component.text(ircConfig.host)) + .append(Component.text(":")) + .append(Component.text(ircConfig.port)) + .color(NamedTextColor.GRAY) + ) + ) + .color(NamedTextColor.BLUE); + + final Component username = Component + .text(message.nickName) + .hoverEvent(HoverEvent.showText(Component.text(message.origin).color(NamedTextColor.RED))) + .color(NamedTextColor.RED); + + final Component messageComponent = Component + .text(message.content) + .color(NamedTextColor.GRAY); + + final Component component = Component.translatable( + "[%s] %s › %s", + prefix, + username, + messageComponent + ).color(NamedTextColor.DARK_GRAY); + + serverBot.chat.tellraw(component); + } + + private void playerMessageReceived (Bot bot, PlayerMessage message) { + final String stringifiedName = ComponentUtilities.stringify(message.displayName); + final String stringifiedContents = ComponentUtilities.stringify(message.contents); + + addMessageToQueue( + bot, + String.format( + "<%s> %s", + stringifiedName, + stringifiedContents + ) + ); + } + + // i only have listened to connected because the ratelimit + private void connected (Bot bot) { + sendMessage( + bot, + String.format( + "Successfully connected to: %s:%s", + bot.host, + bot.port + ) + ); + } + + private void queueTick () { + if (!initialSetupStatus || messageQueue.isEmpty()) return; + + for (Map.Entry> entry : messageQueue.entrySet()) { + final List logs = entry.getValue(); + + if (logs.isEmpty()) continue; + + final String firstLog = logs.get(0); + + logs.remove(0); + + channelMessage(entry.getKey(), firstLog); + } + } + + private void addMessageToQueue (Bot bot, String message) { + final String channel = servers.get(bot.host + ":" + bot.port); + + addMessageToQueue(channel, message); + } + + private void addMessageToQueue (String channel, String message) { + final List split = new ArrayList<>(Arrays.asList(message.split("\n"))); + + if (!messageQueue.containsKey(channel)) { + messageQueue.put(channel, split); + } else { + if (messageQueue.get(channel).size() > 10) return; + + messageQueue.get(channel).addAll(split); + } + } + + public void sendMessage (Bot bot, String message) { + final String hostAndPort = bot.host + ":" + bot.port; + + final String channel = servers.get(hostAndPort); + + if (channel == null) { + bot.logger.error("Channel is not configured for " + hostAndPort); + return; + } + + addMessageToQueue(channel, message); + } +} diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index c64c4f3..363fd1f 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -34,6 +34,19 @@ discord: servers: localhost:25565: 'channel id' +irc: + enabled: true + prefix: '!' + host: 'irc.libera.chat' + port: 6665 + nickname: 'chomens-bot' + username: 'chomens-bot' + realName: 'chomens-bot' + hostName: 'null' + serverName: 'null' + servers: + localhost:25565: '#chomens-localhost' # channel name + music: URLRatelimit: seconds: 15