diff --git a/src/main/java/net/gensokyoreimagined/motoori/Kosuzu.java b/src/main/java/net/gensokyoreimagined/motoori/Kosuzu.java index 03fbfde..87ffca4 100644 --- a/src/main/java/net/gensokyoreimagined/motoori/Kosuzu.java +++ b/src/main/java/net/gensokyoreimagined/motoori/Kosuzu.java @@ -15,21 +15,13 @@ package net.gensokyoreimagined.motoori; -import com.comphenix.protocol.PacketType; -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.ProtocolManager; -import com.comphenix.protocol.events.ListenerPriority; -import com.comphenix.protocol.events.PacketAdapter; -import com.comphenix.protocol.events.PacketEvent; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; -import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.java.JavaPlugin; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import java.util.Objects; public final class Kosuzu extends JavaPlugin { @@ -61,8 +53,13 @@ public void onEnable() { config.addDefault("storage.mysql.username", "kosuzu"); config.addDefault("storage.mysql.password", "changeme"); - var regexDefaults = new HashMap(); - regexDefaults.put("match.include", "https?://[-a-zA-Z0-9@:%._\\+~#?&//=]+"); + var regexDefaults = List.of( + "^<[^>]+> (.*)", // Vanilla + "^[^»]+» (.*)", // Discord + "^(?::build:|:dev_server:).+?: (.*)" // Chatty + ); + + config.addDefault("match.include", regexDefaults); config.options().copyDefaults(true); saveConfig(); diff --git a/src/main/java/net/gensokyoreimagined/motoori/KosuzuKnowsWhereYouLive.java b/src/main/java/net/gensokyoreimagined/motoori/KosuzuKnowsWhereYouLive.java index a177c86..514dbe6 100644 --- a/src/main/java/net/gensokyoreimagined/motoori/KosuzuKnowsWhereYouLive.java +++ b/src/main/java/net/gensokyoreimagined/motoori/KosuzuKnowsWhereYouLive.java @@ -27,9 +27,16 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Properties; +import java.util.logging.Logger; public class KosuzuKnowsWhereYouLive { - public static @Nullable String getCountryCode(Player player) { + private final Logger logger; + + public KosuzuKnowsWhereYouLive(Kosuzu kosuzu) { + logger = kosuzu.getLogger(); + } + + public @Nullable String getCountryCode(Player player) { var address = player.getAddress(); if (address == null) { return null; @@ -43,13 +50,13 @@ public class KosuzuKnowsWhereYouLive { * @param ip The IP address * @return The country code, or null if the IP address is invalid or location is unknown */ - public static @Nullable String getCountryCode(String ip) { + public @Nullable String getCountryCode(String ip) { if (ip == null) { return null; } if (ip.equals("127.0.0.1")) { - System.out.println("Localhost IP address detected from player; check forwarding on proxy?"); + logger.warning("Localhost IP address detected from player; check forwarding on proxy?"); return null; } diff --git a/src/main/java/net/gensokyoreimagined/motoori/KosuzuRemembersEverything.java b/src/main/java/net/gensokyoreimagined/motoori/KosuzuRemembersEverything.java index 087cf6d..d5e0d4a 100644 --- a/src/main/java/net/gensokyoreimagined/motoori/KosuzuRemembersEverything.java +++ b/src/main/java/net/gensokyoreimagined/motoori/KosuzuRemembersEverything.java @@ -348,13 +348,37 @@ public Collection getLanguages() { return null; } - public @Nullable UUID addMessage(@NotNull String message) { + public @Nullable UUID addMessage(@NotNull String json, @NotNull String message, @NotNull UUID player) { try (var connection = getConnection()) { + // First cache the message, if necessary + // Then add the JSON to the database separately (player-specific) + + UUID messageUUID = null; + + try (var statement = connection.prepareStatement("SELECT message_id FROM `message` WHERE `text` = ?")) { + statement.setString(1, message); + var data = statement.executeQuery(); + if (data.next()) { + messageUUID = UUID.fromString(data.getString("message_id")); + } + } + + if (messageUUID == null) { + messageUUID = UUID.randomUUID(); + try (var statement = connection.prepareStatement("INSERT INTO `message` (`message_id`, `text`) VALUES (?, ?)")) { + statement.setString(1, messageUUID.toString()); + statement.setString(2, message); + statement.execute(); + } + } + var uuid = UUID.randomUUID(); - try (var statement = connection.prepareStatement("INSERT INTO `message` (`message_id`, `text`) VALUES (?, ?)")) { + try (var statement = connection.prepareStatement("INSERT INTO `user_message` (`uuid`, `user_id`, `message_id`, `json_msg`) VALUES (?, ?, ?, ?)")) { statement.setString(1, uuid.toString()); - statement.setString(2, message); + statement.setString(2, player.toString()); + statement.setString(3, messageUUID.toString()); + statement.setString(4, json); statement.execute(); } diff --git a/src/main/java/net/gensokyoreimagined/motoori/KosuzuUnderstandsEverything.java b/src/main/java/net/gensokyoreimagined/motoori/KosuzuUnderstandsEverything.java index 5463e88..82256e8 100644 --- a/src/main/java/net/gensokyoreimagined/motoori/KosuzuUnderstandsEverything.java +++ b/src/main/java/net/gensokyoreimagined/motoori/KosuzuUnderstandsEverything.java @@ -21,7 +21,6 @@ import com.comphenix.protocol.events.ListenerPriority; import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketEvent; -import com.google.common.util.concurrent.RateLimiter; import io.papermc.paper.event.player.AsyncChatEvent; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; @@ -31,38 +30,54 @@ import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; +import java.util.logging.Logger; +import java.util.regex.Pattern; public class KosuzuUnderstandsEverything implements Listener { - + private final Logger logger; private final KosuzuTranslatesEverything translator; private final KosuzuRemembersEverything database; + private final KosuzuKnowsWhereYouLive geolocation; + + private final ArrayList regexes = new ArrayList<>(); + private final Map> placeholderRegexes = new HashMap<>(); public KosuzuUnderstandsEverything(Kosuzu kosuzu) { + logger = kosuzu.getLogger(); translator = new KosuzuTranslatesEverything(kosuzu); database = kosuzu.database; + geolocation = new KosuzuKnowsWhereYouLive(kosuzu); ProtocolManager manager = ProtocolLibrary.getProtocolManager(); manager.addPacketListener(new PacketAdapter(kosuzu, ListenerPriority.NORMAL, PacketType.Play.Server.CHAT) { @Override public void onPacketSending(PacketEvent event) { - KosuzuUnderstandsEverything.this.onPacketSending(event); + KosuzuUnderstandsEverything.this.onPacketSending(event, false); } }); manager.addPacketListener(new PacketAdapter(kosuzu, ListenerPriority.NORMAL, PacketType.Play.Server.SYSTEM_CHAT) { @Override public void onPacketSending(PacketEvent event) { - KosuzuUnderstandsEverything.this.onPacketSending(event); + KosuzuUnderstandsEverything.this.onPacketSending(event, true); } }); + + prepareRegexes(kosuzu.getConfig()); } // To be deprecated, replaced by ProtocolLib @@ -88,12 +103,65 @@ public void onPlayerMessage(@NotNull AsyncChatEvent event) { } // Called by ProtocolLib - public void onPacketSending(PacketEvent event) { + private void onPacketSending(PacketEvent event, boolean isSystem) { var player = event.getPlayer(); var packet = event.getPacket(); var message = packet.getChatComponents().read(0); var component = JSONComponentSerializer.json().deserialize(message.getJson()); // Adventure API from raw JSON - // getLogger().info("SYSTEM CHAT EVENT TO " + player.getName() + " " + message.getJson()); + // logger.info("A CHAT EVENT TO " + player.getName() + ": " + getTextMessage(component, isSystem, player)); + } + + private void prepareRegexes(@NotNull FileConfiguration config) { + var regexes = config.getStringList("match.include"); + + for (var regex : regexes) { + if (regex.contains("%username%")) { + placeholderRegexes.put(regex, new HashMap<>()); + } else { + this.regexes.add(Pattern.compile(regex)); + } + } + + logger.info("Prepared " + this.regexes.size() + " regexes"); + } + + /** + * Extracts the text message from a chat component + * Also determines if we should translate the message + * @param component The chat component created from the message + * @param isSystem Whether the message is a system message + * @param player The player who sent the message + * @return The text message, or null if it could/should not be translated + */ + private @Nullable String getTextMessage(Component component, boolean isSystem, Player player) { + var text = PlainTextComponentSerializer.plainText().serialize(component); + + logger.info("TEXT: " + text); + + if (!isSystem) { + return text; + } + + for (var pattern : regexes) { + var matcher = pattern.matcher(text); + if (matcher.matches()) { + return matcher.group(1); + } + } + + for (var placeholder : placeholderRegexes.entrySet()) { + var regex = placeholder.getKey(); + var cache = placeholder.getValue(); + + var pattern = cache.computeIfAbsent(player.getUniqueId(), (key) -> Pattern.compile(regex.replace("%username%", player.getName()))); + var matcher = pattern.matcher(text); + + if (matcher.matches()) { + return matcher.group(1); + } + } + + return null; } @EventHandler(priority = EventPriority.HIGHEST) @@ -107,7 +175,7 @@ public void onPlayerJoin(@NotNull PlayerJoinEvent event) { if (database.isNewUser(uuid, name)) { String welcome; - var country = KosuzuKnowsWhereYouLive.getCountryCode(player); + var country = geolocation.getCountryCode(player); if (country == null) { welcome = database.getTranslation("welcome.new", null); @@ -128,6 +196,14 @@ public void onPlayerJoin(@NotNull PlayerJoinEvent event) { } } + @EventHandler + public void onPlayerDisconnect(@NotNull PlayerQuitEvent event) { + var player = event.getPlayer(); + + // Remove from cache + placeholderRegexes.values().forEach(map -> map.remove(player.getUniqueId())); + } + private void translateCallback(AsyncChatEvent event, Audience player) { // var ratelimit = RateLimiter.create(2000); diff --git a/src/main/resources/dbinit.sql b/src/main/resources/dbinit.sql index e29a168..e2e412c 100644 --- a/src/main/resources/dbinit.sql +++ b/src/main/resources/dbinit.sql @@ -27,7 +27,8 @@ CREATE TABLE IF NOT EXISTS `multilingual` CREATE TABLE IF NOT EXISTS `message` ( `message_id` VARCHAR(36) NOT NULL, - `text` VARCHAR(2048) NOT NULL, -- We don't explicitly know the language of the message, so we store it here + `text` VARCHAR(256) NOT NULL, -- We don't explicitly know the language of the message, so we store it here + `language` VARCHAR(8), -- ISO 639-1 code of the message, NULL if the language is unknown PRIMARY KEY (`message_id`), UNIQUE (`text`) ); @@ -50,9 +51,9 @@ CREATE TABLE IF NOT EXISTS `user_message` `uuid` VARCHAR(36) NOT NULL, -- Surrogate key `user_id` VARCHAR(36) NOT NULL, -- To which user this message belongs `message_id` VARCHAR(36) NOT NULL, -- To which message this translation belongs + `json_msg` JSON NOT NULL, -- The JSON message from Minecraft PRIMARY KEY (`uuid`), - - FOREIGN KEY (`uuid`) REFERENCES `user` (`uuid`) ON DELETE CASCADE, + FOREIGN KEY (`user_id`) REFERENCES `user` (`uuid`) ON DELETE CASCADE, FOREIGN KEY (`message_id`) REFERENCES `message` (`message_id`) ON DELETE CASCADE );