From 1b4dbc9e51a86a9645267e30854e02cc0e98363a Mon Sep 17 00:00:00 2001 From: M_Viper Date: Thu, 7 May 2026 19:39:29 +0000 Subject: [PATCH] Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java --- .../modules/chat/AccountLinkManager.java | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java diff --git a/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java new file mode 100644 index 0000000..0260d62 --- /dev/null +++ b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java @@ -0,0 +1,318 @@ +package net.viper.status.modules.chat; + +import java.io.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +/** + * Verwaltet die Verknüpfung von Minecraft-Accounts mit Discord/Telegram. + * + * Ablauf: + * 1. Spieler tippt /linkdiscord oder /linktelegram → Token wird generiert + * 2. Spieler schickt Token an den Bot + * 3. Bot-Polling erkennt Token → Verknüpfung wird gespeichert + * + * Speicherformat (chat_links.dat): + * minecraft:|name:|discord:|telegram: + */ +public class AccountLinkManager { + + private final File file; + private final Logger logger; + + // UUID → verknüpfte Accounts + private final ConcurrentHashMap links = new ConcurrentHashMap<>(); + + // Ausstehende Token: token → UUID (läuft nach 10 Min ab) + private final ConcurrentHashMap pendingTokens = new ConcurrentHashMap<>(); + + public AccountLinkManager(File dataFolder, Logger logger) { + this.file = new File(dataFolder, "chat_links.dat"); + this.logger = logger; + } + + // ===== Datenklassen ===== + + public static class LinkedAccount { + public UUID minecraftUUID; + public String minecraftName; + public String discordUserId = ""; // leer = nicht verknüpft + public String telegramUserId = ""; // leer = nicht verknüpft + public String telegramUsername = ""; // @username für Anzeige + public String discordUsername = ""; // für Anzeige + } + + private static class PendingToken { + UUID uuid; + String playerName; + String type; // "discord" oder "telegram" + long expiresAt; // Unix-Millis + + PendingToken(UUID uuid, String playerName, String type) { + this.uuid = uuid; + this.playerName = playerName; + this.type = type; + this.expiresAt = System.currentTimeMillis() + (10 * 60 * 1000L); // 10 Min + } + + boolean isExpired() { + return System.currentTimeMillis() > expiresAt; + } + } + + // ===== Token-Generierung ===== + + /** + * Generiert einen neuen Verknüpfungs-Token für einen Spieler. + * Bestehende Token für denselben Spieler+Typ werden überschrieben. + * + * @param uuid UUID des Spielers + * @param playerName Anzeigename + * @param type "discord" oder "telegram" + * @return 6-stelliger alphanumerischer Token (z.B. "A3F9K2") + */ + public String generateToken(UUID uuid, String playerName, String type) { + // Alte Token für diesen Spieler+Typ entfernen + pendingTokens.entrySet().removeIf(e -> + e.getValue().uuid.equals(uuid) && e.getValue().type.equals(type)); + + // Abgelaufene Token bereinigen + pendingTokens.entrySet().removeIf(e -> e.getValue().isExpired()); + + String token; + do { + token = generateRandomToken(); + } while (pendingTokens.containsKey(token)); + + pendingTokens.put(token, new PendingToken(uuid, playerName, type)); + return token; + } + + private String generateRandomToken() { + String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne I,O,0,1 (Verwechslungsgefahr) + Random rnd = new Random(); + StringBuilder sb = new StringBuilder(6); + for (int i = 0; i < 6; i++) sb.append(chars.charAt(rnd.nextInt(chars.length()))); + return sb.toString(); + } + + // ===== Token einlösen ===== + + /** + * Versucht einen Token einzulösen (aufgerufen wenn Bot eine Nachricht empfängt). + * + * @param token Der eingesendete Token + * @param externalId Discord User-ID oder Telegram User-ID (als String) + * @param externalName Discord-Username oder Telegram-@username + * @return LinkedAccount wenn erfolgreich, null wenn Token ungültig/abgelaufen + */ + public LinkedAccount redeemToken(String token, String externalId, String externalName, String type) { + token = token.trim().toUpperCase(); + PendingToken pending = pendingTokens.get(token); + + if (pending == null || pending.isExpired()) { + pendingTokens.remove(token); + return null; + } + // Typ muss übereinstimmen + if (!pending.type.equals(type)) return null; + + pendingTokens.remove(token); + + // Bestehenden Account holen oder neu anlegen + LinkedAccount account = links.computeIfAbsent(pending.uuid, k -> { + LinkedAccount a = new LinkedAccount(); + a.minecraftUUID = pending.uuid; + a.minecraftName = pending.playerName; + return a; + }); + account.minecraftName = pending.playerName; // aktuell halten + + if ("discord".equals(pending.type)) { + account.discordUserId = externalId; + account.discordUsername = externalName; + } else if ("telegram".equals(pending.type)) { + account.telegramUserId = externalId; + account.telegramUsername = externalName; + } + + save(); + return account; + } + + // ===== Lookup ===== + + public LinkedAccount getByUUID(UUID uuid) { + return links.get(uuid); + } + + public LinkedAccount getByDiscordId(String discordUserId) { + for (LinkedAccount a : links.values()) { + if (discordUserId.equals(a.discordUserId)) return a; + } + return null; + } + + public LinkedAccount getByTelegramId(String telegramUserId) { + for (LinkedAccount a : links.values()) { + if (telegramUserId.equals(a.telegramUserId)) return a; + } + return null; + } + + /** Gibt den Minecraft-Namen für eine Discord-User-ID zurück, oder null. */ + public String getMinecraftNameByDiscordId(String discordUserId) { + LinkedAccount a = getByDiscordId(discordUserId); + return a != null ? a.minecraftName : null; + } + + /** Gibt den Minecraft-Namen für eine Telegram-User-ID zurück, oder null. */ + public String getMinecraftNameByTelegramId(String telegramUserId) { + LinkedAccount a = getByTelegramId(telegramUserId); + return a != null ? a.minecraftName : null; + } + + /** Prüft ob ein Token gerade aussteht (für Tab-Complete etc.). */ + public boolean hasPendingToken(UUID uuid, String type) { + for (PendingToken t : pendingTokens.values()) { + if (t.uuid.equals(uuid) && t.type.equals(type) && !t.isExpired()) return true; + } + return false; + } + + // ===== Verknüpfung aufheben ===== + + public boolean unlinkDiscord(UUID uuid) { + LinkedAccount a = links.get(uuid); + if (a == null || a.discordUserId.isEmpty()) return false; + a.discordUserId = ""; + a.discordUsername = ""; + cleanupEmpty(uuid); + save(); + return true; + } + + public boolean unlinkTelegram(UUID uuid) { + LinkedAccount a = links.get(uuid); + if (a == null || a.telegramUserId.isEmpty()) return false; + a.telegramUserId = ""; + a.telegramUsername = ""; + cleanupEmpty(uuid); + save(); + return true; + } + + private void cleanupEmpty(UUID uuid) { + LinkedAccount a = links.get(uuid); + if (a != null && a.discordUserId.isEmpty() && a.telegramUserId.isEmpty()) { + links.remove(uuid); + } + } + + // ===== Convenience-Methoden für Bridges ===== + + /** + * Löst einen Telegram-Token ein. + * Wrapper für redeemToken mit type="telegram". + */ + public LinkedAccount redeemTelegram(String token, String telegramUserId, String telegramUsername) { + return redeemToken(token, telegramUserId, telegramUsername, "telegram"); + } + + /** + * Löst einen Discord-Token ein. + * Wrapper für redeemToken mit type="discord". + */ + public LinkedAccount redeemDiscord(String token, String discordUserId, String discordUsername) { + return redeemToken(token, discordUserId, discordUsername, "discord"); + } + + /** + * Gibt den Anzeigenamen für einen Telegram-Nutzer zurück. + * Wenn verknüpft: "MinecraftName (@telegram)", sonst: "@telegram" + */ + public String resolveTelegramName(String telegramUserId, String fallbackName) { + String mc = getMinecraftNameByTelegramId(telegramUserId); + return mc != null ? mc : fallbackName; + } + + /** + * Gibt den Anzeigenamen für einen Discord-Nutzer zurück. + * Wenn verknüpft: Minecraft-Name, sonst: Discord-Username + */ + public String resolveDiscordName(String discordUserId, String fallbackName) { + String mc = getMinecraftNameByDiscordId(discordUserId); + return mc != null ? mc : fallbackName; + } + + /** + * Gibt das korrekte Format-String zurück abhängig ob Account verknüpft. + * linked=true → linkedFormat (mit {player}), false → unlinkedFormat (mit {user}) + */ + public boolean isLinkedTelegram(String telegramUserId) { + return getByTelegramId(telegramUserId) != null; + } + + public boolean isLinkedDiscord(String discordUserId) { + return getByDiscordId(discordUserId) != null; + } + + // ===== Persistenz ===== + + public void save() { + try (BufferedWriter bw = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) { + for (LinkedAccount a : links.values()) { + bw.write(a.minecraftUUID + + "|" + esc(a.minecraftName) + + "|" + esc(a.discordUserId) + + "|" + esc(a.discordUsername) + + "|" + esc(a.telegramUserId) + + "|" + esc(a.telegramUsername)); + bw.newLine(); + } + } catch (IOException e) { + logger.warning("[ChatModule] Fehler beim Speichern der Account-Links: " + e.getMessage()); + } + } + + public void load() { + links.clear(); + if (!file.exists()) return; + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(file), "UTF-8"))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + String[] p = line.split("\\|", -1); + if (p.length < 6) continue; + try { + LinkedAccount a = new LinkedAccount(); + a.minecraftUUID = UUID.fromString(p[0]); + a.minecraftName = unesc(p[1]); + a.discordUserId = unesc(p[2]); + a.discordUsername = unesc(p[3]); + a.telegramUserId = unesc(p[4]); + a.telegramUsername = unesc(p[5]); + if (!a.discordUserId.isEmpty() || !a.telegramUserId.isEmpty()) { + links.put(a.minecraftUUID, a); + } + } catch (Exception ignored) {} + } + } catch (IOException e) { + logger.warning("[ChatModule] Fehler beim Laden der Account-Links: " + e.getMessage()); + } + } + + private static String esc(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\").replace("|", "\\p"); + } + + private static String unesc(String s) { + if (s == null) return ""; + return s.replace("\\p", "|").replace("\\\\", "\\"); + } +} \ No newline at end of file