diff --git a/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java b/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java deleted file mode 100644 index 14c16be..0000000 --- a/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java +++ /dev/null @@ -1,378 +0,0 @@ -package net.viper.status.modules.broadcast; - -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.plugin.Plugin; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.chat.ClickEvent; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.plugin.Listener; -import net.viper.status.module.Module; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -/** - * BroadcastModule - * - * Fixes: - * - loadSchedules(): ID-Split nutzt jetzt indexOf/lastIndexOf statt split("\\.") mit length==2-Check. - * Damit werden auch clientScheduleIds die Punkte enthalten korrekt geladen. (Bug #2) - * - handleBroadcast(): &-Farbcodes werden jetzt auch in der Nachricht selbst übersetzt. (Bug #3) - * - handleBroadcast(): Literal \n in der Nachricht wird als echter Zeilenumbruch gerendert. (Bug #4) - * - handleBroadcast(): URLs (http/https) werden als anklickbare TextComponents eingebettet. (Bug #5) - */ -public class BroadcastModule implements Module, Listener { - - private Plugin plugin; - private boolean enabled = true; - private String requiredApiKey = ""; - private String format = "%prefix% %message%"; - private String fallbackPrefix = "[Broadcast]"; - private String fallbackPrefixColor = "&c"; - private String fallbackBracketColor = "&8"; - private String fallbackMessageColor = "&f"; - - private final Map scheduledByClientId = new ConcurrentHashMap<>(); - private File schedulesFile; - private final SimpleDateFormat dateFormat; - - public BroadcastModule() { - dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - @Override - public String getName() { return "BroadcastModule"; } - - @Override - public void onEnable(Plugin plugin) { - this.plugin = plugin; - schedulesFile = new File(plugin.getDataFolder(), "broadcasts.schedules"); - loadConfig(); - if (!enabled) return; - try { plugin.getProxy().getPluginManager().registerListener(plugin, this); } catch (Throwable ignored) {} - plugin.getLogger().info("[BroadcastModule] aktiviert. Format: " + format); - loadSchedules(); - plugin.getProxy().getScheduler().schedule(plugin, this::processScheduled, 1, 1, TimeUnit.SECONDS); - } - - @Override - public void onDisable(Plugin plugin) { - saveSchedules(); - scheduledByClientId.clear(); - } - - private void loadConfig() { - File file = new File(plugin.getDataFolder(), "verify.properties"); - if (!file.exists()) return; - try (InputStream in = new FileInputStream(file)) { - Properties props = new Properties(); - props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); - enabled = Boolean.parseBoolean(props.getProperty("broadcast.enabled", "true")); - requiredApiKey = props.getProperty("broadcast.api_key", "").trim(); - format = props.getProperty("broadcast.format", format).trim(); - if (format.isEmpty()) format = "%prefix% %message%"; - fallbackPrefix = props.getProperty("broadcast.prefix", fallbackPrefix).trim(); - fallbackPrefixColor = props.getProperty("broadcast.prefix-color", fallbackPrefixColor).trim(); - fallbackBracketColor = props.getProperty("broadcast.bracket-color", fallbackBracketColor).trim(); - fallbackMessageColor = props.getProperty("broadcast.message-color", fallbackMessageColor).trim(); - } catch (IOException e) { - plugin.getLogger().warning("[BroadcastModule] Fehler beim Laden von verify.properties: " + e.getMessage()); - } - } - - public boolean handleBroadcast(String sourceName, String message, String type, String apiKeyHeader, - String prefix, String prefixColor, String bracketColor, String messageColor) { - loadConfig(); - if (!enabled) return false; - if (requiredApiKey != null && !requiredApiKey.isEmpty()) { - if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) { - plugin.getLogger().warning("[BroadcastModule] Broadcast abgelehnt: API-Key fehlt oder inkorrekt."); - return false; - } - } - - if (message == null) message = ""; - if (sourceName == null || sourceName.isEmpty()) sourceName = "System"; - if (type == null) type = "global"; - - String usedPrefix = (prefix != null && !prefix.trim().isEmpty()) ? prefix.trim() : fallbackPrefix; - String usedPrefixColor = (prefixColor != null && !prefixColor.trim().isEmpty()) ? prefixColor.trim() : fallbackPrefixColor; - String usedBracketColor = (bracketColor != null && !bracketColor.trim().isEmpty()) ? bracketColor.trim() : fallbackBracketColor; - String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor; - - String prefixColorCode = normalizeColorCode(usedPrefixColor); - String bracketColorCode = normalizeColorCode(usedBracketColor); - String messageColorCode = normalizeColorCode(usedMessageColor); - - String finalPrefix; - if (!bracketColorCode.isEmpty()) { - String textContent = usedPrefix; - if (textContent.startsWith("[")) textContent = textContent.substring(1); - if (textContent.endsWith("]")) textContent = textContent.substring(0, textContent.length() - 1); - finalPrefix = bracketColorCode + "[" + prefixColorCode + textContent + bracketColorCode + "]" + ChatColor.RESET; - } else { - finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET; - } - - // FIX #1: &-Farbcodes auch in der Nachricht selbst übersetzen - String translatedMessage = ChatColor.translateAlternateColorCodes('&', message); - String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + translatedMessage; - - String out = format - .replace("%name%", sourceName) - .replace("%prefix%", finalPrefix) - .replace("%prefixColored%", finalPrefix) - .replace("%message%", translatedMessage) - .replace("%messageColored%",coloredMessage) - .replace("%type%", type); - - // FIX #2: \r entfernen (Windows CRLF -> nur LF), Literal \\n als Fallback - out = out.replace("\r\n", "\n").replace("\r", "").replace("\\n", "\n"); - - // FIX #3: Nachricht mit anklickbaren URLs aufbauen - BaseComponent[] components = buildClickableComponents(out); - int sent = 0; - for (ProxiedPlayer p : plugin.getProxy().getPlayers()) { - try { p.sendMessage(components); sent++; } catch (Throwable ignored) {} - } - plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message); - return true; - } - - /** - * Baut ein BaseComponent-Array aus einem formatierten String. - * URLs (http/https) werden als anklickbare TextComponents eingebettet. - * Unterstützt auch echte Newlines (\n) als Zeilenumbruch. - */ - private BaseComponent[] buildClickableComponents(String text) { - // Regex für URLs - java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile( - "(https?://[^\\s\\n]+)", java.util.regex.Pattern.CASE_INSENSITIVE); - - ComponentBuilder builder = new ComponentBuilder(""); - - // Zeilenweise aufteilen (echte \n) - String[] lines = text.split("\n", -1); - for (int li = 0; li < lines.length; li++) { - if (li > 0) { - // Zeilenumbruch als eigene Komponente - builder.append(TextComponent.fromLegacyText("\n")); - } - - String line = lines[li]; - java.util.regex.Matcher matcher = urlPattern.matcher(line); - int lastEnd = 0; - - while (matcher.find()) { - // Text vor der URL (mit Minecraft-Farbcodes) - if (matcher.start() > lastEnd) { - String before = line.substring(lastEnd, matcher.start()); - builder.append(TextComponent.fromLegacyText(before)); - } - - // URL selbst: anklickbar + unterstrichen - String url = matcher.group(1); - TextComponent urlComponent = new TextComponent(url); - urlComponent.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url)); - // Farbe der URL auf Cyan setzen damit sie sich abhebt - urlComponent.setColor(ChatColor.AQUA); - urlComponent.setUnderlined(true); - builder.append(urlComponent, ComponentBuilder.FormatRetention.NONE); - - lastEnd = matcher.end(); - } - - // Restlicher Text nach der letzten URL - if (lastEnd < line.length()) { - builder.append(TextComponent.fromLegacyText(line.substring(lastEnd))); - } - } - - return builder.create(); - } - - private String normalizeColorCode(String code) { - if (code == null) return ""; - code = code.trim(); - if (code.isEmpty()) return ""; - return code.contains("&") ? ChatColor.translateAlternateColorCodes('&', code) : code; - } - - private void saveSchedules() { - Properties props = new Properties(); - for (Map.Entry entry : scheduledByClientId.entrySet()) { - String id = entry.getKey(); - ScheduledBroadcast sb = entry.getValue(); - // Wir escapen den ID-Wert damit Punkte in der ID nicht den Parser verwirren - props.setProperty(id + ".nextRunMillis", String.valueOf(sb.nextRunMillis)); - props.setProperty(id + ".sourceName", sb.sourceName); - props.setProperty(id + ".message", sb.message); - props.setProperty(id + ".type", sb.type); - props.setProperty(id + ".prefix", sb.prefix); - props.setProperty(id + ".prefixColor", sb.prefixColor); - props.setProperty(id + ".bracketColor", sb.bracketColor); - props.setProperty(id + ".messageColor", sb.messageColor); - props.setProperty(id + ".recur", sb.recur); - } - try (OutputStream out = new FileOutputStream(schedulesFile)) { - props.store(out, "PulseCast Scheduled Broadcasts"); - } catch (IOException e) { - plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht speichern: " + e.getMessage()); - } - } - - /** - * FIX #2: Robusteres Parsen der Property-Keys. - * Statt split("\\.") mit length==2-Check nutzen wir lastIndexOf('.') um den letzten - * Punkt als Trenner zu verwenden. Damit funktionieren auch IDs die Punkte enthalten. - * - * Bekannte Felder: nextRunMillis, sourceName, message, type, prefix, prefixColor, - * bracketColor, messageColor, recur → alle ohne Punkte im Namen. - */ - private void loadSchedules() { - if (!schedulesFile.exists()) return; - Properties props = new Properties(); - try (InputStream in = new FileInputStream(schedulesFile)) { - props.load(in); - } catch (IOException e) { - plugin.getLogger().severe("[BroadcastModule] Konnte Schedules nicht laden: " + e.getMessage()); - return; - } - - // Bekannte Feld-Suffixe - Set knownFields = new HashSet<>(Arrays.asList( - "nextRunMillis", "sourceName", "message", "type", - "prefix", "prefixColor", "bracketColor", "messageColor", "recur" - )); - - Map loaded = new LinkedHashMap<>(); - for (String key : props.stringPropertyNames()) { - // Finde das letzte '.' das einen bekannten Feldnamen abtrennt - int lastDot = key.lastIndexOf('.'); - if (lastDot < 0) continue; - String field = key.substring(lastDot + 1); - if (!knownFields.contains(field)) continue; - String id = key.substring(0, lastDot); - if (id.isEmpty()) continue; - String value = props.getProperty(key); - - ScheduledBroadcast sb = loaded.computeIfAbsent(id, - k -> new ScheduledBroadcast(k, 0, "", "", "", "", "", "", "", "")); - - switch (field) { - case "nextRunMillis": try { sb.nextRunMillis = Long.parseLong(value); } catch (NumberFormatException ignored) {} break; - case "sourceName": sb.sourceName = value; break; - case "message": sb.message = value; break; - case "type": sb.type = value; break; - case "prefix": sb.prefix = value; break; - case "prefixColor": sb.prefixColor = value; break; - case "bracketColor": sb.bracketColor = value; break; - case "messageColor": sb.messageColor = value; break; - case "recur": sb.recur = value; break; - } - } - scheduledByClientId.putAll(loaded); - plugin.getLogger().info("[BroadcastModule] " + loaded.size() + " geplante Broadcasts aus Datei wiederhergestellt."); - } - - public boolean scheduleBroadcast(long timestampMillis, String sourceName, String message, String type, - String apiKeyHeader, String prefix, String prefixColor, String bracketColor, - String messageColor, String recur, String clientScheduleId) { - loadConfig(); - if (!enabled) return false; - if (requiredApiKey != null && !requiredApiKey.isEmpty()) { - if (apiKeyHeader == null || !requiredApiKey.equals(apiKeyHeader)) { - plugin.getLogger().warning("[BroadcastModule] schedule abgelehnt: API-Key fehlt oder inkorrekt."); - return false; - } - } - if (message == null) message = ""; - if (sourceName == null || sourceName.isEmpty()) sourceName = "System"; - if (type == null) type = "global"; - if (recur == null) recur = "none"; - - String id = (clientScheduleId != null && !clientScheduleId.trim().isEmpty()) - ? clientScheduleId.trim() : UUID.randomUUID().toString(); - - long now = System.currentTimeMillis(); - if (timestampMillis <= now) { - plugin.getLogger().warning("[BroadcastModule] Geplante Zeit in der Vergangenheit → sende sofort!"); - return handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); - } - - ScheduledBroadcast sb = new ScheduledBroadcast(id, timestampMillis, sourceName, message, type, - prefix, prefixColor, bracketColor, messageColor, recur); - scheduledByClientId.put(id, sb); - saveSchedules(); - plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id - + " @ " + dateFormat.format(new Date(timestampMillis))); - return true; - } - - public boolean cancelScheduled(String clientScheduleId) { - if (clientScheduleId == null || clientScheduleId.trim().isEmpty()) return false; - ScheduledBroadcast removed = scheduledByClientId.remove(clientScheduleId); - if (removed != null) { plugin.getLogger().info("[BroadcastModule] Schedule abgebrochen: " + clientScheduleId); saveSchedules(); return true; } - return false; - } - - private void processScheduled() { - if (scheduledByClientId.isEmpty()) return; - long now = System.currentTimeMillis(); - List toRemove = new ArrayList<>(); - boolean changed = false; - - for (Map.Entry entry : scheduledByClientId.entrySet()) { - ScheduledBroadcast sb = entry.getValue(); - if (sb.nextRunMillis <= now) { - plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ")"); - handleBroadcast(sb.sourceName, sb.message, sb.type, "", sb.prefix, sb.prefixColor, sb.bracketColor, sb.messageColor); - if (!"none".equalsIgnoreCase(sb.recur)) { - long next = computeNextMillis(sb.nextRunMillis, sb.recur); - if (next > 0) { sb.nextRunMillis = next; changed = true; } - else { toRemove.add(entry.getKey()); changed = true; } - } else { toRemove.add(entry.getKey()); changed = true; } - } - } - if (changed || !toRemove.isEmpty()) { - for (String k : toRemove) { scheduledByClientId.remove(k); } - saveSchedules(); - } - } - - private long computeNextMillis(long currentMillis, String recur) { - switch (recur.toLowerCase(Locale.ROOT)) { - case "hourly": return currentMillis + TimeUnit.HOURS.toMillis(1); - case "daily": return currentMillis + TimeUnit.DAYS.toMillis(1); - case "weekly": return currentMillis + TimeUnit.DAYS.toMillis(7); - default: return -1L; - } - } - - private static class ScheduledBroadcast { - final String clientId; - long nextRunMillis; - String sourceName, message, type, prefix, prefixColor, bracketColor, messageColor, recur; - - ScheduledBroadcast(String clientId, long nextRunMillis, String sourceName, String message, String type, - String prefix, String prefixColor, String bracketColor, String messageColor, String recur) { - this.clientId = clientId; - this.nextRunMillis = nextRunMillis; - this.sourceName = sourceName; - this.message = message; - this.type = type; - this.prefix = prefix; - this.prefixColor = prefixColor; - this.bracketColor = bracketColor; - this.messageColor = messageColor; - this.recur = recur == null ? "none" : recur; - } - } -} \ No newline at end of file