diff --git a/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java new file mode 100644 index 0000000..14c16be --- /dev/null +++ b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java @@ -0,0 +1,378 @@ +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