Delete src/main/java/net/viper/status/modules/chat/bridge/DiscordBridge.java via Git Manager GUI
This commit is contained in:
@@ -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("\\\\", "\\");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user