From 5e4d4f210ceb145a1133a7908135191df574208c 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] 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 | 51 +++++- gradlew | 0 .../com/xadale/playerlogger/Commands.java | 15 +- .../com/xadale/playerlogger/IpLogger.java | 20 +++ .../playerlogger/NotificationHandler.java | 158 ++++++++++++++++ .../com/xadale/playerlogger/PlayerLogger.java | 169 ++++++++---------- .../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, 1249 insertions(+), 204 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 0cb953b..eb51ff4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { - id 'fabric-loom' version '1.10.1' + id 'fabric-loom' version '1.10-SNAPSHOT' id 'maven-publish' + id 'com.github.johnrengelman.shadow' version "8+" } group = project.maven_group @@ -9,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. @@ -21,7 +39,7 @@ loom { splitEnvironmentSourceSets() mods { - "modid" { + "altx" { sourceSet sourceSets.main sourceSet sourceSets.client } @@ -35,12 +53,23 @@ 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:0.3.3')) + + // 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 @@ -63,12 +92,26 @@ 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()}"} } } +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 027529b..86cb2da 100644 --- a/src/main/java/com/xadale/playerlogger/PlayerLogger.java +++ b/src/main/java/com/xadale/playerlogger/PlayerLogger.java @@ -1,116 +1,93 @@ package com.xadale.playerlogger; -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.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 me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.server.MinecraftServer; public class PlayerLogger implements ModInitializer { - private File logFile; + 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"); - - // 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 message = - "Player " + playerName + " is a potential alt of: " + potentialAlts + "."; - - // Send the message to staff - server - .getPlayerManager() - .getPlayerList() - .forEach( - player -> { - if (Permissions.check(player, "altx.notify", 4)) { - player.sendMessage( - Text.literal(message).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 static PlayerLogger getInstance() { + return PlayerLogger.instance; + } + + public MinecraftServer getServer() { + return this.server; + } + + public FloodgateIntegration getFloodgateIntegration() { + return this.floodgateIntegration; + } + + public IpAssRepository getIpAssRepository() { + return this.ipAssDataRepository; + } + + public NotifRepository getNotifRepository() { + return this.notifRepository; + } + + public LastReadNotifRepository getLastReadNotifRepository() { + return this.lastReadNotifRepository; } - public File getLogFile() { - return this.logFile; + 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",