diff --git a/src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java b/src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java deleted file mode 100644 index 47aa8b1..0000000 --- a/src/main/java/net/viper/status/modules/forum/ForumBridgeModule.java +++ /dev/null @@ -1,349 +0,0 @@ -package net.viper.status.modules.forum; - -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.ChatColor; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.CommandSender; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.ProxyServer; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.chat.ClickEvent; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.chat.HoverEvent; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.chat.TextComponent; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.event.PostLoginEvent; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.plugin.Command; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.plugin.Listener; -import net.viper.status.StatusAPI; -import net.md_5.bungee.api.plugin.Plugin; -import net.viper.status.StatusAPI; -import net.md_5.bungee.event.EventHandler; -import net.viper.status.module.Module; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.Charset; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.TimeUnit; - -/** - * ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server. - * - * Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zurück. - * Alle Aufrufer müssen nicht mehr auf null prüfen, was NullPointerExceptions verhindert. - */ -public class ForumBridgeModule implements Module, Listener { - - private Plugin plugin; - private ForumNotifStorage storage; - - private boolean enabled = true; - private String wpBaseUrl = ""; - private String apiSecret = ""; - private int loginDelaySeconds = 3; - - @Override - public String getName() { return "ForumBridgeModule"; } - - @Override - public void onEnable(Plugin plugin) { - this.plugin = plugin; - loadConfig(plugin); - if (!enabled) { StatusAPI.debugLog(plugin, "ForumBridgeModule ist deaktiviert."); return; } - - storage = new ForumNotifStorage(plugin.getDataFolder(), plugin.getLogger()); - storage.load(); - - plugin.getProxy().getPluginManager().registerListener(plugin, this); - ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumLinkCommand()); - ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ForumCommand()); - - plugin.getProxy().getScheduler().schedule(plugin, () -> { - try { storage.save(); } catch (Exception e) { plugin.getLogger().warning("ForumBridge Auto-Save Fehler: " + e.getMessage()); } - }, 10, 10, TimeUnit.MINUTES); - - plugin.getProxy().getScheduler().schedule(plugin, () -> storage.purgeOld(30), 1, 24, TimeUnit.HOURS); - plugin.getLogger().fine("ForumBridgeModule aktiviert."); - } - - @Override - public void onDisable(Plugin plugin) { - if (storage != null) { storage.save(); StatusAPI.debugLog(plugin, "Forum-Benachrichtigungen gespeichert."); } - } - - private void loadConfig(Plugin plugin) { - try { - Properties props = new Properties(); - File configFile = new File(plugin.getDataFolder(), "verify.properties"); - if (configFile.exists()) { - try (FileInputStream fis = new FileInputStream(configFile)) { props.load(fis); } - } - this.enabled = !"false".equalsIgnoreCase(props.getProperty("forum.enabled", "true")); - this.wpBaseUrl = props.getProperty("forum.wp_url", props.getProperty("wp_verify_url", "")); - this.apiSecret = props.getProperty("forum.api_secret", ""); - this.loginDelaySeconds = parseInt(props.getProperty("forum.login_delay_seconds", "3"), 3); - } catch (Exception e) { - plugin.getLogger().warning("Fehler beim Laden der ForumBridge-Config: " + e.getMessage()); - } - } - - private int parseInt(String s, int def) { try { return Integer.parseInt(s); } catch (Exception e) { return def; } } - - // ===== HTTP HANDLER ===== - - public String handleNotify(String body, String apiKeyHeader) { - if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) { - return "{\"success\":false,\"error\":\"unauthorized\"}"; - } - - // FIX #13: extractJsonString gibt "" statt null → kein NullPointerException möglich - String playerUuid = extractJsonString(body, "player_uuid"); - String type = extractJsonString(body, "type"); - String title = extractJsonString(body, "title"); - String author = extractJsonString(body, "author"); - String url = extractJsonString(body, "url"); - - if (playerUuid.isEmpty()) return "{\"success\":false,\"error\":\"missing_player_uuid\"}"; - - java.util.UUID uuid; - try { uuid = java.util.UUID.fromString(playerUuid); } - catch (Exception e) { return "{\"success\":false,\"error\":\"invalid_uuid\"}"; } - - if ("thread".equalsIgnoreCase(type) && title.toLowerCase().contains("umfrage")) type = "poll"; - if (type.isEmpty()) type = "reply"; - - // Alle Werte sind garantiert nicht null (extractJsonString gibt "" zurück) - ForumNotification notification = new ForumNotification(uuid, type, title, author, url); - - ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid); - if (online != null && online.isConnected()) { - deliverNotification(online, notification); - notification.setDelivered(true); - return "{\"success\":true,\"delivered\":true}"; - } - - storage.add(notification); - return "{\"success\":true,\"delivered\":false}"; - } - - public String handleUnlink(String body, String apiKeyHeader) { - if (!apiSecret.isEmpty() && !apiSecret.equals(apiKeyHeader)) return "{\"success\":false,\"error\":\"unauthorized\"}"; - return "{\"success\":true}"; - } - - public String handleStatus() { - String version = "unknown"; - try { if (plugin.getDescription() != null) version = plugin.getDescription().getVersion(); } catch (Exception ignored) {} - return "{\"success\":true,\"module\":\"ForumBridgeModule\",\"version\":\"" + version + "\"}"; - } - - // ===== NOTIFICATION ===== - - private void deliverNotification(ProxiedPlayer player, ForumNotification notif) { - String color = notif.getTypeColor(); - String label = notif.getTypeLabel(); - player.sendMessage(new TextComponent("§8§m ")); - player.sendMessage(new TextComponent("§6§l✉ Forum §8» " + color + label)); - if (!notif.getTitle().isEmpty()) player.sendMessage(new TextComponent("§7 " + notif.getTitle())); - if (!notif.getAuthor().isEmpty()) player.sendMessage(new TextComponent("§7 von §f" + notif.getAuthor())); - if (!notif.getUrl().isEmpty()) { - TextComponent link = new TextComponent("§a ➜ Im Forum ansehen"); - link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl())); - link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, - new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create())); - player.sendMessage(link); - } - player.sendMessage(new TextComponent("§8§m ")); - } - - private void deliverPending(ProxiedPlayer player) { - List pending = storage.getPending(player.getUniqueId()); - if (pending.isEmpty()) return; - int count = pending.size(); - if (count > 3) { - player.sendMessage(new TextComponent("§8§m ")); - player.sendMessage(new TextComponent("§6§l✉ Forum §8» §fDu hast §e" + count + " §fneue Benachrichtigungen!")); - player.sendMessage(new TextComponent("§7 Tippe §e/forum §7um sie anzuzeigen.")); - player.sendMessage(new TextComponent("§8§m ")); - } else { - for (ForumNotification n : pending) deliverNotification(player, n); - } - storage.markAllDelivered(player.getUniqueId()); - storage.clearDelivered(player.getUniqueId()); - } - - @EventHandler - public void onJoin(PostLoginEvent e) { - ProxiedPlayer player = e.getPlayer(); - plugin.getProxy().getScheduler().schedule(plugin, () -> { - if (player.isConnected()) deliverPending(player); - }, loginDelaySeconds, TimeUnit.SECONDS); - } - - // ===== COMMANDS ===== - - private class ForumLinkCommand extends Command { - public ForumLinkCommand() { super("forumlink", null, "fl"); } - - @Override - public void execute(CommandSender sender, String[] args) { - if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; } - ProxiedPlayer p = (ProxiedPlayer) sender; - if (args.length != 1) { - p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink ")); - p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7.")); - return; - } - String token = args[0].trim().toUpperCase(); - if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert.")); return; } - p.sendMessage(new TextComponent("§7Überprüfe Token...")); - - plugin.getProxy().getScheduler().runAsync(plugin, () -> { - try { - String endpoint = wpBaseUrl + "/wp-json/mc-bridge/v1/verify-link"; - String payload = "{\"token\":\"" + escapeJson(token) + "\"," - + "\"mc_uuid\":\"" + p.getUniqueId() + "\"," - + "\"mc_name\":\"" + escapeJson(p.getName()) + "\"}"; - - HttpURLConnection conn = (HttpURLConnection) new URL(endpoint).openConnection(); - conn.setRequestMethod("POST"); - conn.setDoOutput(true); - conn.setConnectTimeout(5000); - conn.setReadTimeout(7000); - conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); - if (!apiSecret.isEmpty()) conn.setRequestProperty("X-Api-Key", apiSecret); - - Charset utf8 = Charset.forName("UTF-8"); - try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(utf8)); } - - int code = conn.getResponseCode(); - String resp = code >= 200 && code < 300 - ? streamToString(conn.getInputStream(), utf8) - : streamToString(conn.getErrorStream(), utf8); - - if (resp != null && resp.contains("\"success\":true")) { - String displayName = extractJsonString(resp, "display_name"); - String username = extractJsonString(resp, "username"); - String show = !displayName.isEmpty() ? displayName : username; - p.sendMessage(new TextComponent("§8§m ")); - p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!")); - if (!show.isEmpty()) p.sendMessage(new TextComponent("§7 Forum-User: §f" + show)); - p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen.")); - p.sendMessage(new TextComponent("§8§m ")); - } else { - String error = extractJsonString(resp, "error"); - String message = extractJsonString(resp, "message"); - if ("token_expired".equals(error)) p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen.")); - else if ("uuid_already_linked".equals(error)) p.sendMessage(new TextComponent("§c✗ " + (!message.isEmpty() ? message : "Diese UUID ist bereits verknüpft."))); - else if ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ungültiger Token.")); - else p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler"))); - } - } catch (Exception ex) { - p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum.")); - plugin.getLogger().warning("ForumLink Fehler: " + ex.getMessage()); - } - }); - } - } - - private class ForumCommand extends Command { - public ForumCommand() { super("forum"); } - - @Override - public void execute(CommandSender sender, String[] args) { - if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; } - ProxiedPlayer p = (ProxiedPlayer) sender; - List pending = storage.getPending(p.getUniqueId()); - - if (pending.isEmpty()) { - p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen.")); - if (!wpBaseUrl.isEmpty()) { - TextComponent link = new TextComponent("§a➜ Forum öffnen"); - link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl)); - link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke um das Forum zu öffnen").create())); - p.sendMessage(link); - } - return; - } - - p.sendMessage(new TextComponent("§8§m ")); - p.sendMessage(new TextComponent("§6§l✉ Forum-Benachrichtigungen §8(§f" + pending.size() + "§8)")); - p.sendMessage(new TextComponent("")); - int shown = 0; - for (ForumNotification n : pending) { - if (shown >= 10) { p.sendMessage(new TextComponent("§7 ... und " + (pending.size() - 10) + " weitere")); break; } - String color = n.getTypeColor(); - TextComponent line = new TextComponent(color + " • " + n.getTypeLabel() + "§7: "); - TextComponent detail = new TextComponent(!n.getTitle().isEmpty() ? "§f" + n.getTitle() : "§fvon " + n.getAuthor()); - if (!n.getUrl().isEmpty()) { - detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl())); - detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke zum Öffnen").create())); - } - line.addExtra(detail); - p.sendMessage(line); - shown++; - } - p.sendMessage(new TextComponent("")); - p.sendMessage(new TextComponent("§8§m ")); - storage.markAllDelivered(p.getUniqueId()); - storage.clearDelivered(p.getUniqueId()); - } - } - - // ===== HELPER ===== - - public ForumNotifStorage getStorage() { return storage; } - - /** - * FIX #13: Gibt immer einen leeren String zurück, niemals null. - * Verhindert NullPointerExceptions in allen Aufrufern. - */ - private static String extractJsonString(String json, String key) { - if (json == null || key == null) return ""; - String search = "\"" + key + "\""; - int idx = json.indexOf(search); - if (idx < 0) return ""; - int colon = json.indexOf(':', idx + search.length()); - if (colon < 0) return ""; - int i = colon + 1; - while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++; - if (i >= json.length()) return ""; - char c = json.charAt(i); - if (c == '"') { - i++; - StringBuilder sb = new StringBuilder(); - boolean escape = false; - while (i < json.length()) { - char ch = json.charAt(i++); - if (escape) { sb.append(ch); escape = false; } - else { if (ch == '\\') escape = true; else if (ch == '"') break; else sb.append(ch); } - } - return sb.toString(); - } - return ""; - } - - private static String escapeJson(String s) { - if (s == null) return ""; - return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); - } - - private static String streamToString(InputStream in, Charset charset) throws IOException { - if (in == null) return ""; - try (BufferedReader br = new BufferedReader(new InputStreamReader(in, charset))) { - StringBuilder sb = new StringBuilder(); String line; - while ((line = br.readLine()) != null) sb.append(line); - return sb.toString(); - } - } -}