Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/AccountLinkManager.java
This commit is contained in:
@@ -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:<uuid>|name:<spielername>|discord:<discord-user-id>|telegram:<telegram-user-id>
|
||||||
|
*/
|
||||||
|
public class AccountLinkManager {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
// UUID → verknüpfte Accounts
|
||||||
|
private final ConcurrentHashMap<UUID, LinkedAccount> links = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Ausstehende Token: token → UUID (läuft nach 10 Min ab)
|
||||||
|
private final ConcurrentHashMap<String, PendingToken> 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("\\\\", "\\");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user