diff --git a/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/bridge/TelegramBridge.java b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/bridge/TelegramBridge.java new file mode 100644 index 0000000..e4657ec --- /dev/null +++ b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/chat/bridge/TelegramBridge.java @@ -0,0 +1,399 @@ +package net.viper.status.modules.chat.bridge; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.plugin.Plugin; +import net.viper.status.modules.chat.AccountLinkManager; +import net.viper.status.modules.chat.ChatConfig; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +/** + * Telegram-Brücke für bidirektionale Kommunikation. + * + * Minecraft → Telegram: Via Bot API (sendMessage) + * Telegram → Minecraft: Via Long-Polling (getUpdates) + * + * Voraussetzungen: + * - Telegram Bot via @BotFather erstellen + * - Bot-Token in chat.yml eintragen + * - Bot in die gewünschten Gruppen/Kanäle einladen + * - Bot zu Admin machen (für Gruppen-Nachrichten empfangen) + */ +public class TelegramBridge { + + private static final String API_BASE = "https://api.telegram.org/bot"; + + private final Plugin plugin; + private final ChatConfig config; + private final Logger logger; + private AccountLinkManager linkManager; // wird nach dem Start gesetzt + + // Letztes verarbeitetes Update-ID (für getUpdates Offset) + private final AtomicLong lastUpdateId = new AtomicLong(0L); + + private volatile boolean running = false; + + public TelegramBridge(Plugin plugin, ChatConfig config) { + this.plugin = plugin; + this.config = config; + this.logger = plugin.getLogger(); + } + + /** Setzt den AccountLinkManager – muss vor start() aufgerufen werden. */ + public void setLinkManager(AccountLinkManager linkManager) { + this.linkManager = linkManager; + } + + public void start() { + if (!config.isTelegramEnabled() + || config.getTelegramBotToken().isEmpty() + || config.getTelegramBotToken().equals("YOUR_TELEGRAM_BOT_TOKEN")) { + logger.warning("[ChatModule-Telegram] Bot-Token nicht konfiguriert. Telegram-Empfang deaktiviert."); + return; + } + + running = true; + int interval = Math.max(2, config.getTelegramPollInterval()); + + plugin.getProxy().getScheduler().schedule(plugin, this::pollUpdates, + interval, interval, TimeUnit.SECONDS); + + logger.info("[ChatModule-Telegram] Brücke gestartet (Poll-Intervall: " + interval + "s)."); + } + + public void stop() { + running = false; + } + + // ===== Minecraft → Telegram ===== + + /** + * Sendet eine Nachricht an eine Telegram-Chat-ID. + * Unterstützt Themen-Gruppen via message_thread_id. + */ + public void sendToTelegram(String chatId, String message) { + sendToTelegram(chatId, 0, message); + } + + public void sendToTelegram(String chatId, int threadId, String message) { + if (chatId == null || chatId.isEmpty()) return; + + plugin.getProxy().getScheduler().runAsync(plugin, () -> { + try { + String cleanMessage = ChatColor.stripColor( + ChatColor.translateAlternateColorCodes('&', message)); + + String url = API_BASE + config.getTelegramBotToken() + + "/sendMessage?chat_id=" + URLEncoder.encode(chatId, "UTF-8") + + "&text=" + URLEncoder.encode(cleanMessage, "UTF-8") + + "&parse_mode=HTML" + + (threadId > 0 ? "&message_thread_id=" + threadId : ""); + + getJson(url); + } catch (Exception e) { + logger.warning("[ChatModule-Telegram] Sende-Fehler: " + e.getMessage()); + } + }); + } + + /** + * Sendet eine formatierte HelpOp/Broadcast-Nachricht an Telegram. + * Unterstützt Themen-Gruppen via message_thread_id. + */ + public void sendFormattedToTelegram(String chatId, String header, String content) { + sendFormattedToTelegram(chatId, 0, header, content); + } + + public void sendFormattedToTelegram(String chatId, int threadId, String header, String content) { + if (chatId == null || chatId.isEmpty()) return; + String text = "" + escapeHtml(header) + "\n" + escapeHtml(content); + + plugin.getProxy().getScheduler().runAsync(plugin, () -> { + try { + String url = API_BASE + config.getTelegramBotToken() + + "/sendMessage?chat_id=" + URLEncoder.encode(chatId, "UTF-8") + + "&text=" + URLEncoder.encode(text, "UTF-8") + + "&parse_mode=HTML" + + (threadId > 0 ? "&message_thread_id=" + threadId : ""); + getJson(url); + } catch (Exception e) { + logger.warning("[ChatModule-Telegram] Format-Sende-Fehler: " + e.getMessage()); + } + }); + } + + // ===== Telegram → Minecraft (Polling) ===== + + private void pollUpdates() { + if (!running) return; + try { + // Beim ersten Poll: nur den aktuellen Offset holen, keine alten Updates verarbeiten + if (lastUpdateId.get() == 0L) { + String initUrl = API_BASE + config.getTelegramBotToken() + + "/getUpdates?limit=1&offset=-1"; + String initResp = getJson(initUrl); + if (initResp != null && initResp.contains("\"ok\":true")) { + java.util.List initUpdates = parseUpdates(initResp); + if (!initUpdates.isEmpty()) { + lastUpdateId.set(initUpdates.get(initUpdates.size() - 1).updateId); + } + } + return; // Erster Poll nur zum Initialisieren + } + + long offset = lastUpdateId.get() + 1; + String url = API_BASE + config.getTelegramBotToken() + + "/getUpdates?timeout=2&limit=10" + + (offset > 0 ? "&offset=" + offset : ""); + + String response = getJson(url); + if (response == null || !response.contains("\"ok\":true")) return; + + java.util.List updates = parseUpdates(response); + + for (TelegramUpdate update : updates) { + if (update.updateId > lastUpdateId.get()) { + lastUpdateId.set(update.updateId); + } + if (update.text == null || update.text.isEmpty()) continue; + if (update.isBot) continue; + + // ── Token-Einlösung: /link ── + if (update.text.startsWith("/link ") || update.text.startsWith("/link@")) { + String[] parts = update.text.split("\\s+", 2); + if (parts.length == 2 && linkManager != null) { + String token = parts[1].trim().toUpperCase(); + AccountLinkManager.LinkedAccount acc = + linkManager.redeemTelegram(token, update.fromId, update.fromName); + if (acc != null) { + sendToTelegram(update.chatId, update.threadId, + "✅ Verknüpfung erfolgreich! Minecraft-Account: " + + escapeHtml(acc.minecraftName) + ""); + } else { + sendToTelegram(update.chatId, update.threadId, + "❌ Ungültiger oder abgelaufener Token. Bitte /telegramlink im Spiel erneut ausführen."); + } + } + continue; // Nicht als Chat-Nachricht weiterleiten + } + + // Bot-Befehle ignorieren + if (update.text.startsWith("/")) continue; + + // ── Account-Name auflösen ── + String displayName = (linkManager != null) + ? linkManager.resolveTelegramName(update.fromId, update.fromName) + : update.fromName; + + // Welchem Minecraft-Kanal gehört diese Telegram-Chat-ID + Thread? + final boolean isAdminChat = update.chatId.equals(config.getTelegramAdminChatId()) + && (config.getTelegramAdminTopicId() == 0 + || config.getTelegramAdminTopicId() == update.threadId); + + // Prüfen ob die Nachricht zu einem konfigurierten Kanal-Thema gehört + final boolean matchesChannel = isAdminChat || matchesTelegramChannel(update); + + if (!matchesChannel && !isAdminChat) continue; + + final String format = config.getTelegramFromFormat(); + final String finalDisplay = displayName; + final String formatted = ChatColor.translateAlternateColorCodes('&', + format.replace("{user}", finalDisplay) + .replace("{message}", update.text)); + + ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { + if (isAdminChat) { + for (net.md_5.bungee.api.connection.ProxiedPlayer p : + ProxyServer.getInstance().getPlayers()) { + if (p.hasPermission("chat.admin.bypass")) { + p.sendMessage(new TextComponent(formatted)); + } + } + } else { + ProxyServer.getInstance().broadcast(new TextComponent(formatted)); + } + }); + } + } catch (Exception e) { + logger.fine("[ChatModule-Telegram] Poll-Fehler: " + e.getMessage()); + } + } + + // ===== HTTP-Hilfsmethoden ===== + + private String getJson(String urlStr) throws Exception { + HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(6000); + conn.setReadTimeout(10000); + conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0"); + int code = conn.getResponseCode(); + String result = readStream(code == 200 ? conn.getInputStream() : conn.getErrorStream()); + conn.disconnect(); + return result; + } + + private String readStream(InputStream in) throws IOException { + if (in == null) return ""; + try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) sb.append(line); + return sb.toString(); + } + } + + // ===== JSON Mini-Parser ===== + + private static class TelegramUpdate { + long updateId; + String chatId = ""; + String fromId = ""; // Telegram User-ID (für Account-Link) + String fromName = ""; + String text = ""; + boolean isBot = false; + int threadId = 0; // message_thread_id für Themen-Gruppen (0 = kein Thema) + } + + private java.util.List parseUpdates(String json) { + java.util.List result = new java.util.ArrayList<>(); + // Suche nach "result":[...] + int resultStart = json.indexOf("\"result\":["); + if (resultStart < 0) return result; + + // Extrahiere alle Update-Objekte + int depth = 0, start = -1; + boolean inResult = false; + for (int i = resultStart + 10; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '[' && !inResult) { inResult = true; continue; } + if (!inResult) continue; + if (c == '{') { if (depth++ == 0) start = i; } + else if (c == '}') { + if (--depth == 0 && start >= 0) { + TelegramUpdate upd = parseUpdate(json.substring(start, i + 1)); + if (upd != null) result.add(upd); + start = -1; + } + } else if (c == ']' && depth == 0) break; + } + return result; + } + + /** Prüft ob ein Update zu einem konfigurierten Kanal-Thema gehört. */ + private boolean matchesTelegramChannel(TelegramUpdate update) { + for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) { + if (!ch.getTelegramChatId().equals(update.chatId)) continue; + // Thema konfiguriert? → Thread-ID muss übereinstimmen + if (ch.getTelegramThreadId() > 0 && ch.getTelegramThreadId() != update.threadId) continue; + return true; + } + return false; + } + + private TelegramUpdate parseUpdate(String obj) { + try { + TelegramUpdate upd = new TelegramUpdate(); + upd.updateId = Long.parseLong(extractValue(obj, "update_id")); + + // message-Block + int msgIdx = obj.indexOf("\"message\""); + if (msgIdx < 0) return null; + String msgBlock = extractObject(obj, msgIdx); + + upd.text = unescapeJson(extractString(msgBlock, "text")); + + // message_thread_id (Themen-Gruppen) + String threadIdStr = extractValue(msgBlock, "message_thread_id"); + if (!threadIdStr.isEmpty()) { + try { upd.threadId = Integer.parseInt(threadIdStr); } catch (Exception ignored) {} + } + + // from-Block (Absender) + int fromIdx = msgBlock.indexOf("\"from\""); + if (fromIdx >= 0) { + String fromBlock = extractObject(msgBlock, fromIdx); + String firstName = unescapeJson(extractString(fromBlock, "first_name")); + String lastName = unescapeJson(extractString(fromBlock, "last_name")); + String username = unescapeJson(extractString(fromBlock, "username")); + upd.fromId = extractValue(fromBlock, "id"); + upd.fromName = !username.isEmpty() ? "@" + username + : (firstName + (lastName.isEmpty() ? "" : " " + lastName)).trim(); + String botFlag = extractValue(fromBlock, "is_bot"); + upd.isBot = "true".equals(botFlag); + } + + // chat-Block (Chat-ID) + int chatIdx = msgBlock.indexOf("\"chat\""); + if (chatIdx >= 0) { + String chatBlock = extractObject(msgBlock, chatIdx); + upd.chatId = extractValue(chatBlock, "id"); + } + + return upd; + } catch (Exception e) { + return null; + } + } + + private String extractValue(String json, String key) { + String fullKey = "\"" + key + "\""; + int idx = json.indexOf(fullKey); + if (idx < 0) return ""; + int colon = json.indexOf(':', idx + fullKey.length()); + if (colon < 0) return ""; + int valStart = colon + 1; + while (valStart < json.length() && json.charAt(valStart) == ' ') valStart++; + if (valStart >= json.length()) return ""; + char first = json.charAt(valStart); + if (first == '"') { + return extractString(json.substring(valStart - 1 - key.length()), key); + } + int end = valStart; + while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++; + return json.substring(valStart, end).trim(); + } + + private String extractString(String json, String key) { + String fullKey = "\"" + key + "\":\""; + int idx = json.indexOf(fullKey); + if (idx < 0) return ""; + int start = idx + fullKey.length(); + int end = start; + while (end < json.length()) { + if (json.charAt(end) == '"' && json.charAt(end - 1) != '\\') break; + end++; + } + return json.substring(start, end); + } + + private String extractObject(String json, int fromIndex) { + int depth = 0, start = -1; + for (int i = fromIndex; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '{') { if (depth++ == 0) start = i; } + else if (c == '}') { if (--depth == 0 && start >= 0) return json.substring(start, i + 1); } + } + return ""; + } + + private static String unescapeJson(String s) { + if (s == null) return ""; + return s.replace("\\\"", "\"").replace("\\n", "\n") + .replace("\\r", "\r").replace("\\\\", "\\"); + } + + private static String escapeHtml(String s) { + if (s == null) return ""; + return s.replace("&", "&").replace("<", "<").replace(">", ">"); + } +} \ No newline at end of file