diff --git a/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java b/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java new file mode 100644 index 0000000..9228034 --- /dev/null +++ b/src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java @@ -0,0 +1,381 @@ +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.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 + * + * Speichert geplante Broadcasts jetzt persistent in 'broadcasts.schedules'. + * Beim Neustart werden diese automatisch wieder geladen. + */ +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"; // Neu + 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) { + plugin.getLogger().info("[BroadcastModule] deaktiviert via verify.properties (broadcast.enabled=false)"); + 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) { + plugin.getLogger().info("[BroadcastModule] deaktiviert."); + saveSchedules(); + scheduledByClientId.clear(); + } + + private void loadConfig() { + File file = new File(plugin.getDataFolder(), "verify.properties"); + if (!file.exists()) { + enabled = true; + requiredApiKey = ""; + format = "%prefix% %message%"; + fallbackPrefix = "[Broadcast]"; + fallbackPrefixColor = "&c"; + fallbackBracketColor = "&8"; // Neu + fallbackMessageColor = "&f"; + 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(); // Neu + 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) { + plugin.getLogger().info("[BroadcastModule] Broadcast abgelehnt: Modul ist deaktiviert."); + 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; // Neu + String usedMessageColor = (messageColor != null && !messageColor.trim().isEmpty()) ? messageColor.trim() : fallbackMessageColor; + + String prefixColorCode = normalizeColorCode(usedPrefixColor); + String bracketColorCode = normalizeColorCode(usedBracketColor); // Neu + String messageColorCode = normalizeColorCode(usedMessageColor); + + // --- KLAMMER LOGIK --- + String finalPrefix; + + // Wenn eine Klammerfarbe gesetzt ist, bauen wir den Prefix neu zusammen + // Format: [BracketColor][ [PrefixColor]Text [BracketColor]] + if (!bracketColorCode.isEmpty()) { + String textContent = usedPrefix; + // Entferne manuelle Klammern, falls der User [Broadcast] in das Textfeld geschrieben hat + 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 { + // Altes Verhalten: Ganzen String einfärben + finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET; + } + // --------------------- + + String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + message; + + String out = format.replace("%name%", sourceName) + .replace("%prefix%", finalPrefix) // Neu verwendete Variable + .replace("%prefixColored%", finalPrefix) // Fallback + .replace("%message%", message) + .replace("%messageColored%", coloredMessage) + .replace("%type%", type); + + if (!out.contains("%prefixColored%") && !out.contains("%messageColored%") && !out.contains("%prefix%") && !out.contains("%message%")) { + out = finalPrefix + " " + coloredMessage; + } + + TextComponent tc = new TextComponent(out); + int sent = 0; + for (ProxiedPlayer p : plugin.getProxy().getPlayers()) { + try { p.sendMessage(tc); sent++; } catch (Throwable ignored) {} + } + + plugin.getLogger().info("[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message); + return true; + } + + 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(); + 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); // Neu + 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()); + } + } + + private void loadSchedules() { + if (!schedulesFile.exists()) { + plugin.getLogger().info("[BroadcastModule] Keine bestehenden Schedules gefunden (Neustart)."); + 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; + } + + Map loaded = new HashMap<>(); + for (String key : props.stringPropertyNames()) { + if (!key.contains(".")) continue; + String[] parts = key.split("\\."); + if (parts.length != 2) continue; + + String id = parts[0]; + String field = parts[1]; + String value = props.getProperty(key); + + ScheduledBroadcast sb = loaded.get(id); + if (sb == null) { + sb = new ScheduledBroadcast(id, 0, "", "", "", "", "", "", "", ""); // Ein leerer String mehr für Bracket + loaded.put(id, sb); + } + + 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; // Neu + 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) { + plugin.getLogger().info("[BroadcastModule] schedule abgelehnt: Modul deaktiviert."); + 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(); + String scheduledTimeStr = dateFormat.format(new Date(timestampMillis)); + + plugin.getLogger().info("[BroadcastModule] Neue geplante Nachricht registriert: " + id + " @ " + scheduledTimeStr); + + if (timestampMillis <= now) { + plugin.getLogger().warning("[BroadcastModule] Geplante Zeit liegt 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(); + 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] Geplante Nachricht abgebrochen: id=" + 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) { + String timeStr = dateFormat.format(new Date(sb.nextRunMillis)); + plugin.getLogger().info("[BroadcastModule] ⏰ Sende geplante Nachricht (ID: " + entry.getKey() + ", Zeit: " + timeStr + ")"); + + 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; + String nextTimeStr = dateFormat.format(new Date(next)); + plugin.getLogger().info("[BroadcastModule] Nächste Wiederholung (" + sb.recur + "): " + nextTimeStr); + 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); + plugin.getLogger().info("[BroadcastModule] Schedule entfernt: " + 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; + String message; + String type; + String prefix; + String prefixColor; + String bracketColor; // Neu + String messageColor; + String 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; // Neu + this.messageColor = messageColor; + this.recur = (recur == null ? "none" : recur); + } + } +} \ No newline at end of file