Delete src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java via Git Manager GUI

This commit is contained in:
2026-05-07 19:49:36 +00:00
parent ed5758f816
commit a3d863ef82

View File

@@ -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<String, ScheduledBroadcast> 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<String, ScheduledBroadcast> 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<String> knownFields = new HashSet<>(Arrays.asList(
"nextRunMillis", "sourceName", "message", "type",
"prefix", "prefixColor", "bracketColor", "messageColor", "recur"
));
Map<String, ScheduledBroadcast> 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<String> toRemove = new ArrayList<>();
boolean changed = false;
for (Map.Entry<String, ScheduledBroadcast> 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;
}
}
}