Delete src/main/java/net/viper/status/modules/broadcast/BroadcastModule.java via Git Manager GUI
This commit is contained in:
@@ -1,386 +0,0 @@
|
|||||||
package net.viper.status.modules.broadcast;
|
|
||||||
|
|
||||||
import net.viper.status.StatusAPI;
|
|
||||||
import net.md_5.bungee.api.ChatColor;
|
|
||||||
import net.viper.status.StatusAPI;
|
|
||||||
import net.md_5.bungee.api.plugin.Plugin;
|
|
||||||
import net.viper.status.StatusAPI;
|
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
|
||||||
import net.viper.status.StatusAPI;
|
|
||||||
import net.md_5.bungee.api.chat.BaseComponent;
|
|
||||||
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.TextComponent;
|
|
||||||
import net.viper.status.StatusAPI;
|
|
||||||
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().fine("[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) {}
|
|
||||||
}
|
|
||||||
StatusAPI.debugLog(plugin, "[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().fine("[BroadcastModule] geplante Broadcasts 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();
|
|
||||||
StatusAPI.debugLog(plugin, "[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) { StatusAPI.debugLog(plugin, "[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) {
|
|
||||||
StatusAPI.debugLog(plugin, "[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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user