From c051ec4848b86107585ae7f04443ce9c6f7670e8 Mon Sep 17 00:00:00 2001 From: GoldenKoopa <100142836+GoldenKoopa@users.noreply.github.com> Date: Sat, 26 Apr 2025 03:10:51 +0200 Subject: [PATCH 1/8] feat!: use json, uuids, integrations and missed notifications - major refactor for storing data - floodgate integration to support bedrock accounts joining with geyser - justsync integration to not give notifications for registered alts BREAKING CHANGE: old data is incompatible due to usage of usernames instead of uuids --- .editorconfig | 68 ++++++ .gitignore | 1 + build.gradle | 47 +++- gradlew | 0 .../com/xadale/playerlogger/Commands.java | 15 +- .../com/xadale/playerlogger/IpLogger.java | 20 ++ .../playerlogger/NotificationHandler.java | 158 ++++++++++++++ .../com/xadale/playerlogger/PlayerLogger.java | 203 ++++++------------ .../java/com/xadale/playerlogger/Utils.java | 77 +++++++ .../commands/HandleAltsCommand.java | 80 ++++--- .../commands/ListIpsWithMultiplePlayers.java | 84 +++----- .../compat/FloodgateIntegration.java | 50 +++++ .../compat/JustSyncIntegration.java | 62 ++++++ .../xadale/playerlogger/config/Config.java | 71 ++++++ .../playerlogger/core/AbstractData.java | 23 ++ .../playerlogger/core/DataRepository.java | 17 ++ .../core/LocalDateTimeAdapter.java | 31 +++ .../com/xadale/playerlogger/data/IpAss.java | 69 ++++++ .../playerlogger/data/LastReadNotif.java | 30 +++ .../com/xadale/playerlogger/data/Notif.java | 37 ++++ .../mixin/PlayerManagerMixin.java | 41 ++++ .../repositories/IpAssRepository.java | 6 + .../repositories/JsonIpAssRepositoryImpl.java | 86 ++++++++ .../JsonLastReadNotifRepositoryImpl.java | 87 ++++++++ .../repositories/JsonNotifRepositoryImpl.java | 92 ++++++++ .../repositories/LastReadNotifRepository.java | 7 + .../repositories/NotifRepository.java | 6 + src/main/resources/altx.mixins.json | 12 ++ src/main/resources/fabric.mod.json | 3 + 29 files changed, 1242 insertions(+), 241 deletions(-) create mode 100644 .editorconfig mode change 100644 => 100755 gradlew create mode 100644 src/main/java/com/xadale/playerlogger/IpLogger.java create mode 100644 src/main/java/com/xadale/playerlogger/NotificationHandler.java create mode 100644 src/main/java/com/xadale/playerlogger/Utils.java create mode 100644 src/main/java/com/xadale/playerlogger/compat/FloodgateIntegration.java create mode 100644 src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java create mode 100644 src/main/java/com/xadale/playerlogger/config/Config.java create mode 100644 src/main/java/com/xadale/playerlogger/core/AbstractData.java create mode 100644 src/main/java/com/xadale/playerlogger/core/DataRepository.java create mode 100644 src/main/java/com/xadale/playerlogger/core/LocalDateTimeAdapter.java create mode 100644 src/main/java/com/xadale/playerlogger/data/IpAss.java create mode 100644 src/main/java/com/xadale/playerlogger/data/LastReadNotif.java create mode 100644 src/main/java/com/xadale/playerlogger/data/Notif.java create mode 100644 src/main/java/com/xadale/playerlogger/mixin/PlayerManagerMixin.java create mode 100644 src/main/java/com/xadale/playerlogger/repositories/IpAssRepository.java create mode 100644 src/main/java/com/xadale/playerlogger/repositories/JsonIpAssRepositoryImpl.java create mode 100644 src/main/java/com/xadale/playerlogger/repositories/JsonLastReadNotifRepositoryImpl.java create mode 100644 src/main/java/com/xadale/playerlogger/repositories/JsonNotifRepositoryImpl.java create mode 100644 src/main/java/com/xadale/playerlogger/repositories/LastReadNotifRepository.java create mode 100644 src/main/java/com/xadale/playerlogger/repositories/NotifRepository.java create mode 100644 src/main/resources/altx.mixins.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7dfc609 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,68 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 80 +tab_width = 2 + +[.gitignore] +max_line_length = unset + +[*.md] +max_line_length = unset + +[*.feature] +tab_width = 4 + +[*.gsp] +indent_size = 4 +tab_width = 4 + +[*.haml] +tab_width = 4 + +[*.less] +tab_width = 4 + +[*.styl] +tab_width = 4 + +[.editorconfig] +max_line_length = unset + +[{*.as,*.js2,*.es}] +indent_size = 4 +tab_width = 4 + +[{*.cfml,*.cfm,*.cfc}] +indent_size = 4 +tab_width = 4 + +[{*.cjs,*.js}] +max_line_length = 80 + +[{*.gradle,*.groovy,*.gant,*.gdsl,*.gy,*.gson}] +indent_size = 4 +tab_width = 4 + +[{*.jspx,*.tagx}] +indent_size = 4 +tab_width = 4 + +[{*.kts,*.kt}] +indent_size = 4 +tab_width = 4 + +[{*.vsl,*.vm,*.ft}] +indent_size = 4 +tab_width = 4 + +[{*.xjsp,*.tag,*.jsf,*.jsp,*.jspf,*.tagf}] +indent_size = 4 +tab_width = 4 + +[{*.yml,*.yaml}] +tab_width = 4 + diff --git a/.gitignore b/.gitignore index c476faf..8c6cb44 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ out/ classes/ +.factorypath # eclipse diff --git a/build.gradle b/build.gradle index 40a3d4c..436cce2 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,24 @@ base { archivesName = project.archives_base_name } +configurations { + embed + compileOnly.extendsFrom(embed) +} + +shadowJar { + configurations = [project.configurations.embed] + exclude('META-INF/services/**') + relocate "com.moandjiezana", "dcshadow.com.moandjiezana" +} + repositories { + mavenCentral() + maven { + url = uri("https://repo.opencollab.dev/main/") + } + maven { url 'https://jitpack.io' } + // Add repositories to retrieve artifacts from in here. // You should only use this when depending on other mods because // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. @@ -23,7 +40,7 @@ loom { splitEnvironmentSourceSets() mods { - "modid" { + "altx" { sourceSet sourceSets.main sourceSet sourceSets.client } @@ -46,14 +63,26 @@ dependencies { mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" include(modImplementation("me.lucko:fabric-permissions-api:${project.fabric_permission_api_version}")) embed implementation("club.minnced:discord-webhooks:${project.minnced_discord_webhook_version}") + implementation("club.minnced:discord-webhooks:${project.minnced_discord_webhook_version}") + + // config + embed implementation("de.erdbeerbaerlp:toml4j:dd60f51") + + // floodgate integration + compileOnly('org.geysermc.floodgate:api:2.2.4-SNAPSHOT') + + // justsync integration + compileOnly('com.github.OrdinarySMP:DiscordJustSync:master-SNAPSHOT') + } +tasks.build.dependsOn(tasks.shadowJar) + processResources { inputs.property "version", project.version @@ -76,6 +105,15 @@ java { targetCompatibility = JavaVersion.VERSION_21 } +remapJar { + // wait until the shadowJar is done + dependsOn(shadowJar) + mustRunAfter(shadowJar) + // Set the input jar for the task. Here use the shadow Jar that include the .class of the transitive dependency + inputFile = file(shadowJar.archiveFile) +} + + jar { from("LICENSE") { rename { "${it}_${project.base.archivesName.get()}"} @@ -90,6 +128,11 @@ remapJar { inputFile = file(shadowJar.archiveFile) } +artifacts { + archives tasks.shadowJar +} + + // configure the maven publication publishing { publications { diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/xadale/playerlogger/Commands.java b/src/main/java/com/xadale/playerlogger/Commands.java index d842cc9..e6a4728 100644 --- a/src/main/java/com/xadale/playerlogger/Commands.java +++ b/src/main/java/com/xadale/playerlogger/Commands.java @@ -12,12 +12,6 @@ public class Commands { - private final PlayerLogger altx; - - public Commands(PlayerLogger altx) { - this.altx = altx; - } - public void register() { // Register the command CommandRegistrationCallback.EVENT.register( @@ -42,7 +36,9 @@ public void register() { if (Permissions.check(source, "altx.trace", 4)) { help.append( - "\n§b/altx trace §fShows all players on given players IP address"); + "\n" + + "§b/altx trace §fShows all players on given players IP" + + " address"); if (Permissions.check(source, "altx.viewips", 4)) { help.append( @@ -85,7 +81,8 @@ public void register() { .executes( (context) -> HandleAltsCommand.execute( - context, this.altx.getLogFile())))) + context, + PlayerLogger.getInstance().getIpAssRepository())))) // Command list .then( @@ -94,7 +91,7 @@ public void register() { .executes( (context) -> ListIpsWithMultiplePlayers.execute( - context, this.altx.getLogFile())))); + context, PlayerLogger.getInstance().getIpAssRepository())))); }); } } diff --git a/src/main/java/com/xadale/playerlogger/IpLogger.java b/src/main/java/com/xadale/playerlogger/IpLogger.java new file mode 100644 index 0000000..f27ec58 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/IpLogger.java @@ -0,0 +1,20 @@ +package com.xadale.playerlogger; + +import com.xadale.playerlogger.data.IpAss; +import com.xadale.playerlogger.repositories.IpAssRepository; +import java.util.Optional; +import java.util.UUID; + +public class IpLogger { + + public static void handleJoin(String ip, UUID uuid) { + IpAssRepository ipAssdataRepository = PlayerLogger.getInstance().getIpAssRepository(); + Optional ipAss = ipAssdataRepository.get(ip); + if (ipAss.isEmpty()) { + ipAssdataRepository.add(new IpAss(ip, uuid)); + return; + } + ipAss.get().addUUid(uuid); + NotificationHandler.handleNotif(ipAss.get(), uuid); + } +} diff --git a/src/main/java/com/xadale/playerlogger/NotificationHandler.java b/src/main/java/com/xadale/playerlogger/NotificationHandler.java new file mode 100644 index 0000000..ef01fa2 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/NotificationHandler.java @@ -0,0 +1,158 @@ +package com.xadale.playerlogger; + +import com.xadale.playerlogger.compat.JustSyncIntegration; +import com.xadale.playerlogger.data.IpAss; +import com.xadale.playerlogger.data.LastReadNotif; +import com.xadale.playerlogger.data.Notif; +import com.xadale.playerlogger.repositories.NotifRepository; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public class NotificationHandler { + + public static void handleNotif(IpAss ipAss, UUID uuid) { + if (!PlayerLogger.getInstance().getConfig().altNotifs.enableNotifs) { + return; + } + List uuids = ipAss.getUuids(); + List linkedUuids = JustSyncIntegration.getIntegration().getRelatedUuids(uuid); + // TODO: filter authorized accounts + List filteredUuids = uuids.stream().filter(u -> !linkedUuids.contains(u)).toList(); + if (filteredUuids.isEmpty()) { + return; + } + String message = getMessage(uuid, filteredUuids); + + PlayerLogger.getInstance() + .getServer() + .getPlayerManager() + .getPlayerList() + .forEach( + player -> { + if (Permissions.check(player, "altx.notify", 4)) { + player.sendMessage(Text.literal(message).formatted(Formatting.RED), false); + PlayerLogger.getInstance() + .getLastReadNotifRepository() + .get(player.getUuid()) + .get() + .setLastNotifId(getLastNotifId() + 1); + ; + } + }); + // File log = + // PlayerLogger.getConfigFolder().resolve(PlayerLogger.modId + + // ".notifications.log").toFile(); + String my_file_name = + PlayerLogger.getConfigFolder() + .resolve(PlayerLogger.modId + ".notifications.log") + .toString(); + try (BufferedWriter output = new BufferedWriter(new FileWriter(my_file_name, true))) { + output.append(LocalDateTime.now().toString() + ' '); + output.append(message); + output.newLine(); + } catch (IOException ignored) { + } + + Notif notif = new Notif(getLastNotifId() + 1, uuid, filteredUuids, LocalDateTime.now()); + PlayerLogger.getInstance().getNotifRepository().add(notif); + } + + public static void onJoin( + ServerPlayNetworkHandler handler, PacketSender sender, MinecraftServer server) { + ServerPlayerEntity player = handler.getPlayer(); + + // has permission for notifications + if (!Permissions.check(player, "altx.notify", 4)) { + return; + } + + // if not already seen notifications before (joined with permission before) + // create last read tracking object + Optional lastReadNotif = + PlayerLogger.getInstance().getLastReadNotifRepository().get(player.getUuid()); + if (lastReadNotif.isEmpty()) { + LastReadNotif newLastReadNotif = new LastReadNotif(player.getUuid(), getLastNotifId()); + PlayerLogger.getInstance().getLastReadNotifRepository().add(newLastReadNotif); + return; + } + + if (!PlayerLogger.getInstance().getConfig().altNotifs.showMissedNotifsOnJoin) { + return; + } + + // show missed notifications + List missedNotifs = + getMissedNotifs( + lastReadNotif.get().getLastNotifId(), + PlayerLogger.getInstance().getConfig().altNotifs.notificationPeriod); + for (Notif notif : missedNotifs) { + player.sendMessage( + Text.literal(getMessage(notif.getUuid(), notif.getAlts())).formatted(Formatting.RED), + false); + } + + lastReadNotif.get().setLastNotifId(getLastNotifId()); + } + + public static List getMissedNotifs(long afterId, int hours) { + LocalDateTime cutoff = LocalDateTime.now().minusHours(hours); + NotifRepository repository = PlayerLogger.getInstance().getNotifRepository(); + + return repository + .getAll() + .filter(notif -> notif.getId() > afterId) + .filter(notif -> notif.getTime() != null && !notif.getTime().isBefore(cutoff)) + .toList(); + } + + public static String getMessage(UUID uuid, List uuids) { + return "Player " + + Utils.getPlayerName(uuid) + + " is a potential alt of: " + + uuids.stream().map(Utils::getPlayerName).toList() + + "."; + } + + public static int getLastNotifId() { + return PlayerLogger.getInstance() + .getNotifRepository() + .getAll() + .mapToInt(Notif::getId) + .max() + .orElse(0); + } + + public static void purgeOldNotifs(int hours) { + LocalDateTime cutoff = LocalDateTime.now().minusHours(hours); + NotifRepository repository = PlayerLogger.getInstance().getNotifRepository(); + + Optional newestNotifOptional = repository.get(getLastNotifId()); + + if (newestNotifOptional.isEmpty()) { + return; + } + + Notif newestNotif = newestNotifOptional.get(); + + List toRemove = + repository + .getAll() + .filter(notif -> notif != newestNotif) + .filter(notif -> notif.getTime() != null && notif.getTime().isBefore(cutoff)) + .toList(); + + toRemove.forEach(repository::remove); + } +} diff --git a/src/main/java/com/xadale/playerlogger/PlayerLogger.java b/src/main/java/com/xadale/playerlogger/PlayerLogger.java index d99d0e8..86cb2da 100644 --- a/src/main/java/com/xadale/playerlogger/PlayerLogger.java +++ b/src/main/java/com/xadale/playerlogger/PlayerLogger.java @@ -1,160 +1,93 @@ package com.xadale.playerlogger; -import club.minnced.discord.webhook.WebhookClient; -import java.io.BufferedReader; -import java.io.BufferedWriter; +import com.xadale.playerlogger.compat.FloodgateIntegration; +import com.xadale.playerlogger.config.Config; +import com.xadale.playerlogger.repositories.IpAssRepository; +import com.xadale.playerlogger.repositories.JsonIpAssRepositoryImpl; +import com.xadale.playerlogger.repositories.JsonLastReadNotifRepositoryImpl; +import com.xadale.playerlogger.repositories.JsonNotifRepositoryImpl; +import com.xadale.playerlogger.repositories.LastReadNotifRepository; +import com.xadale.playerlogger.repositories.NotifRepository; import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; -import java.net.InetSocketAddress; import java.nio.file.Files; -import java.util.Objects; - -import club.minnced.discord.webhook.send.WebhookMessageBuilder; -import me.lucko.fabric.api.permissions.v0.Permissions; +import java.nio.file.Path; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.server.MinecraftServer; public class PlayerLogger implements ModInitializer { - private File logFile; - - private WebhookClient webhookClient; + public static String modId = "altx"; + private static PlayerLogger instance; + private MinecraftServer server; + private FloodgateIntegration floodgateIntegration; + private IpAssRepository ipAssDataRepository; + private NotifRepository notifRepository; + private LastReadNotifRepository lastReadNotifRepository; + private Config config = Config.loadConfig(); @Override public void onInitialize() { - File logDir = new File("AltX-Files"); - if (!logDir.exists()) { - logDir.mkdir(); // Creates the folder if it doesn't exist + PlayerLogger.instance = this; + + ServerLifecycleEvents.SERVER_STARTING.register(s -> this.server = s); + this.floodgateIntegration = new FloodgateIntegration(); + + File ipAssFile = + PlayerLogger.getConfigFolder().resolve(PlayerLogger.modId + ".ips.json").toFile(); + File notifFile = + PlayerLogger.getConfigFolder().resolve(PlayerLogger.modId + ".notifs.json").toFile(); + File lastReadNotifFile = + PlayerLogger.getConfigFolder().resolve(PlayerLogger.modId + ".lastReadNotif.json").toFile(); + + try { + Files.createDirectories(PlayerLogger.getConfigFolder()); + } catch (IOException ignored) { } - this.logFile = new File(logDir, "player_ips.txt"); - this.loadWebhook(logDir); - - // Register connection event to log player IPs - ServerPlayConnectionEvents.JOIN.register( - (handler, sender, server) -> { - String playerName = handler.getPlayer().getName().getString(); - - String ipAddress = "Unknown IP"; - if (handler.getConnectionAddress() instanceof InetSocketAddress inetSocketAddress) { - ipAddress = inetSocketAddress.getAddress().getHostAddress(); - } - - String logEntry = playerName + ";" + ipAddress; - - // Only proceed if the log entry is new - if (writeToFileIfAbsent(logEntry)) { - try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) { - StringBuilder potentialAlts = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - String[] parts = line.split(";"); - if (parts.length == 2 - && parts[1].equals(ipAddress) - && !parts[0].equals(playerName)) { - if (potentialAlts.length() > 0) { - potentialAlts.append(", "); // Add a comma between names - } - potentialAlts.append(parts[0]); - } - } - - if (potentialAlts.length() > 0) { - // Found one or more potential alts - String ingameMessage = - "Player " + playerName + " is a potential alt of: " + potentialAlts + "."; - - String discordMessage = - "Player `" + playerName + "` is a potential alt of: `" + potentialAlts + "`"; - - // Send the message in discord - if (this.webhookClient != null) { - WebhookMessageBuilder builder = - new WebhookMessageBuilder() - .setContent(discordMessage) - .setUsername("AltX Notification") - .setAvatarUrl("https://raw.githubusercontent.com/OrdinarySMP/AltX/main/assets/AltX-avatar.png"); - - this.webhookClient.send(builder.build()); - } - - // Send the message to staff - server - .getPlayerManager() - .getPlayerList() - .forEach( - player -> { - if (Permissions.check(player, "altx.notify", 4)) { - player.sendMessage( - Text.literal(ingameMessage).formatted(Formatting.RED), false); - } - }); - } - } catch (IOException e) { - System.err.println("Failed to read log file: " + e.getMessage()); - } - } - }); - - Commands commands = new Commands(this); + this.ipAssDataRepository = JsonIpAssRepositoryImpl.from(ipAssFile); + this.notifRepository = JsonNotifRepositoryImpl.from(notifFile); + this.lastReadNotifRepository = JsonLastReadNotifRepositoryImpl.from(lastReadNotifFile); + + ServerPlayConnectionEvents.JOIN.register(NotificationHandler::onJoin); + + NotificationHandler.purgeOldNotifs(this.config.altNotifs.purgePeriod); + + Commands commands = new Commands(); commands.register(); } - private boolean writeToFileIfAbsent(String entry) { - try { - if (!logFile.exists()) { - logFile.createNewFile(); - } - - boolean alreadyLogged = false; - try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) { - String line; - while ((line = reader.readLine()) != null) { - if (line.trim().equals(entry)) { - alreadyLogged = true; - break; - } - } - } - - if (!alreadyLogged) { - try (BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true))) { - writer.write(entry + "\n"); - } - return true; // New entry added - } - } catch (IOException e) { - System.err.println("Failed to write player log: " + e.getMessage()); - } - return false; // No new entry added or an exception occurred + public static Path getConfigFolder() { + return FabricLoader.getInstance().getConfigDir().resolve(PlayerLogger.modId); } - public File getLogFile() { - return this.logFile; + public static PlayerLogger getInstance() { + return PlayerLogger.instance; } - private void loadWebhook(File logDir) { - File webhookUrlFile = new File(logDir, "webhook-url.txt"); - String webhookUrl = ""; + public MinecraftServer getServer() { + return this.server; + } - try { - if (!webhookUrlFile.exists()) { - webhookUrlFile.createNewFile(); - } - webhookUrl = Files.readString(webhookUrlFile.toPath()); - } catch (IOException e) { - System.out.println("Failed to load webhook url: " + e.getMessage()); - } + public FloodgateIntegration getFloodgateIntegration() { + return this.floodgateIntegration; + } - if (!Objects.equals(webhookUrl, "")) { - try { - this.webhookClient = WebhookClient.withUrl(webhookUrl); - } catch (IllegalArgumentException e) { - System.out.println("Failed to load webhook: " + e.getMessage()); - } - } + public IpAssRepository getIpAssRepository() { + return this.ipAssDataRepository; + } + + public NotifRepository getNotifRepository() { + return this.notifRepository; + } + + public LastReadNotifRepository getLastReadNotifRepository() { + return this.lastReadNotifRepository; + } + + public Config getConfig() { + return this.config; } } diff --git a/src/main/java/com/xadale/playerlogger/Utils.java b/src/main/java/com/xadale/playerlogger/Utils.java new file mode 100644 index 0000000..0aae565 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/Utils.java @@ -0,0 +1,77 @@ +package com.xadale.playerlogger; + +import com.google.gson.Gson; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.yggdrasil.ProfileResult; +import com.xadale.playerlogger.core.DataRepository; +import com.xadale.playerlogger.data.IpAss; +import com.xadale.playerlogger.repositories.IpAssRepository; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class Utils { + + private static final Gson gson = new Gson(); + + public static String getPlayerName(UUID uuid) { + if (PlayerLogger.getInstance().getFloodgateIntegration().isBedrock(uuid)) { + return PlayerLogger.getInstance().getFloodgateIntegration().getUsername(uuid); + } + ProfileResult result = + PlayerLogger.getInstance().getServer().getSessionService().fetchProfile(uuid, false); + if (result == null) { + return "unknown"; + } + return result.profile().getName(); + } + + public static GameProfile fetchProfile(String name) { + if (PlayerLogger.getInstance().getFloodgateIntegration().isFloodGateName(name)) { + return PlayerLogger.getInstance().getFloodgateIntegration().getGameProfileFor(name); + } + try { + return fetchProfileData("https://api.mojang.com/users/profiles/minecraft/" + name); + } catch (IOException ignored) { + } + try { + return fetchProfileData("https://api.minetools.eu/uuid/" + name); + } catch (IOException e) { + return null; + } + } + + private static GameProfile fetchProfileData(String urlLink) throws IOException { + URL url = URI.create(urlLink).toURL(); + URLConnection connection = url.openConnection(); + connection.addRequestProperty("User-Agent", "DiscordJS"); + connection.addRequestProperty("Accept", "application/json"); + connection.connect(); + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String data = reader.lines().collect(Collectors.joining()); + if (data.endsWith("\"ERR\"}")) { + return null; + } + // fix uuid format + String fixed = + data.replaceFirst( + "\"(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)\"", + "$1-$2-$3-$4-$5"); + return gson.fromJson(fixed, GameProfile.class); + } + + public static Set getIpsOfUuid(IpAssRepository ipAssDataRepository, UUID uuid) { + return ipAssDataRepository + .getAll() + .filter(ipAss -> ipAss.getUuids().contains(uuid)) + .map(IpAss::getIp) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/xadale/playerlogger/commands/HandleAltsCommand.java b/src/main/java/com/xadale/playerlogger/commands/HandleAltsCommand.java index 8c39f09..2c4212e 100644 --- a/src/main/java/com/xadale/playerlogger/commands/HandleAltsCommand.java +++ b/src/main/java/com/xadale/playerlogger/commands/HandleAltsCommand.java @@ -1,40 +1,33 @@ package com.xadale.playerlogger.commands; +import com.mojang.authlib.GameProfile; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; +import com.xadale.playerlogger.PlayerLogger; +import com.xadale.playerlogger.Utils; +import com.xadale.playerlogger.data.IpAss; +import com.xadale.playerlogger.repositories.IpAssRepository; +import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; - +import java.util.stream.Collectors; import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; public class HandleAltsCommand { private static final Pattern ipPattern = Pattern.compile("^(\\d{1,3}\\.){3}\\d{1,3}$"); - public static int execute(CommandContext context, File logFile) { + public static int execute( + CommandContext context, IpAssRepository ipAssDataRepository) { String query = StringArgumentType.getString(context, "query").trim(); - Map> ipToPlayers = new HashMap<>(); - Map> playerToIps = new HashMap<>(); final ServerCommandSource source = context.getSource(); - try { - HandleAltsCommand.readLogFile(ipToPlayers, playerToIps, logFile); - } catch (IOException e) { - context.getSource().sendError(Text.literal("§cFailed to read log file: " + e.getMessage())); - return 0; - } - if ((query.contains(".") && !Permissions.check(source, "altx.viewips", 4))) { context .getSource() @@ -47,8 +40,10 @@ public static int execute(CommandContext context, File logF Matcher m = ipPattern.matcher(query); if (m.matches()) { // Query is an IP address - Set players = ipToPlayers.get(query); - if (players != null) { + Optional ipAss = ipAssDataRepository.get(query); + if (ipAss.isPresent()) { + Set players = + ipAss.get().getUuids().stream().map(Utils::getPlayerName).collect(Collectors.toSet()); context .getSource() .sendFeedback( @@ -65,8 +60,23 @@ public static int execute(CommandContext context, File logF } // Query is a username - Set ips = playerToIps.get(query); - if (ips != null) { + ServerPlayerEntity player = + PlayerLogger.getInstance().getServer().getPlayerManager().getPlayer(query); + UUID uuid = null; + if (player == null) { + GameProfile profile = Utils.fetchProfile(query); + if (profile != null) { + uuid = profile.getId(); + } + } else { + uuid = player.getUuid(); + } + if (uuid == null) { + context.getSource().sendFeedback(() -> Text.literal("§cPlayer not found: " + query), false); + return 1; + } + Set ips = Utils.getIpsOfUuid(ipAssDataRepository, uuid); + if (!ips.isEmpty()) { StringBuilder response = new StringBuilder(); if (Permissions.check(source, "altx.viewips", 4)) { response @@ -78,7 +88,10 @@ public static int execute(CommandContext context, File logF response.append("§bPlayers with the same IP as §3").append(query).append("§b:"); } for (String ip : ips) { - Set players = ipToPlayers.get(ip); + Set players = + ipAssDataRepository.get(ip).get().getUuids().stream() + .map(Utils::getPlayerName) + .collect(Collectors.toSet()); response.append("\n"); if (Permissions.check(source, "altx.viewips", 4)) { @@ -96,25 +109,4 @@ public static int execute(CommandContext context, File logF } return 1; } - - private static void readLogFile( - Map> ipToPlayers, Map> playerToIps, File logFile) - throws IOException { - BufferedReader reader = new BufferedReader(new FileReader(logFile)); - - String line; - while ((line = reader.readLine()) != null) { - String[] parts = line.split(";"); - if (parts.length == 2) { - String playerName = parts[0].trim(); - String ipAddress = parts[1].trim(); - - ipToPlayers.computeIfAbsent(ipAddress, k -> new HashSet<>()).add(playerName); - - playerToIps.computeIfAbsent(playerName, k -> new HashSet<>()).add(ipAddress); - } - } - - reader.close(); - } } diff --git a/src/main/java/com/xadale/playerlogger/commands/ListIpsWithMultiplePlayers.java b/src/main/java/com/xadale/playerlogger/commands/ListIpsWithMultiplePlayers.java index 313e1f1..e1d6058 100644 --- a/src/main/java/com/xadale/playerlogger/commands/ListIpsWithMultiplePlayers.java +++ b/src/main/java/com/xadale/playerlogger/commands/ListIpsWithMultiplePlayers.java @@ -1,72 +1,54 @@ package com.xadale.playerlogger.commands; import com.mojang.brigadier.context.CommandContext; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import com.xadale.playerlogger.Utils; +import com.xadale.playerlogger.data.IpAss; +import com.xadale.playerlogger.repositories.IpAssRepository; +import java.util.List; +import java.util.stream.Collectors; import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.Text; public class ListIpsWithMultiplePlayers { - public static int execute(CommandContext context, File logFile) { - Map> ipToPlayers = new HashMap<>(); + public static int execute( + CommandContext context, IpAssRepository ipAssDataRepository) { - // check if show ips final ServerCommandSource source = context.getSource(); - try { - ListIpsWithMultiplePlayers.readLogFile(logFile, ipToPlayers); - } catch (IOException e) { - context.getSource().sendError(Text.literal("§cFailed to read log file: " + e.getMessage())); - return 0; - } + StringBuilder response = new StringBuilder(); - // Filter and build the result - StringBuilder response = new StringBuilder("§bIPs with two or more users:"); - boolean found = false; + List multiAccountIps = + ipAssDataRepository.getAll().filter(ipAss -> ipAss.getUuids().size() >= 2).toList(); - for (Map.Entry> entry : ipToPlayers.entrySet()) { - if (entry.getValue().size() >= 2) { - found = true; - response.append("\n"); - if (Permissions.check(source, "altx.viewips", 4)) { - response.append("§3- (§b").append(entry.getKey()).append("§3): §f"); - } else { - response.append("§3- §f"); - } - response.append(String.join(", ", entry.getValue())); - } + if (multiAccountIps.isEmpty()) { + response.append("§cNo IPs with two or more players found."); + } else { + multiAccountIps.forEach( + ipAss -> { + response.append("\n"); + + if (Permissions.check(source, "altx.viewips", 4)) { + response.append("§3- (§b").append(ipAss.getIp()).append("§3): §f"); + } else { + response.append("§3- §f"); + } + + String playerNames = + ipAss.getUuids().stream() + .map(Utils::getPlayerName) + .collect(Collectors.joining(", ")); + + response.append(playerNames); + }); } - if (!found) { - response.append("§cNo IPs with two or more players found."); + if (!multiAccountIps.isEmpty()) { + response.insert(0, "§bIPs with two or more users:"); } - // Send the response - context.getSource().sendFeedback(() -> Text.literal(response.toString()), false); + source.sendFeedback(() -> Text.literal(response.toString()), false); return 1; } - - private static void readLogFile(File logFile, Map> ipToPlayers) - throws IOException { - // Load the IP-to-players map from the log file - BufferedReader reader = new BufferedReader(new FileReader(logFile)); - String line; - while ((line = reader.readLine()) != null) { - String[] parts = line.split(";"); - if (parts.length == 2) { - String playerName = parts[0].trim(); - String ipAddress = parts[1].trim(); - - ipToPlayers.computeIfAbsent(ipAddress, k -> new HashSet<>()).add(playerName); - } - } - } } diff --git a/src/main/java/com/xadale/playerlogger/compat/FloodgateIntegration.java b/src/main/java/com/xadale/playerlogger/compat/FloodgateIntegration.java new file mode 100644 index 0000000..6ec368a --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/compat/FloodgateIntegration.java @@ -0,0 +1,50 @@ +package com.xadale.playerlogger.compat; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import org.geysermc.floodgate.api.FloodgateApi; + +import com.mojang.authlib.GameProfile; + +import net.fabricmc.loader.api.FabricLoader; + +public class FloodgateIntegration { + private FloodgateApi floodgateApi; + + public FloodgateIntegration() { + if (FabricLoader.getInstance().isModLoaded("floodgate")) { + this.floodgateApi = FloodgateApi.getInstance(); + } + } + + public boolean isBedrock(UUID uuid) { + return this.floodgateApi != null && this.floodgateApi.isFloodgateId(uuid); + } + + public String getUsername(UUID uuid) { + if (this.floodgateApi != null) { + try { + return this.floodgateApi.getPlayerPrefix() + this.floodgateApi.getGamertagFor(uuid.getLeastSignificantBits()).get(); + } catch (InterruptedException | ExecutionException ignored) { + } + } + return "unknown"; + } + + public boolean isFloodGateName(String name) { + return this.floodgateApi != null && name.startsWith(this.floodgateApi.getPlayerPrefix()); + } + + public GameProfile getGameProfileFor(String name) { + String gamerTag = name.replaceFirst(this.floodgateApi.getPlayerPrefix(), ""); + try { + UUID id = this.floodgateApi.getUuidFor(gamerTag).get(); + String username = this.getUsername(id); + return new GameProfile(id, username); + } catch (InterruptedException | ExecutionException ignored) { + } + return null; + } + +} diff --git a/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java b/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java new file mode 100644 index 0000000..202f0da --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java @@ -0,0 +1,62 @@ +package com.xadale.playerlogger.compat; + +import com.xadale.playerlogger.PlayerLogger; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import net.fabricmc.loader.api.FabricLoader; +import tronka.justsync.JustSyncApplication; +import tronka.justsync.linking.PlayerData; +import tronka.justsync.linking.PlayerLink; + +/** Provides integration with Discord: JustSync mod for player UUID linking. */ +public class JustSyncIntegration { + private JustSyncApplication integration; + private static JustSyncIntegration instance; + private static boolean loaded = false; + + /** Initializes integration if Discord: JustSync is loaded and enabled in config. */ + private JustSyncIntegration() { + if (FabricLoader.getInstance().isModLoaded("discordjustsync") + && PlayerLogger.getInstance().getConfig().discordJustSyncIntegration.enable) { + if (JustSyncApplication.getInstance() != null) { + this.integration = JustSyncApplication.getInstance(); + } + } + JustSyncIntegration.instance = this; + } + + /** + * @return Singleton instance of this integration handler + */ + public static JustSyncIntegration getIntegration() { + if (!JustSyncIntegration.loaded) { + JustSyncIntegration.instance = new JustSyncIntegration(); + JustSyncIntegration.loaded = true; + } + return JustSyncIntegration.instance; + } + + /** + * @param uuid Player UUID to check + * @return List of all related UUIDs (main + alts) from JustSync's linking system + */ + public List getRelatedUuids(UUID uuid) { + if (this.integration != null && this.integration.getConfig().linking.enableLinking) { + Optional playerLink = this.integration.getLinkManager().getDataOf(uuid); + if (playerLink.isPresent()) { + // TODO: once implemented change to: + // return playerLink.get().getAllUuids(); + List uuids = + playerLink.get().getAlts().stream() + .map(PlayerData::getId) + .collect(Collectors.toList()); + uuids.add(playerLink.get().getPlayerId()); + return uuids; + } + } + + return List.of(uuid); + } +} diff --git a/src/main/java/com/xadale/playerlogger/config/Config.java b/src/main/java/com/xadale/playerlogger/config/Config.java new file mode 100644 index 0000000..b2e4d48 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/config/Config.java @@ -0,0 +1,71 @@ +package com.xadale.playerlogger.config; + +import com.moandjiezana.toml.Toml; +import com.moandjiezana.toml.TomlComment; +import com.moandjiezana.toml.TomlWriter; +import com.xadale.playerlogger.PlayerLogger; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class Config { + + public static Config loadConfig() { + Path configDir = PlayerLogger.getConfigFolder(); + File configFile = configDir.resolve(PlayerLogger.modId + ".toml").toFile(); + Config instance; + if (configFile.exists()) { + instance = new Toml().read(configFile).to(Config.class); + } else { + instance = new Config(); + } + upgradeConfig(instance); + try { + Files.createDirectories(configDir); + new TomlWriter().write(instance, configFile); + } catch (IOException ignored) { + } + return instance; + } + + // config migration if needed + private static void upgradeConfig(Config instance) { + ; + } + + public AltNotifs altNotifs = new AltNotifs(); + @TomlComment({"------------------------------", + "", + "------------------------------"}) + public DiscordJustSyncIntegration discordJustSyncIntegration = new DiscordJustSyncIntegration(); + + public static class DiscordJustSyncIntegration { + @TomlComment({ + "check if account on same ip as other account is linked as alt", + "only relevant if alt notifs are enabled and Discord: JustSync is installed and the alt" + + " feature is in use" + }) + public boolean enable = false; + } + + public static class AltNotifs { + @TomlComment({ + "get a notification in chat when an account", + "joins with the same ip as another", + "players to notify are determined by 'altx.notify' permission or op level 4" + }) + public boolean enableNotifs = true; + + @TomlComment("show notifs issued when player was offline") + public boolean showMissedNotifsOnJoin = true; + + @TomlComment("oldest notification to show in hours") + public int notificationPeriod = 24; + + @TomlComment( + "period after which notifications will only be accessible through the respective file not" + + " ingame") + public int purgePeriod = 72; + } +} diff --git a/src/main/java/com/xadale/playerlogger/core/AbstractData.java b/src/main/java/com/xadale/playerlogger/core/AbstractData.java new file mode 100644 index 0000000..50f5201 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/core/AbstractData.java @@ -0,0 +1,23 @@ +package com.xadale.playerlogger.core; + +public abstract class AbstractData, ID> { + private transient DataRepository dataRepository; + + /** + * Returns the current Repository instance associated with this IpAss object. + * + * @return The Repository instance used for data operations + */ + public DataRepository getRepository() { + return this.dataRepository; + } + + /** + * Sets the Repository instance to be associated with this IpAss object. + * + * @param dataRepository The Repository instance to be used for data operations + */ + public void setRepository(DataRepository dataRepository) { + this.dataRepository = dataRepository; + } +} diff --git a/src/main/java/com/xadale/playerlogger/core/DataRepository.java b/src/main/java/com/xadale/playerlogger/core/DataRepository.java new file mode 100644 index 0000000..60edf03 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/core/DataRepository.java @@ -0,0 +1,17 @@ +package com.xadale.playerlogger.core; + +import java.util.Optional; +import java.util.stream.Stream; + +public interface DataRepository, ID> { + + Optional get(ID identifier); + + void add(T data); + + void remove(T data); + + void update(T data); + + Stream getAll(); +} diff --git a/src/main/java/com/xadale/playerlogger/core/LocalDateTimeAdapter.java b/src/main/java/com/xadale/playerlogger/core/LocalDateTimeAdapter.java new file mode 100644 index 0000000..110b42e --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/core/LocalDateTimeAdapter.java @@ -0,0 +1,31 @@ +package com.xadale.playerlogger.core; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeAdapter + implements JsonSerializer, JsonDeserializer { + + private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + @Override + public JsonElement serialize( + LocalDateTime src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.format(formatter)); + } + + @Override + public LocalDateTime deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return LocalDateTime.parse(json.getAsString(), formatter); + } +} diff --git a/src/main/java/com/xadale/playerlogger/data/IpAss.java b/src/main/java/com/xadale/playerlogger/data/IpAss.java new file mode 100644 index 0000000..1749303 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/data/IpAss.java @@ -0,0 +1,69 @@ +package com.xadale.playerlogger.data; + +import com.xadale.playerlogger.core.AbstractData; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Represents an IP address association with one or more UUIDs. + * + *

The IpAss (IP Association) class maintains a relationship between a single IP address and + * multiple unique identifiers (UUIDs), typically used to track associations between network + * addresses and entities. The class includes mechanisms to: + * + *

    + *
  • Store and manage UUIDs associated with an IP address + *
  • Prevent duplicate UUID entries + *
  • Sync changes through an associated Repository + *
+ */ +public class IpAss extends AbstractData { + + private String ip; + private List uuids; + + /** + * Constructs an IpAss object with the specified IP address and initial UUID. The UUID list is + * initialized with the provided UUID as its first entry. + * + * @param ip The IP address to associate with this object + * @param uuid The initial UUID to add to the association list + */ + public IpAss(String ip, UUID uuid) { + this.ip = ip; + this.uuids = new ArrayList<>(); + this.uuids.add(uuid); + } + + /** + * Returns the IP address associated with this IpAss object. + * + * @return The IP address string + */ + public String getIp() { + return ip; + } + + /** + * Returns a list of UUIDs associated with the IP address. + * + * @return A List containing all associated UUIDs + */ + public List getUuids() { + return uuids; + } + + /** + * Adds a UUID to the association list if it doesn't already exist, and triggers a data update + * through the associated Repository. + * + * @param uuid The UUID to add to the association list + */ + public void addUUid(UUID uuid) { + if (!this.uuids.contains(uuid)) { + this.uuids.add(uuid); + } + this.getRepository().update(this); + } +} diff --git a/src/main/java/com/xadale/playerlogger/data/LastReadNotif.java b/src/main/java/com/xadale/playerlogger/data/LastReadNotif.java new file mode 100644 index 0000000..85806be --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/data/LastReadNotif.java @@ -0,0 +1,30 @@ +package com.xadale.playerlogger.data; + +import com.xadale.playerlogger.core.AbstractData; +import java.util.UUID; + +public class LastReadNotif extends AbstractData { + + private UUID uuid; + private int lastNotifId; + + public LastReadNotif() {} + + public LastReadNotif(UUID uuid, int lastNotifId) { + this.uuid = uuid; + this.lastNotifId = lastNotifId; + } + + public UUID getUuid() { + return uuid; + } + + public int getLastNotifId() { + return lastNotifId; + } + + public void setLastNotifId(int lastNotifId) { + this.lastNotifId = lastNotifId; + this.getRepository().update(this); + } +} diff --git a/src/main/java/com/xadale/playerlogger/data/Notif.java b/src/main/java/com/xadale/playerlogger/data/Notif.java new file mode 100644 index 0000000..66a10c1 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/data/Notif.java @@ -0,0 +1,37 @@ +package com.xadale.playerlogger.data; + +import com.xadale.playerlogger.core.AbstractData; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public class Notif extends AbstractData { + + private int id; + private UUID uuid; + private List alts; + private LocalDateTime time; + + public Notif(int id, UUID uuid, List alts, LocalDateTime time) { + this.id = id; + this.uuid = uuid; + this.alts = alts; + this.time = time; + } + + public int getId() { + return id; + } + + public UUID getUuid() { + return uuid; + } + + public List getAlts() { + return alts; + } + + public LocalDateTime getTime() { + return time; + } +} diff --git a/src/main/java/com/xadale/playerlogger/mixin/PlayerManagerMixin.java b/src/main/java/com/xadale/playerlogger/mixin/PlayerManagerMixin.java new file mode 100644 index 0000000..37ed59a --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/mixin/PlayerManagerMixin.java @@ -0,0 +1,41 @@ +package com.xadale.playerlogger.mixin; + +import com.mojang.authlib.GameProfile; +import com.mojang.logging.LogUtils; +import com.xadale.playerlogger.IpLogger; +import java.net.InetSocketAddress; +import java.util.UUID; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.network.ServerLoginNetworkHandler; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = ServerLoginNetworkHandler.class, priority = 100) +public class PlayerManagerMixin { + + @Final @Shadow ClientConnection connection; + + @Inject(method = "tickVerify", at = @At("HEAD")) + private void canJoin(GameProfile profile, CallbackInfo ci) { + UUID uuid = profile.getId(); + String ip = null; + try { + if (this.connection.getAddress() instanceof InetSocketAddress inetAddr) { + ip = inetAddr.getAddress().getHostAddress(); + } + } catch (Exception e) { + LogUtils.getLogger().info("ip gathering not successful"); + return; + } + + if (ip == null) { + return; + } + + IpLogger.handleJoin(ip, uuid); + } +} diff --git a/src/main/java/com/xadale/playerlogger/repositories/IpAssRepository.java b/src/main/java/com/xadale/playerlogger/repositories/IpAssRepository.java new file mode 100644 index 0000000..0a81dc1 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/repositories/IpAssRepository.java @@ -0,0 +1,6 @@ +package com.xadale.playerlogger.repositories; + +import com.xadale.playerlogger.core.DataRepository; +import com.xadale.playerlogger.data.IpAss; + +public interface IpAssRepository extends DataRepository {} diff --git a/src/main/java/com/xadale/playerlogger/repositories/JsonIpAssRepositoryImpl.java b/src/main/java/com/xadale/playerlogger/repositories/JsonIpAssRepositoryImpl.java new file mode 100644 index 0000000..cf1728d --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/repositories/JsonIpAssRepositoryImpl.java @@ -0,0 +1,86 @@ +package com.xadale.playerlogger.repositories; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; +import com.xadale.playerlogger.data.IpAss; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.slf4j.Logger; + +public class JsonIpAssRepositoryImpl implements IpAssRepository { + + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private static final Logger LOGGER = LogUtils.getLogger(); + private final File file; + private List data; + + private JsonIpAssRepositoryImpl(File file) { + this.file = file; + if (!file.exists()) { + this.data = new ArrayList<>(); + return; + } + try (FileReader reader = new FileReader(file)) { + this.data = gson.fromJson(reader, new TypeToken>() {}); + } catch (JsonSyntaxException ex) { + throw new RuntimeException("Cannot parse data in %s".formatted(file.getAbsolutePath()), ex); + } catch (Exception e) { + throw new RuntimeException("Cannot load data (%s)".formatted(file.getAbsolutePath()), e); + } + if (this.data == null) { + // gson failed to parse a valid list + this.data = new ArrayList<>(); + } + this.data.forEach(data -> data.setRepository(this)); + System.out.println("Loaded " + this.data.size() + " data entries"); + } + + public static IpAssRepository from(File file) { + return new JsonIpAssRepositoryImpl(file); + } + + @Override + public Optional get(String ip) { + return this.data.stream().filter(link -> ip.equals(link.getIp())).findFirst(); + } + + @Override + public void add(IpAss ipAss) { + this.data.add(ipAss); + ipAss.setRepository(this); + this.onUpdated(); + } + + @Override + public void remove(IpAss ipAss) { + this.data.remove(ipAss); + this.onUpdated(); + } + + @Override + public void update(IpAss ipAss) { + this.onUpdated(); + } + + private void onUpdated() { + try (FileWriter writer = new FileWriter(this.file)) { + gson.toJson(this.data, writer); + } catch (IOException e) { + LOGGER.error("Failed to save data to file {}", this.file.getAbsolutePath(), e); + } + } + + @Override + public Stream getAll() { + return this.data.stream(); + } +} diff --git a/src/main/java/com/xadale/playerlogger/repositories/JsonLastReadNotifRepositoryImpl.java b/src/main/java/com/xadale/playerlogger/repositories/JsonLastReadNotifRepositoryImpl.java new file mode 100644 index 0000000..10d28e8 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/repositories/JsonLastReadNotifRepositoryImpl.java @@ -0,0 +1,87 @@ +package com.xadale.playerlogger.repositories; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; +import com.xadale.playerlogger.data.LastReadNotif; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import org.slf4j.Logger; + +public class JsonLastReadNotifRepositoryImpl implements LastReadNotifRepository { + + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private static final Logger LOGGER = LogUtils.getLogger(); + private final File file; + private List data; + + private JsonLastReadNotifRepositoryImpl(File file) { + this.file = file; + if (!file.exists()) { + this.data = new ArrayList<>(); + return; + } + try (FileReader reader = new FileReader(file)) { + this.data = gson.fromJson(reader, new TypeToken>() {}); + } catch (JsonSyntaxException ex) { + throw new RuntimeException("Cannot parse data in %s".formatted(file.getAbsolutePath()), ex); + } catch (Exception e) { + throw new RuntimeException("Cannot load data (%s)".formatted(file.getAbsolutePath()), e); + } + if (this.data == null) { + // gson failed to parse a valid list + this.data = new ArrayList<>(); + } + this.data.forEach(data -> data.setRepository(this)); + System.out.println("Loaded " + this.data.size() + " last read notification entries"); + } + + public static LastReadNotifRepository from(File file) { + return new JsonLastReadNotifRepositoryImpl(file); + } + + @Override + public Optional get(UUID uuid) { + return this.data.stream().filter(notif -> uuid.equals(notif.getUuid())).findFirst(); + } + + @Override + public void add(LastReadNotif notif) { + this.data.add(notif); + notif.setRepository(this); + this.onUpdated(); + } + + @Override + public void remove(LastReadNotif notif) { + this.data.remove(notif); + this.onUpdated(); + } + + @Override + public void update(LastReadNotif notif) { + this.onUpdated(); + } + + private void onUpdated() { + try (FileWriter writer = new FileWriter(this.file)) { + gson.toJson(this.data, writer); + } catch (IOException e) { + LOGGER.error("Failed to save data to file {}", this.file.getAbsolutePath(), e); + } + } + + @Override + public Stream getAll() { + return this.data.stream(); + } +} diff --git a/src/main/java/com/xadale/playerlogger/repositories/JsonNotifRepositoryImpl.java b/src/main/java/com/xadale/playerlogger/repositories/JsonNotifRepositoryImpl.java new file mode 100644 index 0000000..7651ae4 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/repositories/JsonNotifRepositoryImpl.java @@ -0,0 +1,92 @@ +package com.xadale.playerlogger.repositories; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; +import com.xadale.playerlogger.core.LocalDateTimeAdapter; +import com.xadale.playerlogger.data.Notif; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.slf4j.Logger; + +public class JsonNotifRepositoryImpl implements NotifRepository { + + private static final Gson gson = + new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()) + .create(); + private static final Logger LOGGER = LogUtils.getLogger(); + private final File file; + private List data; + + private JsonNotifRepositoryImpl(File file) { + this.file = file; + if (!file.exists()) { + this.data = new ArrayList<>(); + return; + } + try (FileReader reader = new FileReader(file)) { + this.data = gson.fromJson(reader, new TypeToken>() {}); + } catch (JsonSyntaxException ex) { + throw new RuntimeException("Cannot parse data in %s".formatted(file.getAbsolutePath()), ex); + } catch (Exception e) { + throw new RuntimeException("Cannot load data (%s)".formatted(file.getAbsolutePath()), e); + } + if (this.data == null) { + // gson failed to parse a valid list + this.data = new ArrayList<>(); + } + this.data.forEach(data -> data.setRepository(this)); + System.out.println("Loaded " + this.data.size() + " notification entries"); + } + + public static NotifRepository from(File file) { + return new JsonNotifRepositoryImpl(file); + } + + @Override + public Optional get(Integer id) { + return this.data.stream().filter(notif -> id.equals(notif.getId())).findFirst(); + } + + @Override + public void add(Notif notif) { + this.data.add(notif); + notif.setRepository(this); + this.onUpdated(); + } + + @Override + public void remove(Notif notif) { + this.data.remove(notif); + this.onUpdated(); + } + + @Override + public void update(Notif notif) { + this.onUpdated(); + } + + private void onUpdated() { + try (FileWriter writer = new FileWriter(this.file)) { + gson.toJson(this.data, writer); + } catch (IOException e) { + LOGGER.error("Failed to save data to file {}", this.file.getAbsolutePath(), e); + } + } + + @Override + public Stream getAll() { + return this.data.stream(); + } +} diff --git a/src/main/java/com/xadale/playerlogger/repositories/LastReadNotifRepository.java b/src/main/java/com/xadale/playerlogger/repositories/LastReadNotifRepository.java new file mode 100644 index 0000000..8b1e492 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/repositories/LastReadNotifRepository.java @@ -0,0 +1,7 @@ +package com.xadale.playerlogger.repositories; + +import com.xadale.playerlogger.core.DataRepository; +import com.xadale.playerlogger.data.LastReadNotif; +import java.util.UUID; + +public interface LastReadNotifRepository extends DataRepository {} diff --git a/src/main/java/com/xadale/playerlogger/repositories/NotifRepository.java b/src/main/java/com/xadale/playerlogger/repositories/NotifRepository.java new file mode 100644 index 0000000..c2a6eaa --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/repositories/NotifRepository.java @@ -0,0 +1,6 @@ +package com.xadale.playerlogger.repositories; + +import com.xadale.playerlogger.core.DataRepository; +import com.xadale.playerlogger.data.Notif; + +public interface NotifRepository extends DataRepository {} diff --git a/src/main/resources/altx.mixins.json b/src/main/resources/altx.mixins.json new file mode 100644 index 0000000..93e110f --- /dev/null +++ b/src/main/resources/altx.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.xadale.playerlogger.mixin", + "compatibilityLevel": "JAVA_21", + "server": [ + "PlayerManagerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index f23fe44..c19796d 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -19,6 +19,9 @@ "com.xadale.playerlogger.PlayerLogger" ] }, + "mixins": [ + "altx.mixins.json" + ], "depends": { "fabricloader": ">=0.16.9", "minecraft": "~1.21.4", From 789e2e9f92919e0192d2af06f1ffb2e696f53e41 Mon Sep 17 00:00:00 2001 From: GoldenKoopa <100142836+GoldenKoopa@users.noreply.github.com> Date: Sat, 3 May 2025 15:24:09 +0200 Subject: [PATCH 2/8] feat: add config reload --- src/main/java/com/xadale/playerlogger/Commands.java | 10 +++++++++- .../java/com/xadale/playerlogger/PlayerLogger.java | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/xadale/playerlogger/Commands.java b/src/main/java/com/xadale/playerlogger/Commands.java index e6a4728..2c4fd92 100644 --- a/src/main/java/com/xadale/playerlogger/Commands.java +++ b/src/main/java/com/xadale/playerlogger/Commands.java @@ -91,7 +91,15 @@ public void register() { .executes( (context) -> ListIpsWithMultiplePlayers.execute( - context, PlayerLogger.getInstance().getIpAssRepository())))); + context, PlayerLogger.getInstance().getIpAssRepository()))) + .then( + CommandManager.literal("reload") + .requires(Permissions.require("altx.reload", 4)) + .executes( + (context) -> { + PlayerLogger.getInstance().reloadConfig(); + return 1; + }))); }); } } diff --git a/src/main/java/com/xadale/playerlogger/PlayerLogger.java b/src/main/java/com/xadale/playerlogger/PlayerLogger.java index 86cb2da..760867d 100644 --- a/src/main/java/com/xadale/playerlogger/PlayerLogger.java +++ b/src/main/java/com/xadale/playerlogger/PlayerLogger.java @@ -1,5 +1,6 @@ package com.xadale.playerlogger; +import com.mojang.logging.LogUtils; import com.xadale.playerlogger.compat.FloodgateIntegration; import com.xadale.playerlogger.config.Config; import com.xadale.playerlogger.repositories.IpAssRepository; @@ -59,6 +60,13 @@ public void onInitialize() { commands.register(); } + public void reloadConfig() { + LogUtils.getLogger().info("Reloading Config"); + Config config = Config.loadConfig(); + this.config = config; + LogUtils.getLogger().info("Config succesfully reloaded"); + } + public static Path getConfigFolder() { return FabricLoader.getInstance().getConfigDir().resolve(PlayerLogger.modId); } From 564280af73105855d08fcc38f59d0f19ba43af2d Mon Sep 17 00:00:00 2001 From: GoldenKoopa <100142836+GoldenKoopa@users.noreply.github.com> Date: Sun, 4 May 2025 22:14:30 +0200 Subject: [PATCH 3/8] feat: add authorized accounts & fix latency issues --- .../com/xadale/playerlogger/Commands.java | 51 +++++--- .../com/xadale/playerlogger/IpLogger.java | 6 +- .../playerlogger/NotificationHandler.java | 31 +++-- .../com/xadale/playerlogger/PlayerLogger.java | 23 +++- .../java/com/xadale/playerlogger/Utils.java | 22 +++- .../commands/AuthorizeAltsCommand.java | 42 +++++++ .../commands/ListIpsWithMultiplePlayers.java | 91 ++++++++++---- .../compat/JustSyncIntegration.java | 15 ++- .../playerlogger/compat/PlayerLinkStub.java | 37 ++++++ .../playerlogger/data/AuthorizedAccounts.java | 18 +++ .../AuthorizedAccountsRepository.java | 7 ++ .../JsonAuthorizedAccountsRepositoryImpl.java | 116 ++++++++++++++++++ .../JsonIpAssRepositoryImpl.java | 3 +- .../JsonLastReadNotifRepositoryImpl.java | 3 +- .../JsonNotifRepositoryImpl.java | 3 +- 15 files changed, 397 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/xadale/playerlogger/commands/AuthorizeAltsCommand.java create mode 100644 src/main/java/com/xadale/playerlogger/compat/PlayerLinkStub.java create mode 100644 src/main/java/com/xadale/playerlogger/data/AuthorizedAccounts.java create mode 100644 src/main/java/com/xadale/playerlogger/repositories/AuthorizedAccountsRepository.java create mode 100644 src/main/java/com/xadale/playerlogger/repositories/implementations/JsonAuthorizedAccountsRepositoryImpl.java rename src/main/java/com/xadale/playerlogger/repositories/{ => implementations}/JsonIpAssRepositoryImpl.java (95%) rename src/main/java/com/xadale/playerlogger/repositories/{ => implementations}/JsonLastReadNotifRepositoryImpl.java (95%) rename src/main/java/com/xadale/playerlogger/repositories/{ => implementations}/JsonNotifRepositoryImpl.java (95%) diff --git a/src/main/java/com/xadale/playerlogger/Commands.java b/src/main/java/com/xadale/playerlogger/Commands.java index 2c4fd92..c2a4127 100644 --- a/src/main/java/com/xadale/playerlogger/Commands.java +++ b/src/main/java/com/xadale/playerlogger/Commands.java @@ -1,8 +1,13 @@ package com.xadale.playerlogger; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.xadale.playerlogger.commands.AuthorizeAltsCommand; import com.xadale.playerlogger.commands.HandleAltsCommand; import com.xadale.playerlogger.commands.ListIpsWithMultiplePlayers; +import java.util.concurrent.CompletableFuture; import me.lucko.fabric.api.permissions.v0.Permissions; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.minecraft.server.command.CommandManager; @@ -59,25 +64,7 @@ public void register() { .requires(Permissions.require("altx.trace", 4)) .then( CommandManager.argument("query", StringArgumentType.string()) - .suggests( - (context, builder) -> { - String partialQuery = - builder.getRemaining(); // Get the current typed string - for (ServerPlayerEntity player : - context - .getSource() - .getServer() - .getPlayerManager() - .getPlayerList()) { - String playerName = player.getName().getString(); - if (playerName - .toLowerCase() - .startsWith(partialQuery.toLowerCase())) { - builder.suggest(playerName); - } - } - return builder.buildFuture(); - }) + .suggests((context, builder) -> extracted(context, builder)) .executes( (context) -> HandleAltsCommand.execute( @@ -99,7 +86,31 @@ public void register() { (context) -> { PlayerLogger.getInstance().reloadConfig(); return 1; - }))); + })) + .then( + CommandManager.literal("authorize") + .requires(Permissions.require("altx.authorize", 4)) + .then( + CommandManager.argument("player1", StringArgumentType.string()) + .suggests(this::extracted) + .then( + CommandManager.argument( + "player2", StringArgumentType.string()) + .suggests(this::extracted) + .executes(AuthorizeAltsCommand::execute))))); }); } + + private CompletableFuture extracted( + CommandContext context, SuggestionsBuilder builder) { + String partialQuery = builder.getRemaining(); // Get the current typed string + for (ServerPlayerEntity player : + context.getSource().getServer().getPlayerManager().getPlayerList()) { + String playerName = player.getName().getString(); + if (playerName.toLowerCase().startsWith(partialQuery.toLowerCase())) { + builder.suggest(playerName); + } + } + return builder.buildFuture(); + } } diff --git a/src/main/java/com/xadale/playerlogger/IpLogger.java b/src/main/java/com/xadale/playerlogger/IpLogger.java index f27ec58..f3ab963 100644 --- a/src/main/java/com/xadale/playerlogger/IpLogger.java +++ b/src/main/java/com/xadale/playerlogger/IpLogger.java @@ -8,10 +8,10 @@ public class IpLogger { public static void handleJoin(String ip, UUID uuid) { - IpAssRepository ipAssdataRepository = PlayerLogger.getInstance().getIpAssRepository(); - Optional ipAss = ipAssdataRepository.get(ip); + IpAssRepository ipAssDataRepository = PlayerLogger.getInstance().getIpAssRepository(); + Optional ipAss = ipAssDataRepository.get(ip); if (ipAss.isEmpty()) { - ipAssdataRepository.add(new IpAss(ip, uuid)); + ipAssDataRepository.add(new IpAss(ip, uuid)); return; } ipAss.get().addUUid(uuid); diff --git a/src/main/java/com/xadale/playerlogger/NotificationHandler.java b/src/main/java/com/xadale/playerlogger/NotificationHandler.java index ef01fa2..4d86a21 100644 --- a/src/main/java/com/xadale/playerlogger/NotificationHandler.java +++ b/src/main/java/com/xadale/playerlogger/NotificationHandler.java @@ -1,14 +1,17 @@ package com.xadale.playerlogger; import com.xadale.playerlogger.compat.JustSyncIntegration; +import com.xadale.playerlogger.data.AuthorizedAccounts; import com.xadale.playerlogger.data.IpAss; import com.xadale.playerlogger.data.LastReadNotif; import com.xadale.playerlogger.data.Notif; +import com.xadale.playerlogger.repositories.AuthorizedAccountsRepository; import com.xadale.playerlogger.repositories.NotifRepository; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -28,8 +31,21 @@ public static void handleNotif(IpAss ipAss, UUID uuid) { } List uuids = ipAss.getUuids(); List linkedUuids = JustSyncIntegration.getIntegration().getRelatedUuids(uuid); - // TODO: filter authorized accounts - List filteredUuids = uuids.stream().filter(u -> !linkedUuids.contains(u)).toList(); + + AuthorizedAccountsRepository authAltsRepo = + PlayerLogger.getInstance().getAuthorizedAccountsRepository(); + Optional authorizedAccountsOptional = authAltsRepo.get(uuid); + List filteredUuids = new ArrayList<>(); + if (authorizedAccountsOptional.isEmpty()) { + filteredUuids = uuids.stream().filter(u -> !linkedUuids.contains(u)).toList(); + } else { + List authorizedAccounts = authorizedAccountsOptional.get().getUuids(); + filteredUuids = + uuids.stream() + .filter(u -> !linkedUuids.contains(u)) + .filter(u -> !authorizedAccounts.contains(u)) + .toList(); + } if (filteredUuids.isEmpty()) { return; } @@ -46,20 +62,19 @@ public static void handleNotif(IpAss ipAss, UUID uuid) { PlayerLogger.getInstance() .getLastReadNotifRepository() .get(player.getUuid()) + // TODO: CHANGEEEEE .get() .setLastNotifId(getLastNotifId() + 1); ; } }); - // File log = - // PlayerLogger.getConfigFolder().resolve(PlayerLogger.modId + - // ".notifications.log").toFile(); - String my_file_name = + + String notificationLog = PlayerLogger.getConfigFolder() .resolve(PlayerLogger.modId + ".notifications.log") .toString(); - try (BufferedWriter output = new BufferedWriter(new FileWriter(my_file_name, true))) { - output.append(LocalDateTime.now().toString() + ' '); + try (BufferedWriter output = new BufferedWriter(new FileWriter(notificationLog, true))) { + output.append(LocalDateTime.now().toString()).append(String.valueOf(' ')); output.append(message); output.newLine(); } catch (IOException ignored) { diff --git a/src/main/java/com/xadale/playerlogger/PlayerLogger.java b/src/main/java/com/xadale/playerlogger/PlayerLogger.java index 760867d..5bf43f6 100644 --- a/src/main/java/com/xadale/playerlogger/PlayerLogger.java +++ b/src/main/java/com/xadale/playerlogger/PlayerLogger.java @@ -3,12 +3,14 @@ import com.mojang.logging.LogUtils; import com.xadale.playerlogger.compat.FloodgateIntegration; import com.xadale.playerlogger.config.Config; +import com.xadale.playerlogger.repositories.AuthorizedAccountsRepository; import com.xadale.playerlogger.repositories.IpAssRepository; -import com.xadale.playerlogger.repositories.JsonIpAssRepositoryImpl; -import com.xadale.playerlogger.repositories.JsonLastReadNotifRepositoryImpl; -import com.xadale.playerlogger.repositories.JsonNotifRepositoryImpl; import com.xadale.playerlogger.repositories.LastReadNotifRepository; import com.xadale.playerlogger.repositories.NotifRepository; +import com.xadale.playerlogger.repositories.implementations.JsonAuthorizedAccountsRepositoryImpl; +import com.xadale.playerlogger.repositories.implementations.JsonIpAssRepositoryImpl; +import com.xadale.playerlogger.repositories.implementations.JsonLastReadNotifRepositoryImpl; +import com.xadale.playerlogger.repositories.implementations.JsonNotifRepositoryImpl; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -28,6 +30,7 @@ public class PlayerLogger implements ModInitializer { private IpAssRepository ipAssDataRepository; private NotifRepository notifRepository; private LastReadNotifRepository lastReadNotifRepository; + private AuthorizedAccountsRepository authorizedAccountsRepository; private Config config = Config.loadConfig(); @Override @@ -43,14 +46,21 @@ public void onInitialize() { PlayerLogger.getConfigFolder().resolve(PlayerLogger.modId + ".notifs.json").toFile(); File lastReadNotifFile = PlayerLogger.getConfigFolder().resolve(PlayerLogger.modId + ".lastReadNotif.json").toFile(); + File authorizedAccountsFile = + PlayerLogger.getConfigFolder() + .resolve(PlayerLogger.modId + ".authorizedAccounts.json") + .toFile(); try { Files.createDirectories(PlayerLogger.getConfigFolder()); } catch (IOException ignored) { } + this.ipAssDataRepository = JsonIpAssRepositoryImpl.from(ipAssFile); this.notifRepository = JsonNotifRepositoryImpl.from(notifFile); this.lastReadNotifRepository = JsonLastReadNotifRepositoryImpl.from(lastReadNotifFile); + this.authorizedAccountsRepository = + JsonAuthorizedAccountsRepositoryImpl.from(authorizedAccountsFile); ServerPlayConnectionEvents.JOIN.register(NotificationHandler::onJoin); @@ -62,8 +72,7 @@ public void onInitialize() { public void reloadConfig() { LogUtils.getLogger().info("Reloading Config"); - Config config = Config.loadConfig(); - this.config = config; + this.config = Config.loadConfig(); LogUtils.getLogger().info("Config succesfully reloaded"); } @@ -95,6 +104,10 @@ public LastReadNotifRepository getLastReadNotifRepository() { return this.lastReadNotifRepository; } + public AuthorizedAccountsRepository getAuthorizedAccountsRepository() { + return this.authorizedAccountsRepository; + } + public Config getConfig() { return this.config; } diff --git a/src/main/java/com/xadale/playerlogger/Utils.java b/src/main/java/com/xadale/playerlogger/Utils.java index 0aae565..6374526 100644 --- a/src/main/java/com/xadale/playerlogger/Utils.java +++ b/src/main/java/com/xadale/playerlogger/Utils.java @@ -3,33 +3,51 @@ import com.google.gson.Gson; import com.mojang.authlib.GameProfile; import com.mojang.authlib.yggdrasil.ProfileResult; -import com.xadale.playerlogger.core.DataRepository; import com.xadale.playerlogger.data.IpAss; import com.xadale.playerlogger.repositories.IpAssRepository; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URI; import java.net.URL; import java.net.URLConnection; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; public class Utils { private static final Gson gson = new Gson(); + private static final Map uuidNameCache = new ConcurrentHashMap<>(); public static String getPlayerName(UUID uuid) { if (PlayerLogger.getInstance().getFloodgateIntegration().isBedrock(uuid)) { return PlayerLogger.getInstance().getFloodgateIntegration().getUsername(uuid); } + + MinecraftServer server = PlayerLogger.getInstance().getServer(); + ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); + if (player != null) { + String name = player.getGameProfile().getName(); + uuidNameCache.put(uuid, name); + return name; + } + + String cachedName = uuidNameCache.get(uuid); + if (cachedName != null) { + return cachedName; + } + ProfileResult result = PlayerLogger.getInstance().getServer().getSessionService().fetchProfile(uuid, false); if (result == null) { return "unknown"; } + uuidNameCache.put(uuid, result.profile().getName()); return result.profile().getName(); } diff --git a/src/main/java/com/xadale/playerlogger/commands/AuthorizeAltsCommand.java b/src/main/java/com/xadale/playerlogger/commands/AuthorizeAltsCommand.java new file mode 100644 index 0000000..5ec15ed --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/commands/AuthorizeAltsCommand.java @@ -0,0 +1,42 @@ +package com.xadale.playerlogger.commands; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.xadale.playerlogger.PlayerLogger; +import com.xadale.playerlogger.Utils; +import com.xadale.playerlogger.data.AuthorizedAccounts; +import java.util.List; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +public class AuthorizeAltsCommand { + + public static int execute(CommandContext context) { + String playerString1 = StringArgumentType.getString(context, "player1").trim(); + String playerString2 = StringArgumentType.getString(context, "player2").trim(); + + GameProfile profile1 = Utils.fetchProfile(playerString1); + GameProfile profile2 = Utils.fetchProfile(playerString2); + + if (profile1 == null) { + context.getSource().sendFeedback(() -> Text.literal("§cPlayer 1 could not be found"), false); + } + if (profile2 == null) { + context.getSource().sendFeedback(() -> Text.literal("§cPlayer 2 could not be found"), false); + } + if (profile1 == null || profile2 == null) { + return 1; + } + + AuthorizedAccounts authorizedAccounts = + new AuthorizedAccounts(List.of(profile1.getId(), profile2.getId())); + PlayerLogger.getInstance().getAuthorizedAccountsRepository().add(authorizedAccounts); + + context + .getSource() + .sendFeedback(() -> Text.literal("Successfully added authorized accounts"), false); + + return 1; + } +} diff --git a/src/main/java/com/xadale/playerlogger/commands/ListIpsWithMultiplePlayers.java b/src/main/java/com/xadale/playerlogger/commands/ListIpsWithMultiplePlayers.java index e1d6058..757fcc7 100644 --- a/src/main/java/com/xadale/playerlogger/commands/ListIpsWithMultiplePlayers.java +++ b/src/main/java/com/xadale/playerlogger/commands/ListIpsWithMultiplePlayers.java @@ -1,10 +1,19 @@ package com.xadale.playerlogger.commands; import com.mojang.brigadier.context.CommandContext; +import com.xadale.playerlogger.PlayerLogger; import com.xadale.playerlogger.Utils; +import com.xadale.playerlogger.compat.JustSyncIntegration; +import com.xadale.playerlogger.data.AuthorizedAccounts; import com.xadale.playerlogger.data.IpAss; import com.xadale.playerlogger.repositories.IpAssRepository; +import java.util.HashSet; import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; import java.util.stream.Collectors; import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.server.command.ServerCommandSource; @@ -18,37 +27,65 @@ public static int execute( StringBuilder response = new StringBuilder(); - List multiAccountIps = - ipAssDataRepository.getAll().filter(ipAss -> ipAss.getUuids().size() >= 2).toList(); - - if (multiAccountIps.isEmpty()) { - response.append("§cNo IPs with two or more players found."); - } else { - multiAccountIps.forEach( - ipAss -> { - response.append("\n"); - - if (Permissions.check(source, "altx.viewips", 4)) { - response.append("§3- (§b").append(ipAss.getIp()).append("§3): §f"); - } else { - response.append("§3- §f"); - } - - String playerNames = - ipAss.getUuids().stream() - .map(Utils::getPlayerName) - .collect(Collectors.joining(", ")); - - response.append(playerNames); - }); + CompletableFuture.runAsync( + () -> { + List multiAccountIps = + ipAssDataRepository + .getAll() + .filter(ipAss -> ipAss.getUuids().size() >= 2) + .filter(Predicate.not(ListIpsWithMultiplePlayers::isAuthorized)) + .toList(); + + if (multiAccountIps.isEmpty()) { + response.append("§cNo IPs with two or more players found."); + } else { + multiAccountIps.forEach( + ipAss -> { + response.append("\n"); + + if (Permissions.check(source, "altx.viewips", 4)) { + response.append("§3- (§b").append(ipAss.getIp()).append("§3): §f"); + } else { + response.append("§3- §f"); + } + + String playerNames = + ipAss.getUuids().stream() + .map(Utils::getPlayerName) + .collect(Collectors.joining(", ")); + + response.append(playerNames); + }); + } + if (!multiAccountIps.isEmpty()) { + response.insert(0, "§bIPs with two or more users:"); + } + + source.sendFeedback(() -> Text.literal(response.toString()), false); + }); + return 1; + } + + private static boolean isAuthorized(IpAss ipass) { + Set uuidSet = new HashSet<>(); + for (UUID uuid : ipass.getUuids()) { + uuidSet.add(JustSyncIntegration.getIntegration().getPlayerLink(uuid).getUuid()); } - if (!multiAccountIps.isEmpty()) { - response.insert(0, "§bIPs with two or more users:"); + // only registered alts from justsync + if (uuidSet.size() <= 1) { + return true; } - source.sendFeedback(() -> Text.literal(response.toString()), false); + Optional authorizedAccounts = + PlayerLogger.getInstance().getAuthorizedAccountsRepository().get(uuidSet.iterator().next()); - return 1; + // no authorized and multiple accounts on ip + if (authorizedAccounts.isEmpty()) { + return false; + } + + // authorized when all uuids are in authorization object + return new HashSet<>(authorizedAccounts.get().getUuids()).containsAll(uuidSet); } } diff --git a/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java b/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java index 202f0da..d56e4dc 100644 --- a/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java +++ b/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java @@ -18,6 +18,7 @@ public class JustSyncIntegration { /** Initializes integration if Discord: JustSync is loaded and enabled in config. */ private JustSyncIntegration() { + // TODO: change if (FabricLoader.getInstance().isModLoaded("discordjustsync") && PlayerLogger.getInstance().getConfig().discordJustSyncIntegration.enable) { if (JustSyncApplication.getInstance() != null) { @@ -38,6 +39,16 @@ public static JustSyncIntegration getIntegration() { return JustSyncIntegration.instance; } + public PlayerLinkStub getPlayerLink(UUID uuid) { + if (this.integration != null && this.integration.getConfig().linking.enableLinking) { + Optional playerLink = this.integration.getLinkManager().getDataOf(uuid); + if (playerLink.isPresent()) { + return new PlayerLinkStub(playerLink.get()); + } + } + return new PlayerLinkStub(uuid); + } + /** * @param uuid Player UUID to check * @return List of all related UUIDs (main + alts) from JustSync's linking system @@ -49,9 +60,7 @@ public List getRelatedUuids(UUID uuid) { // TODO: once implemented change to: // return playerLink.get().getAllUuids(); List uuids = - playerLink.get().getAlts().stream() - .map(PlayerData::getId) - .collect(Collectors.toList()); + playerLink.get().getAlts().stream().map(PlayerData::getId).collect(Collectors.toList()); uuids.add(playerLink.get().getPlayerId()); return uuids; } diff --git a/src/main/java/com/xadale/playerlogger/compat/PlayerLinkStub.java b/src/main/java/com/xadale/playerlogger/compat/PlayerLinkStub.java new file mode 100644 index 0000000..9d84287 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/compat/PlayerLinkStub.java @@ -0,0 +1,37 @@ +package com.xadale.playerlogger.compat; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import tronka.justsync.linking.PlayerData; +import tronka.justsync.linking.PlayerLink; + +public class PlayerLinkStub { + + private final UUID uuid; + private final List alts; + + public PlayerLinkStub(PlayerLink playerLink) { + this.uuid = playerLink.getPlayerId(); + this.alts = playerLink.getAlts().stream().map(PlayerData::getId).collect(Collectors.toList()); + } + + public PlayerLinkStub(UUID uuid, List altsUuids) { + this.uuid = uuid; + this.alts = altsUuids; + } + + public PlayerLinkStub(UUID uuid) { + this.uuid = uuid; + this.alts = new ArrayList<>(); + } + + public UUID getUuid() { + return uuid; + } + + public List getAlts() { + return alts; + } +} diff --git a/src/main/java/com/xadale/playerlogger/data/AuthorizedAccounts.java b/src/main/java/com/xadale/playerlogger/data/AuthorizedAccounts.java new file mode 100644 index 0000000..4d7ab8f --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/data/AuthorizedAccounts.java @@ -0,0 +1,18 @@ +package com.xadale.playerlogger.data; + +import com.xadale.playerlogger.core.AbstractData; +import java.util.List; +import java.util.UUID; + +public class AuthorizedAccounts extends AbstractData { + + private List uuids; + + public AuthorizedAccounts(List uuids) { + this.uuids = uuids; + } + + public List getUuids() { + return uuids; + } +} diff --git a/src/main/java/com/xadale/playerlogger/repositories/AuthorizedAccountsRepository.java b/src/main/java/com/xadale/playerlogger/repositories/AuthorizedAccountsRepository.java new file mode 100644 index 0000000..9cbc4b0 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/repositories/AuthorizedAccountsRepository.java @@ -0,0 +1,7 @@ +package com.xadale.playerlogger.repositories; + +import com.xadale.playerlogger.core.DataRepository; +import com.xadale.playerlogger.data.AuthorizedAccounts; +import java.util.UUID; + +public interface AuthorizedAccountsRepository extends DataRepository {} diff --git a/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonAuthorizedAccountsRepositoryImpl.java b/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonAuthorizedAccountsRepositoryImpl.java new file mode 100644 index 0000000..3d34855 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonAuthorizedAccountsRepositoryImpl.java @@ -0,0 +1,116 @@ +package com.xadale.playerlogger.repositories.implementations; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; +import com.xadale.playerlogger.data.AuthorizedAccounts; +import com.xadale.playerlogger.repositories.AuthorizedAccountsRepository; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; +import org.slf4j.Logger; + +public class JsonAuthorizedAccountsRepositoryImpl implements AuthorizedAccountsRepository { + + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private static final Logger LOGGER = LogUtils.getLogger(); + private final File file; + private List data; + + private JsonAuthorizedAccountsRepositoryImpl(File file) { + this.file = file; + if (!file.exists()) { + this.data = new ArrayList<>(); + return; + } + try (FileReader reader = new FileReader(file)) { + this.data = gson.fromJson(reader, new TypeToken>() {}); + } catch (JsonSyntaxException ex) { + throw new RuntimeException("Cannot parse data in %s".formatted(file.getAbsolutePath()), ex); + } catch (Exception e) { + throw new RuntimeException("Cannot load data (%s)".formatted(file.getAbsolutePath()), e); + } + if (this.data == null) { + // gson failed to parse a valid list + this.data = new ArrayList<>(); + } + this.data.forEach(data -> data.setRepository(this)); + System.out.println("Loaded " + this.data.size() + " data entries"); + } + + public static AuthorizedAccountsRepository from(File file) { + return new JsonAuthorizedAccountsRepositoryImpl(file); + } + + @Override + public Optional get(UUID uuid) { + return this.data.stream().filter(authAlts -> authAlts.getUuids().contains(uuid)).findFirst(); + } + + @Override + public void add(AuthorizedAccounts authorizedAccounts) { + + List authorizedAccountsList = + authorizedAccounts.getUuids().stream() + .map(this::get) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + // none authorized already + if (authorizedAccountsList.isEmpty()) { + this.data.add(authorizedAccounts); + authorizedAccounts.setRepository(this); + this.onUpdated(); + return; + } + + // all authorized with each other already + if (new HashSet<>(authorizedAccountsList.getFirst().getUuids()) + .containsAll(authorizedAccounts.getUuids())) { + return; + } + + Set uuidSet = new HashSet<>(authorizedAccounts.getUuids()); + for (AuthorizedAccounts authAlts : authorizedAccountsList) { + uuidSet.addAll(authAlts.getUuids()); + this.remove(authAlts); + } + AuthorizedAccounts newAuthorizedAccounts = new AuthorizedAccounts(new ArrayList<>(uuidSet)); + this.add(newAuthorizedAccounts); + } + + @Override + public void remove(AuthorizedAccounts authorizedAccounts) { + this.data.remove(authorizedAccounts); + this.onUpdated(); + } + + @Override + public void update(AuthorizedAccounts authorizedAccounts) { + this.onUpdated(); + } + + private void onUpdated() { + try (FileWriter writer = new FileWriter(this.file)) { + gson.toJson(this.data, writer); + } catch (IOException e) { + LOGGER.error("Failed to save data to file {}", this.file.getAbsolutePath(), e); + } + } + + @Override + public Stream getAll() { + return this.data.stream(); + } +} diff --git a/src/main/java/com/xadale/playerlogger/repositories/JsonIpAssRepositoryImpl.java b/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonIpAssRepositoryImpl.java similarity index 95% rename from src/main/java/com/xadale/playerlogger/repositories/JsonIpAssRepositoryImpl.java rename to src/main/java/com/xadale/playerlogger/repositories/implementations/JsonIpAssRepositoryImpl.java index cf1728d..a03fc06 100644 --- a/src/main/java/com/xadale/playerlogger/repositories/JsonIpAssRepositoryImpl.java +++ b/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonIpAssRepositoryImpl.java @@ -1,4 +1,4 @@ -package com.xadale.playerlogger.repositories; +package com.xadale.playerlogger.repositories.implementations; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -6,6 +6,7 @@ import com.google.gson.reflect.TypeToken; import com.mojang.logging.LogUtils; import com.xadale.playerlogger.data.IpAss; +import com.xadale.playerlogger.repositories.IpAssRepository; import java.io.File; import java.io.FileReader; import java.io.FileWriter; diff --git a/src/main/java/com/xadale/playerlogger/repositories/JsonLastReadNotifRepositoryImpl.java b/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonLastReadNotifRepositoryImpl.java similarity index 95% rename from src/main/java/com/xadale/playerlogger/repositories/JsonLastReadNotifRepositoryImpl.java rename to src/main/java/com/xadale/playerlogger/repositories/implementations/JsonLastReadNotifRepositoryImpl.java index 10d28e8..22d245c 100644 --- a/src/main/java/com/xadale/playerlogger/repositories/JsonLastReadNotifRepositoryImpl.java +++ b/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonLastReadNotifRepositoryImpl.java @@ -1,4 +1,4 @@ -package com.xadale.playerlogger.repositories; +package com.xadale.playerlogger.repositories.implementations; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -6,6 +6,7 @@ import com.google.gson.reflect.TypeToken; import com.mojang.logging.LogUtils; import com.xadale.playerlogger.data.LastReadNotif; +import com.xadale.playerlogger.repositories.LastReadNotifRepository; import java.io.File; import java.io.FileReader; import java.io.FileWriter; diff --git a/src/main/java/com/xadale/playerlogger/repositories/JsonNotifRepositoryImpl.java b/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonNotifRepositoryImpl.java similarity index 95% rename from src/main/java/com/xadale/playerlogger/repositories/JsonNotifRepositoryImpl.java rename to src/main/java/com/xadale/playerlogger/repositories/implementations/JsonNotifRepositoryImpl.java index 7651ae4..82d6c5d 100644 --- a/src/main/java/com/xadale/playerlogger/repositories/JsonNotifRepositoryImpl.java +++ b/src/main/java/com/xadale/playerlogger/repositories/implementations/JsonNotifRepositoryImpl.java @@ -1,4 +1,4 @@ -package com.xadale.playerlogger.repositories; +package com.xadale.playerlogger.repositories.implementations; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -7,6 +7,7 @@ import com.mojang.logging.LogUtils; import com.xadale.playerlogger.core.LocalDateTimeAdapter; import com.xadale.playerlogger.data.Notif; +import com.xadale.playerlogger.repositories.NotifRepository; import java.io.File; import java.io.FileReader; import java.io.FileWriter; From dcd58ccf8aae598ea195888e7191fcf82dfb7d1e Mon Sep 17 00:00:00 2001 From: GoldenKoopa <100142836+GoldenKoopa@users.noreply.github.com> Date: Sat, 21 Jun 2025 17:19:28 +0200 Subject: [PATCH 4/8] feat: add authorized accounts list command --- .../com/xadale/playerlogger/Commands.java | 17 +++++++++---- .../commands/AuthorizeAltsCommand.java | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/xadale/playerlogger/Commands.java b/src/main/java/com/xadale/playerlogger/Commands.java index c2a4127..50e9da2 100644 --- a/src/main/java/com/xadale/playerlogger/Commands.java +++ b/src/main/java/com/xadale/playerlogger/Commands.java @@ -89,15 +89,22 @@ public void register() { })) .then( CommandManager.literal("authorize") - .requires(Permissions.require("altx.authorize", 4)) .then( - CommandManager.argument("player1", StringArgumentType.string()) - .suggests(this::extracted) + CommandManager.literal("add") + .requires(Permissions.require("altx.authorize", 4)) .then( CommandManager.argument( - "player2", StringArgumentType.string()) + "player1", StringArgumentType.string()) .suggests(this::extracted) - .executes(AuthorizeAltsCommand::execute))))); + .then( + CommandManager.argument( + "player2", StringArgumentType.string()) + .suggests(this::extracted) + .executes(AuthorizeAltsCommand::execute)))) + .then( + CommandManager.literal("list") + .requires(Permissions.require("altx.authorize", 4)) + .executes(AuthorizeAltsCommand::listAuthorizedExecute)))); }); } diff --git a/src/main/java/com/xadale/playerlogger/commands/AuthorizeAltsCommand.java b/src/main/java/com/xadale/playerlogger/commands/AuthorizeAltsCommand.java index 5ec15ed..5b238ab 100644 --- a/src/main/java/com/xadale/playerlogger/commands/AuthorizeAltsCommand.java +++ b/src/main/java/com/xadale/playerlogger/commands/AuthorizeAltsCommand.java @@ -7,6 +7,7 @@ import com.xadale.playerlogger.Utils; import com.xadale.playerlogger.data.AuthorizedAccounts; import java.util.List; +import java.util.stream.Collectors; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.Text; @@ -39,4 +40,28 @@ public static int execute(CommandContext context) { return 1; } + + public static int listAuthorizedExecute(CommandContext context) { + List authorizedAccounts = + PlayerLogger.getInstance().getAuthorizedAccountsRepository().getAll().toList(); + + StringBuilder response = new StringBuilder(); + authorizedAccounts.forEach( + auth -> { + response.append("\n"); + response.append("§3- §f"); + String playernames = + auth.getUuids().stream().map(Utils::getPlayerName).collect(Collectors.joining(", ")); + response.append(playernames); + }); + + if (authorizedAccounts.isEmpty()) { + response.append("§cNo authorized accounts found."); + } else { + response.insert(0, "§bAuthorized accounts:"); + } + + context.getSource().sendFeedback(() -> Text.literal(response.toString()), false); + return 1; + } } From cd4154575c51eed1ca84e0c5bce1e1adaeaec082 Mon Sep 17 00:00:00 2001 From: GoldenKoopa <100142836+GoldenKoopa@users.noreply.github.com> Date: Fri, 18 Jul 2025 17:44:44 +0200 Subject: [PATCH 5/8] chore: cleanup leftover TODOs --- .../xadale/playerlogger/NotificationHandler.java | 5 +---- .../playerlogger/compat/JustSyncIntegration.java | 16 ++++++---------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/xadale/playerlogger/NotificationHandler.java b/src/main/java/com/xadale/playerlogger/NotificationHandler.java index 4d86a21..b283d37 100644 --- a/src/main/java/com/xadale/playerlogger/NotificationHandler.java +++ b/src/main/java/com/xadale/playerlogger/NotificationHandler.java @@ -62,10 +62,7 @@ public static void handleNotif(IpAss ipAss, UUID uuid) { PlayerLogger.getInstance() .getLastReadNotifRepository() .get(player.getUuid()) - // TODO: CHANGEEEEE - .get() - .setLastNotifId(getLastNotifId() + 1); - ; + .ifPresent(lastReadNotif -> lastReadNotif.setLastNotifId(getLastNotifId() + 1)); } }); diff --git a/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java b/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java index d56e4dc..f873111 100644 --- a/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java +++ b/src/main/java/com/xadale/playerlogger/compat/JustSyncIntegration.java @@ -4,10 +4,8 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; import net.fabricmc.loader.api.FabricLoader; import tronka.justsync.JustSyncApplication; -import tronka.justsync.linking.PlayerData; import tronka.justsync.linking.PlayerLink; /** Provides integration with Discord: JustSync mod for player UUID linking. */ @@ -18,8 +16,7 @@ public class JustSyncIntegration { /** Initializes integration if Discord: JustSync is loaded and enabled in config. */ private JustSyncIntegration() { - // TODO: change - if (FabricLoader.getInstance().isModLoaded("discordjustsync") + if (FabricLoader.getInstance().isModLoaded("discord-justsync") && PlayerLogger.getInstance().getConfig().discordJustSyncIntegration.enable) { if (JustSyncApplication.getInstance() != null) { this.integration = JustSyncApplication.getInstance(); @@ -39,6 +36,10 @@ public static JustSyncIntegration getIntegration() { return JustSyncIntegration.instance; } + /** + * @param uuid Player uuid + * @returns A stub of the PlayerLink object of justsync + */ public PlayerLinkStub getPlayerLink(UUID uuid) { if (this.integration != null && this.integration.getConfig().linking.enableLinking) { Optional playerLink = this.integration.getLinkManager().getDataOf(uuid); @@ -57,12 +58,7 @@ public List getRelatedUuids(UUID uuid) { if (this.integration != null && this.integration.getConfig().linking.enableLinking) { Optional playerLink = this.integration.getLinkManager().getDataOf(uuid); if (playerLink.isPresent()) { - // TODO: once implemented change to: - // return playerLink.get().getAllUuids(); - List uuids = - playerLink.get().getAlts().stream().map(PlayerData::getId).collect(Collectors.toList()); - uuids.add(playerLink.get().getPlayerId()); - return uuids; + return playerLink.get().getAllUuids(); } } From 6f407007a1288df65d8feb6266e4dd96d5d237f5 Mon Sep 17 00:00:00 2001 From: GoldenKoopa <100142836+GoldenKoopa@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:08:03 +0200 Subject: [PATCH 6/8] fix: reimplement full webhook functionality (0d5aebe) --- .../playerlogger/NotificationHandler.java | 1 + .../playerlogger/WebhookMessageSender.java | 44 +++++++++++++++++++ .../xadale/playerlogger/config/Config.java | 6 +++ 3 files changed, 51 insertions(+) create mode 100644 src/main/java/com/xadale/playerlogger/WebhookMessageSender.java diff --git a/src/main/java/com/xadale/playerlogger/NotificationHandler.java b/src/main/java/com/xadale/playerlogger/NotificationHandler.java index b283d37..70944dc 100644 --- a/src/main/java/com/xadale/playerlogger/NotificationHandler.java +++ b/src/main/java/com/xadale/playerlogger/NotificationHandler.java @@ -50,6 +50,7 @@ public static void handleNotif(IpAss ipAss, UUID uuid) { return; } String message = getMessage(uuid, filteredUuids); + WebhookMessageSender.sendMessage(message); PlayerLogger.getInstance() .getServer() diff --git a/src/main/java/com/xadale/playerlogger/WebhookMessageSender.java b/src/main/java/com/xadale/playerlogger/WebhookMessageSender.java new file mode 100644 index 0000000..6004ee3 --- /dev/null +++ b/src/main/java/com/xadale/playerlogger/WebhookMessageSender.java @@ -0,0 +1,44 @@ +package com.xadale.playerlogger; + +import club.minnced.discord.webhook.WebhookClient; +import club.minnced.discord.webhook.send.WebhookMessage; +import club.minnced.discord.webhook.send.WebhookMessageBuilder; +import com.mojang.logging.LogUtils; + +public class WebhookMessageSender { + + private static WebhookClient webhookClient = setupWebhook(); + private static final String WEBHOOK_URL = + PlayerLogger.getInstance().getConfig().altNotifs.webhookUrl; + private static final String WEBHOOK_NAME = "AltX Notification"; + private static final String WEBHOOK_AVATAR_URL = + "https://raw.githubusercontent.com/OrdinarySMP/AltX/main/assets/AltX-avatar.png"; + + private static WebhookClient setupWebhook() { + if (!PlayerLogger.getInstance().getConfig().altNotifs.enableWebhookNotifs) { + return null; + } + + WebhookClient client = null; + try { + client = WebhookClient.withUrl(WEBHOOK_URL); + } catch (IllegalArgumentException e) { + LogUtils.getLogger().warn("Failed to load webhook: " + e.getMessage()); + } + return client; + } + + public static void sendMessage(String messageString) { + if (webhookClient == null) { + return; + } + WebhookMessage message = + new WebhookMessageBuilder() + .setContent(messageString) + .setUsername(WEBHOOK_NAME) + .setAvatarUrl(WEBHOOK_AVATAR_URL) + .build(); + + webhookClient.send(message); + } +} diff --git a/src/main/java/com/xadale/playerlogger/config/Config.java b/src/main/java/com/xadale/playerlogger/config/Config.java index b2e4d48..81bdb49 100644 --- a/src/main/java/com/xadale/playerlogger/config/Config.java +++ b/src/main/java/com/xadale/playerlogger/config/Config.java @@ -57,6 +57,12 @@ public static class AltNotifs { }) public boolean enableNotifs = true; + @TomlComment("send notifications to a webhook") + public boolean enableWebhookNotifs = false; + + @TomlComment("url of the webhook") + public String webhookUrl = ""; + @TomlComment("show notifs issued when player was offline") public boolean showMissedNotifsOnJoin = true; From f8652d76c31bb6f6e4f499739fb3171aaba07cff Mon Sep 17 00:00:00 2001 From: GoldenKoopa <100142836+GoldenKoopa@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:36:18 +0200 Subject: [PATCH 7/8] fix: webhook reload --- .../com/xadale/playerlogger/PlayerLogger.java | 1 + .../xadale/playerlogger/WebhookMessageSender.java | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/xadale/playerlogger/PlayerLogger.java b/src/main/java/com/xadale/playerlogger/PlayerLogger.java index 5bf43f6..88c7d4d 100644 --- a/src/main/java/com/xadale/playerlogger/PlayerLogger.java +++ b/src/main/java/com/xadale/playerlogger/PlayerLogger.java @@ -73,6 +73,7 @@ public void onInitialize() { public void reloadConfig() { LogUtils.getLogger().info("Reloading Config"); this.config = Config.loadConfig(); + WebhookMessageSender.reload(); LogUtils.getLogger().info("Config succesfully reloaded"); } diff --git a/src/main/java/com/xadale/playerlogger/WebhookMessageSender.java b/src/main/java/com/xadale/playerlogger/WebhookMessageSender.java index 6004ee3..ddce2c2 100644 --- a/src/main/java/com/xadale/playerlogger/WebhookMessageSender.java +++ b/src/main/java/com/xadale/playerlogger/WebhookMessageSender.java @@ -7,25 +7,24 @@ public class WebhookMessageSender { - private static WebhookClient webhookClient = setupWebhook(); - private static final String WEBHOOK_URL = - PlayerLogger.getInstance().getConfig().altNotifs.webhookUrl; + private static WebhookClient webhookClient = null; private static final String WEBHOOK_NAME = "AltX Notification"; private static final String WEBHOOK_AVATAR_URL = "https://raw.githubusercontent.com/OrdinarySMP/AltX/main/assets/AltX-avatar.png"; - private static WebhookClient setupWebhook() { + public static void reload() { if (!PlayerLogger.getInstance().getConfig().altNotifs.enableWebhookNotifs) { - return null; + webhookClient = null; + return; } - WebhookClient client = null; + String webhookUrl = PlayerLogger.getInstance().getConfig().altNotifs.webhookUrl; try { - client = WebhookClient.withUrl(WEBHOOK_URL); + webhookClient = WebhookClient.withUrl(webhookUrl); } catch (IllegalArgumentException e) { + webhookClient = null; LogUtils.getLogger().warn("Failed to load webhook: " + e.getMessage()); } - return client; } public static void sendMessage(String messageString) { From c9c68e6c2aff95bf3b67053d9e3b1c33d36bd590 Mon Sep 17 00:00:00 2001 From: GoldenKoopa <100142836+GoldenKoopa@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:00:54 +0200 Subject: [PATCH 8/8] build: update build and workflow for multi version builds --- .github/workflows/release.yml | 25 ++++++++++- build.gradle | 67 +++++++++++++++++++++++++----- gradle.properties | 12 ++++-- src/main/resources/fabric.mod.json | 59 +++++++++++++------------- 4 files changed, 118 insertions(+), 45 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17d2489..4f5f0f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,8 @@ jobs: distribution: 'microsoft' - name: make gradle wrapper executable run: chmod +x ./gradlew + - name: gradle setup + uses: gradle/actions/setup-gradle@v4 - name: build run: ./gradlew build -Pversion=${{ needs.release-dry.outputs.version }} - name: capture build artifacts @@ -93,6 +95,27 @@ jobs: run: | minecraft_version=$(grep "^minecraft_version=" "gradle.properties" | cut -d'=' -f2) echo "MINECRAFT_VERSION=$minecraft_version" >> $GITHUB_ENV + last_minecraft_version=$(grep "^last_minecraft_version=" "gradle.properties" | cut -d'=' -f2) + echo "LAST_MINECRAFT_VERSION=$last_minecraft_version" >> $GITHUB_ENV + - name: Generate Minecraft versions JSON + run: | + if [[ -z "$LAST_MINECRAFT_VERSION" ]]; then + LAST_MINECRAFT_VERSION=$(curl -s https://piston-meta.mojang.com/mc/game/version_manifest_v2.json | jq -r '.latest.release') + fi + + MAJOR_MINOR=$(echo "$MINECRAFT_VERSION" | cut -d. -f1,2) + START_PATCH=$(echo "$MINECRAFT_VERSION" | cut -d. -f3) + END_PATCH=$(echo "$LAST_MINECRAFT_VERSION" | cut -d. -f3) + + seq $START_PATCH $END_PATCH | awk -v mm="$MAJOR_MINOR" '{print mm "." $1}' | jq -R . | jq -s -c . > versions.json + + - name: Load generated versions into env var + run: | + VERSIONS=$(jq -r '.[]' versions.json | paste -sd '\n' -) + echo "MINECRAFT_VERSIONS<> $GITHUB_ENV + echo "$VERSIONS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + - name: Upload to Modrinth if: steps.semantic-release.outputs.new_release_version != '' uses: cloudnode-pro/modrinth-publish@2.0.0 @@ -105,5 +128,5 @@ jobs: loaders: |- fabric game-versions: |- - ${{ env.MINECRAFT_VERSION }} + ${{ env.MINECRAFT_VERSIONS }} files: build/libs/AltX-fabric-${{ steps.semantic-release.outputs.new_release_version }}.jar diff --git a/build.gradle b/build.gradle index 436cce2..5355229 100644 --- a/build.gradle +++ b/build.gradle @@ -64,30 +64,35 @@ dependencies { modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + include(modImplementation("me.lucko:fabric-permissions-api:${project.fabric_permission_api_version}")) - include(modImplementation("me.lucko:fabric-permissions-api:${project.fabric_permission_api_version}")) - - embed implementation("club.minnced:discord-webhooks:${project.minnced_discord_webhook_version}") - implementation("club.minnced:discord-webhooks:${project.minnced_discord_webhook_version}") + embed implementation("club.minnced:discord-webhooks:${project.minnced_discord_webhook_version}") // config embed implementation("de.erdbeerbaerlp:toml4j:dd60f51") - // floodgate integration - compileOnly('org.geysermc.floodgate:api:2.2.4-SNAPSHOT') - - // justsync integration - compileOnly('com.github.OrdinarySMP:DiscordJustSync:master-SNAPSHOT') + // integrations + compileOnly "org.geysermc.floodgate:api:${project.floodgate_api_version}" + compileOnly "com.github.OrdinarySMP:DiscordJustSync:${project.justsync_version}" } tasks.build.dependsOn(tasks.shadowJar) processResources { - inputs.property "version", project.version + def minecraft_version = project.minecraft_version + if (project.last_minecraft_version != "") { + minecraft_version = minecraft_version + " <=" + project.last_minecraft_version + } + + inputs.property "version", project.version + inputs.property "minecraft_version", minecraft_version + inputs.property "loader_version", project.loader_version filesMatching("fabric.mod.json") { - expand "version": project.version + expand "version": project.version, + "loader_version": project.loader_version, + "minecraft_version": minecraft_version } } @@ -95,6 +100,46 @@ tasks.withType(JavaCompile).configureEach { it.options.release = 21 } +tasks.register("prodServer", net.fabricmc.loom.task.prod.ServerProductionRunTask) { + def version = (project.hasProperty('version') && project.property('version') != 'unspecified') + ? project.property('version'): project.minecraft_version + + def fabricApiMetadataUrl = 'https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/maven-metadata.xml' + + def latestFabricApiVersion = null + try { + def xmlText = new URL(fabricApiMetadataUrl).text + def parsedXml = new XmlSlurper().parseText(xmlText) + def matchingVersions = parsedXml.versioning.versions.version.findAll { + v -> v.text().endsWith("+${version}") + } + + if (!matchingVersions.isEmpty()) { + latestFabricApiVersion = matchingVersions[-1].text() + } + + } catch (Exception e) { + logger.warn("Failed to fetch or parse Fabric API metadata, falling back to default version: $e") + latestFabricApiVersion = project.fabric_version + version = project.minecraft_version + } + + if (latestFabricApiVersion == null) { + throw new GradleException("No Fabric API version found for specified Minecraft version: ${version}") + } + + configurations { + register("runtimeMods") + } + + dependencies.add("runtimeMods", "net.fabricmc.fabric-api:fabric-api:${latestFabricApiVersion}") + mods.from(configurations.named("runtimeMods")) + + installerVersion = "1.0.1" + loaderVersion = project.loader_version + minecraftVersion = version +} + java { // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task // if it is present. diff --git a/gradle.properties b/gradle.properties index 6c5006d..f0b853c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,16 +4,20 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/develop -minecraft_version=1.21.8 -yarn_mappings=1.21.8+build.1 +minecraft_version=1.21.6 +last_minecraft_version= +yarn_mappings=1.21.6+build.1 loader_version=0.16.14 # Mod Properties maven_group=com.xadale archives_base_name=AltX-fabric -# Dependencies -fabric_version=0.129.0+1.21.8 +# Mod Dependencies +fabric_version=0.127.0+1.21.6 fabric_permission_api_version=0.4.0 +floodgate_api_version=2.2.4-SNAPSHOT +justsync_version=master-SNAPSHOT +# Dependencies minnced_discord_webhook_version=0.8.4 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index c19796d..0ea00dc 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,34 +1,35 @@ { - "schemaVersion": 1, - "id": "altx", - "version": "${version}", - "name": "AltX", - "description": "A lightweight Fabric mod that logs player IPs and detects alt accounts.", - "authors": [ - "MrCookiePrincess" - ], - "contact": { - "homepage": "https://github.com/OrdinarySMP", - "sources": "https://github.com/OrdinarySMP/AltX-fabric" - }, - "license": "GPL-3.0-or-later", - "icon": "assets/modid/icon.png", - "environment": "*", - "entrypoints": { - "main": [ - "com.xadale.playerlogger.PlayerLogger" - ] - }, + "schemaVersion": 1, + "id": "altx", + "version": "${version}", + "name": "AltX", + "description": "A lightweight Fabric mod that logs player IPs and detects alt accounts.", + "authors": [ + "MrCookiePrincess" + ], + "contributors": [ + "PadBro", + "GoldenKoopa" + ], + "contact": { + "homepage": "https://github.com/OrdinarySMP", + "sources": "https://github.com/OrdinarySMP/AltX" + }, + "license": "GPL-3.0-or-later", + "icon": "assets/modid/icon.png", + "environment": "server", + "entrypoints": { + "main": [ + "com.xadale.playerlogger.PlayerLogger" + ] + }, "mixins": [ "altx.mixins.json" ], - "depends": { - "fabricloader": ">=0.16.9", - "minecraft": "~1.21.4", - "java": ">=21", - "fabric-api": "*" - }, - "suggests": { - "another-mod": "*" - } + "depends": { + "fabricloader": ">=${loader_version}", + "minecraft": "~${minecraft_version}", + "java": ">=21", + "fabric-api": "*" + } }