Delete src/main/java/net/viper/status/modules/chat/bridge/DiscordBridge.java via Git Manager GUI

This commit is contained in:
2026-05-22 17:25:35 +00:00
parent 2cabae6cf0
commit 951bacfc2a

View File

@@ -1,323 +0,0 @@
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.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
/**
* Discord-Brücke für bidirektionale Kommunikation.
*
* Fix #12: extractJsonString() behandelt Escape-Sequenzen jetzt korrekt.
* Statt Zeichenvergleich mit dem Vorgänger-Char wird ein expliziter Escape-Flag verwendet.
*/
public class DiscordBridge {
private final Plugin plugin;
private final ChatConfig config;
private final Logger logger;
private AccountLinkManager linkManager;
private final java.util.Map<String, AtomicLong> lastMessageIds = new java.util.concurrent.ConcurrentHashMap<>();
private volatile boolean running = false;
public DiscordBridge(Plugin plugin, ChatConfig config) {
this.plugin = plugin;
this.config = config;
this.logger = plugin.getLogger();
}
public void setLinkManager(AccountLinkManager linkManager) { this.linkManager = linkManager; }
public void start() {
if (!config.isDiscordEnabled()
|| config.getDiscordBotToken().isEmpty()
|| config.getDiscordBotToken().equals("YOUR_BOT_TOKEN_HERE")) {
logger.warning("[ChatModule-Discord] Bot-Token nicht konfiguriert. Discord-Empfang deaktiviert.");
return;
}
running = true;
int interval = Math.max(2, config.getDiscordPollInterval());
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, interval, interval, TimeUnit.SECONDS);
logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s).");
}
public void stop() { running = false; }
// ===== Minecraft → Discord =====
public void sendToDiscord(String webhookUrl, String username, String message, String avatarUrl) {
if (webhookUrl == null || webhookUrl.isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try {
String payload = "{\"username\":\"" + escapeJson(username) + "\""
+ (avatarUrl != null && !avatarUrl.isEmpty() ? ",\"avatar_url\":\"" + avatarUrl + "\"" : "")
+ ",\"content\":\"" + escapeJson(message) + "\"}";
postJson(webhookUrl, payload, null);
} catch (Exception e) {
logger.warning("[ChatModule-Discord] Webhook-Fehler: " + e.getMessage());
}
});
}
public void sendEmbedToDiscord(String webhookUrl, String title, String description, String colorHex) {
if (webhookUrl == null || webhookUrl.isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try {
int color = 0x5865F2;
try { color = Integer.parseInt(colorHex.replace("#", ""), 16); } catch (Exception ignored) {}
String payload = "{\"embeds\":[{\"title\":\"" + escapeJson(title) + "\""
+ ",\"description\":\"" + escapeJson(description) + "\""
+ ",\"color\":" + color + "}]}";
postJson(webhookUrl, payload, null);
} catch (Exception e) {
logger.warning("[ChatModule-Discord] Embed-Fehler: " + e.getMessage());
}
});
}
public void sendToChannel(String channelId, String message) {
if (channelId == null || channelId.isEmpty()) return;
if (config.getDiscordBotToken().isEmpty()) return;
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try {
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages";
postJson(url, "{\"content\":\"" + escapeJson(message) + "\"}", "Bot " + config.getDiscordBotToken());
} catch (Exception e) {
logger.warning("[ChatModule-Discord] Send-to-Channel-Fehler: " + e.getMessage());
}
});
}
// ===== Discord → Minecraft (Polling) =====
private void pollAllChannels() {
if (!running) return;
java.util.Set<String> channelIds = new java.util.LinkedHashSet<>();
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
if (!ch.getDiscordChannelId().isEmpty()) channelIds.add(ch.getDiscordChannelId());
}
if (!config.getDiscordAdminChannelId().isEmpty()) channelIds.add(config.getDiscordAdminChannelId());
for (String channelId : channelIds) pollChannel(channelId);
}
private void pollChannel(String channelId) {
try {
AtomicLong lastId = lastMessageIds.computeIfAbsent(channelId, k -> new AtomicLong(0L));
if (lastId.get() == 0L) {
String initResp = getJson("https://discord.com/api/v10/channels/" + channelId + "/messages?limit=1",
"Bot " + config.getDiscordBotToken());
if (initResp != null && !initResp.equals("[]") && !initResp.isEmpty()) {
java.util.List<DiscordMessage> initMsgs = parseMessages(initResp);
if (!initMsgs.isEmpty()) lastId.set(initMsgs.get(0).id);
}
return;
}
String url = "https://discord.com/api/v10/channels/" + channelId + "/messages?after=" + lastId.get() + "&limit=10";
String response = getJson(url, "Bot " + config.getDiscordBotToken());
if (response == null || response.equals("[]") || response.isEmpty()) return;
java.util.List<DiscordMessage> messages = parseMessages(response);
messages.sort(java.util.Comparator.comparingLong(m -> m.id));
for (DiscordMessage msg : messages) {
if (msg.id <= lastId.get()) continue;
if (msg.isBot) continue;
if (msg.content.isEmpty()) continue;
lastId.set(msg.id);
if (msg.content.startsWith("!link ")) {
String token = msg.content.substring(6).trim().toUpperCase();
if (linkManager != null) {
AccountLinkManager.LinkedAccount acc = linkManager.redeemDiscord(token, msg.authorId, msg.authorName);
if (acc != null) sendToChannel(channelId, "✅ Verknüpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
else sendToChannel(channelId, "❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen.");
}
continue;
}
String displayName = (linkManager != null)
? linkManager.resolveDiscordName(msg.authorId, msg.authorName) : msg.authorName;
String mcFormat = resolveFormat(channelId);
if (mcFormat == null) continue;
String formatted = ChatColor.translateAlternateColorCodes('&',
mcFormat.replace("{user}", displayName).replace("{message}", msg.content));
ProxyServer.getInstance().getScheduler().runAsync(plugin,
() -> ProxyServer.getInstance().broadcast(new TextComponent(formatted)));
}
} catch (Exception e) {
logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage());
}
}
private String resolveFormat(String channelId) {
if (channelId.equals(config.getDiscordAdminChannelId())) return config.getDiscordFromFormat();
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
if (channelId.equals(ch.getDiscordChannelId())) return config.getDiscordFromFormat();
}
return null;
}
// ===== HTTP =====
private void postJson(String urlStr, String payload, String authorization) throws Exception {
HttpURLConnection conn = openConnection(urlStr, "POST", authorization);
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
conn.setRequestProperty("Content-Length", String.valueOf(data.length));
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) { os.write(data); }
int code = conn.getResponseCode();
if (code >= 400) logger.warning("[ChatModule-Discord] HTTP " + code + ": " + readStream(conn.getErrorStream()));
conn.disconnect();
}
private String getJson(String urlStr, String authorization) throws Exception {
HttpURLConnection conn = openConnection(urlStr, "GET", authorization);
int code = conn.getResponseCode();
if (code != 200) { conn.disconnect(); return null; }
String result = readStream(conn.getInputStream());
conn.disconnect();
return result;
}
private HttpURLConnection openConnection(String urlStr, String method, String authorization) throws Exception {
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(5000);
conn.setReadTimeout(8000);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "StatusAPI-ChatModule/1.0");
if (authorization != null && !authorization.isEmpty()) conn.setRequestProperty("Authorization", authorization);
return conn;
}
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 DiscordMessage {
long id;
String authorId = "", authorName = "", content = "";
boolean isBot = false;
}
private java.util.List<DiscordMessage> parseMessages(String json) {
java.util.List<DiscordMessage> result = new java.util.ArrayList<>();
int depth = 0, start = -1;
for (int i = 0; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '{') { if (depth++ == 0) start = i; }
else if (c == '}') {
if (--depth == 0 && start != -1) {
DiscordMessage msg = parseMessage(json.substring(start, i + 1));
if (msg != null) result.add(msg);
start = -1;
}
}
}
return result;
}
private DiscordMessage parseMessage(String obj) {
try {
DiscordMessage msg = new DiscordMessage();
msg.id = Long.parseLong(extractJsonString(obj, "id"));
msg.content = unescapeJson(extractJsonString(obj, "content"));
// Webhook-Nachrichten als Bot markieren (Echo-Loop verhindern)
if (!extractJsonString(obj, "webhook_id").isEmpty()) {
msg.isBot = true;
return msg;
}
int authStart = obj.indexOf("\"author\"");
if (authStart >= 0) {
String authBlock = extractJsonObject(obj, authStart);
msg.authorId = extractJsonString(authBlock, "id");
msg.authorName = unescapeJson(extractJsonString(authBlock, "username"));
msg.isBot = "true".equals(extractJsonString(authBlock, "bot"));
}
return msg;
} catch (Exception e) {
return null;
}
}
/**
* FIX #12: Escape-Sequenzen werden korrekt mit einem Escape-Flag behandelt
* statt den Vorgänger-Char zu vergleichen (der bei '\\' + '"' versagt).
* Gibt immer einen leeren String zurück wenn der Key nicht gefunden wird (nie null).
*/
private String extractJsonString(String json, String key) {
if (json == null || key == null) return "";
String fullKey = "\"" + key + "\"";
int keyIdx = json.indexOf(fullKey);
if (keyIdx < 0) return "";
int colon = json.indexOf(':', keyIdx + 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 == '"') {
// FIX: Expliziter Escape-Flag statt Vorgänger-Char-Vergleich
int end = valStart + 1;
boolean escaped = false;
while (end < json.length()) {
char ch = json.charAt(end);
if (escaped) {
escaped = false;
} else if (ch == '\\') {
escaped = true;
} else if (ch == '"') {
break;
}
end++;
}
return json.substring(valStart + 1, end);
} else {
int end = valStart;
while (end < json.length() && ",}\n".indexOf(json.charAt(end)) < 0) end++;
return json.substring(valStart, end).trim();
}
}
private String extractJsonObject(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 escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
}
private static String unescapeJson(String s) {
if (s == null) return "";
return s.replace("\\\"", "\"").replace("\\n", "\n").replace("\\r", "\r").replace("\\\\", "\\");
}
}