From e1c6f8dfca8ecb5bb1023f3619b0b81421d51276 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sun, 1 Nov 2026 11:16:47 +0000 Subject: [PATCH] Upload via GUI (24 Dateien) --- minecraft-plugin/README.md | 11 ++- .../dist/DeskHub-Minecraft-1.1.0.jar | Bin 0 -> 49760 bytes minecraft-plugin/integration-test/pom.xml | 2 +- .../de/deskhub/minecraft/PaperPluginTest.java | 25 +++++- minecraft-plugin/pom.xml | 2 +- .../minecraft/bukkit/BukkitPlugin.java | 39 ++++++++- .../minecraft/bungee/BungeePlugin.java | 30 ++++++- .../deskhub/minecraft/core/OpenTickets.java | 78 ++++++++++++++++++ .../de/deskhub/minecraft/core/Settings.java | 5 ++ .../deskhub/minecraft/core/TicketClient.java | 18 +++- .../src/main/resources/config.yml | 5 ++ .../de/deskhub/minecraft/core/CoreTest.java | 51 ++++++++++++ 12 files changed, 255 insertions(+), 11 deletions(-) create mode 100644 minecraft-plugin/dist/DeskHub-Minecraft-1.1.0.jar create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/OpenTickets.java diff --git a/minecraft-plugin/README.md b/minecraft-plugin/README.md index 48addc35..156b9917 100644 --- a/minecraft-plugin/README.md +++ b/minecraft-plugin/README.md @@ -13,13 +13,18 @@ Ein Server, der abstürzt, kann nicht mehr selbst "Hilfe" rufen. Deshalb gibt es Gegen Ticket-Fluten: ein einzelner Aussetzer macht kein Ticket, pro Ausfall höchstens eines, und pro Server frühestens nach der eingestellten Wartezeit (30 Minuten) wieder eines. Server, die seit dem Start des Proxys nie online waren (z. B. absichtlich abgeschaltet), werden ignoriert. +**Automatisch lösen:** Das Plugin setzt das Ausfall-Ticket in DeskHub auf „Gelöst“ und hinterlässt eine Notiz mit der Ausfalldauer. Die Ticketnummer merkt es sich in `offene-tickets.properties`, auch ein Neustart währenddessen schadet nicht. Abschalten mit `ticket.resolve-on-recovery: false`. Voraussetzung: DeskHub **1.0.48 oder neuer** (API-Aktion „resolve“) und der API-Schlüssel mit dem Recht „Tickets anlegen“; gelöst werden nur Tickets, die dieser Schlüssel über die API angelegt hat. + +- **BungeeCord:** Sobald der Server wieder auf Pings antwortet. +- **Spigot/Paper:** Beim nächsten Start. Ein Absturz-Ticket wird dort gleich nach dem Anlegen gelöst (der Server läuft ja wieder, die Notiz nennt die Ausfalldauer). Ein Herunterfahren-Ticket (falls eingeschaltet) wird beim nächsten Start gelöst. + Normales Herunterfahren (`/stop`, geplanter Neustart) erzeugt **kein** Ticket (auf Wunsch einstellbar). ## Installation 1. **API-Schlüssel in DeskHub:** Adminportal → Verwalten → **API-Schlüssel** → neuen Schlüssel mit dem Recht **„Tickets anlegen“** erzeugen (er wird nur einmal angezeigt). Trage unter „IP-Beschränkung“ die IP-Adresse deines Minecraft-Servers ein, dann ist der Schlüssel nur von dort nutzbar. 2. **Abteilung und Priorität:** Öffne im Adminportal die Abteilung bzw. Priorität. Die Zahl am Ende der Adresse (`…?id=4`) ist die ID für die config. -3. **Jar kopieren:** `DeskHub-Minecraft-1.0.0.jar` in den Ordner `plugins` deines Servers bzw. Proxys. Server einmal starten, damit `plugins/DeskHub-Minecraft/config.yml` entsteht. +3. **Jar kopieren:** `DeskHub-Minecraft-1.1.0.jar` in den Ordner `plugins` deines Servers bzw. Proxys. Server einmal starten, damit `plugins/DeskHub-Minecraft/config.yml` entsteht. 4. **config.yml ausfüllen** (mindestens `url`, `api-key`, `requester-email`; optional `department-id`, `priority-id`), dann `/deskhubmc reload`. 5. **Testen:** `/deskhubmc test` legt ein Testticket an und zeigt die Ticketnummer oder den Grund für einen Fehler. @@ -70,7 +75,7 @@ Voraussetzungen: JDK 17 oder neuer, Maven. ```bash cd minecraft-plugin -mvn package # Tests und Jar: target/DeskHub-Minecraft-1.0.0.jar +mvn package # Tests und Jar: target/DeskHub-Minecraft-1.1.0.jar ``` Zusätzlicher Test des Spigot/Paper-Teils gegen einen simulierten Paper-Server (braucht JDK 21): @@ -83,6 +88,6 @@ mvn test ## Grenzen -- Auf einem **einzelnen** Spigot/Paper-Server ohne Proxy kommt das Ticket erst, wenn der Server wieder startet. Wer eine Meldung in Echtzeit braucht, lässt zusätzlich das Plugin auf dem Proxy laufen oder überwacht den Server von außen. +- Auf einem **einzelnen** Spigot/Paper-Server ohne Proxy kommt das Ticket erst, wenn der Server wieder startet (und wird dann gleich gelöst). Wer eine Meldung in Echtzeit braucht, lässt zusätzlich das Plugin auf dem Proxy laufen oder überwacht den Server von außen. - **Velocity** wird nicht unterstützt (eigenes Plugin-System). Auf Waterfall läuft die BungeeCord-Variante. - Das Plugin ist kompiliert für Java 8 und die Spigot-API 1.16.5. Es sollte auf älteren und neueren Versionen laufen, geprüft sind Paper 1.21.11 (simuliert) und BungeeCord (echt). diff --git a/minecraft-plugin/dist/DeskHub-Minecraft-1.1.0.jar b/minecraft-plugin/dist/DeskHub-Minecraft-1.1.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..27ec92395008ba2f724f068a45be0f5ef1e31563 GIT binary patch literal 49760 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

{k4?Hm_#MYcmk`Mi{=HC62Bc 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 de.deskhub deskhub-minecraft - 1.0.0 + 1.1.0 test ** diff --git a/minecraft-plugin/integration-test/src/test/java/de/deskhub/minecraft/PaperPluginTest.java b/minecraft-plugin/integration-test/src/test/java/de/deskhub/minecraft/PaperPluginTest.java index bbf54d1e..0c7e30aa 100644 --- a/minecraft-plugin/integration-test/src/test/java/de/deskhub/minecraft/PaperPluginTest.java +++ b/minecraft-plugin/integration-test/src/test/java/de/deskhub/minecraft/PaperPluginTest.java @@ -91,12 +91,35 @@ class PaperPluginTest { configure(""); writeHeartbeat(true, false, System.currentTimeMillis() - 5 * 60_000L); enable(); - assertEquals(1, bodies.size(), "genau ein Ticket"); + assertEquals(2, bodies.size(), "ein Ticket und gleich danach seine Lösung (der Server läuft ja wieder)"); String b = bodies.get(0); assertEquals(KEY, keys.get(0)); assertTrue(b.contains("\"departmentId\":4") && b.contains("\"priorityId\":3"), b); assertTrue(b.contains("war nicht erreichbar"), b); assertTrue(b.contains("Ausfalldauer: ca. 5 Minuten"), b); + String r = bodies.get(1); + assertTrue(r.contains("\"action\":\"resolve\"") && r.contains("\"number\":\"T-1\"") && r.contains("\"email\":\"mc@example.de\""), r); + assertTrue(r.contains("wieder erreichbar"), r); + } + + @Test + void crashTicketStaysOpenWhenResolvingIsOff() throws Exception { + configure(" resolve-on-recovery: false\n"); + writeHeartbeat(true, false, System.currentTimeMillis() - 60_000L); + enable(); + assertEquals(1, bodies.size(), "nur das Ticket, keine Lösung"); + } + + @Test + void shutdownTicketIsResolvedOnNextStart() throws Exception { + configure("backend:\n ticket-on-shutdown: true\n"); + enable(); + server.getPluginManager().disablePlugin(plugin); + assertEquals(1, bodies.size()); + assertTrue(new File(plugin.getDataFolder(), "offene-tickets.properties").isFile(), "Ticketnummer wird gemerkt"); + enable(); + assertEquals(2, bodies.size(), "beim Start wird das Herunterfahren-Ticket gelöst"); + assertTrue(bodies.get(1).contains("\"action\":\"resolve\"") && bodies.get(1).contains("\"number\":\"T-1\""), bodies.get(1)); } @Test diff --git a/minecraft-plugin/pom.xml b/minecraft-plugin/pom.xml index 41a6a32d..8387f67f 100644 --- a/minecraft-plugin/pom.xml +++ b/minecraft-plugin/pom.xml @@ -5,7 +5,7 @@ de.deskhub deskhub-minecraft - 1.0.0 + 1.1.0 jar DeskHub-Minecraft Legt in DeskHub ein Ticket an, wenn ein Minecraft-Server offline geht. Eine Jar für Spigot, Paper und BungeeCord. diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitPlugin.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitPlugin.java index d25bcfb4..4cd88709 100644 --- a/minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitPlugin.java +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitPlugin.java @@ -1,6 +1,7 @@ package de.deskhub.minecraft.bukkit; import de.deskhub.minecraft.core.HeartbeatFile; +import de.deskhub.minecraft.core.OpenTickets; import de.deskhub.minecraft.core.Settings; import de.deskhub.minecraft.core.Template; import de.deskhub.minecraft.core.TicketClient; @@ -28,6 +29,7 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor { private Settings settings; private TicketClient client; private HeartbeatFile heartbeat; + private OpenTickets openTickets; private BukkitTask beatTask; @Override @@ -37,7 +39,9 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor { if (getCommand("deskhubmc") != null) getCommand("deskhubmc").setExecutor(this); heartbeat = new HeartbeatFile(new File(getDataFolder(), "lebenszeichen.properties")); HeartbeatFile.Previous previous = heartbeat.readPrevious(); + openTickets = new OpenTickets(new File(getDataFolder(), "offene-tickets.properties")); long now = System.currentTimeMillis(); + resolveOpenOnStart(now); try { heartbeat.start(now); } catch (IOException e) { @@ -70,6 +74,7 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor { // Beim Beenden darf gewartet werden: der Server wartet auf das Plugin, solange es nicht länger als das Zeitlimit dauert Map v = Template.vars(serverName(), "Plugin auf dem Server", now); TicketResult r = client.create(Template.apply(settings.subjectShutdown, v), Template.apply(settings.messageShutdown, v)); + if (r.ok) openTickets.put(serverName(), r.number, now); getLogger().info(r.ok ? "Ticket " + r.number + " angelegt (Herunterfahren)." : "Ticket nicht angelegt: " + r.error); } } @@ -104,12 +109,42 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor { v.put("lastSeen", previous.lastBeat > 0 ? Template.formatTime(previous.lastBeat) : "unbekannt"); v.put("downtime", previous.lastBeat > 0 ? Template.formatDuration(now - previous.lastBeat) : "unbekannt"); TicketResult r = client.create(Template.apply(settings.subjectCrash, v), Template.apply(settings.messageCrash, v)); - if (r.ok) getLogger().info("Ticket " + r.number + " angelegt."); - else getLogger().warning("Ticket nicht angelegt: " + r.error); + if (r.ok) { + getLogger().info("Ticket " + r.number + " angelegt."); + // Der Server läuft wieder (sonst würde dieser Code nicht laufen): Das Ausfall-Ticket gleich lösen + resolveNow(r.number, previous.lastBeat > 0 ? previous.lastBeat : 0, now); + } else getLogger().warning("Ticket nicht angelegt: " + r.error); } }); } + /** Beim Start: Ein beim letzten Herunterfahren angelegtes Ticket ist erledigt, der Server läuft wieder. */ + private void resolveOpenOnStart(final long now) { + final String number = openTickets.get(serverName()); + if (number == null || !settings.resolveOnRecovery || !settings.problems().isEmpty()) return; + final long since = openTickets.since(serverName()); + Bukkit.getScheduler().runTaskAsynchronously(this, new Runnable() { + @Override + public void run() { + resolveNow(number, since, now); + } + }); + } + + /** Blockierend (nur in einem Hintergrund-Thread aufrufen): Ticket auf Gelöst setzen und aus der Merkliste nehmen. */ + private void resolveNow(String number, long since, long now) { + if (!settings.resolveOnRecovery || !settings.problems().isEmpty()) return; + Map v = Template.vars(serverName(), "Plugin auf dem Server", now); + v.put("downtime", since > 0 ? Template.formatDuration(now - since) : "unbekannt"); + TicketResult r = client.resolve(number, Template.apply(settings.messageRecovered, v)); + if (r.ok || r.httpCode == 404) { + openTickets.remove(serverName()); + getLogger().info(r.ok ? "Ticket " + number + " gelöst." : "Ticket " + number + " gibt es in DeskHub nicht mehr, wird nicht weiter verfolgt."); + } else { + getLogger().warning("Ticket " + number + " konnte nicht gelöst werden: " + r.error + " (bitte von Hand lösen)"); + } + } + @Override public boolean onCommand(final CommandSender sender, Command command, String label, String[] args) { if (!sender.hasPermission("deskhubmc.admin")) { diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeePlugin.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeePlugin.java index 01352058..26477c8f 100644 --- a/minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeePlugin.java +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeePlugin.java @@ -1,5 +1,6 @@ package de.deskhub.minecraft.bungee; +import de.deskhub.minecraft.core.OpenTickets; import de.deskhub.minecraft.core.OutageTracker; import de.deskhub.minecraft.core.Settings; import de.deskhub.minecraft.core.Template; @@ -36,6 +37,7 @@ public final class BungeePlugin extends Plugin { private Settings settings; private TicketClient client; private OutageTracker tracker; + private OpenTickets openTickets; private ScheduledTask watchdog; @Override @@ -69,6 +71,7 @@ public final class BungeePlugin extends Plugin { settings = new Settings(new BungeeConfig(new Configuration())); } client = new TicketClient(settings, getDescription().getVersion()); + openTickets = new OpenTickets(new File(getDataFolder(), "offene-tickets.properties")); tracker = new OutageTracker(settings.monitorFailures, settings.cooldownMinutes, settings.ignoreNeverOnline); for (String w : settings.warnings()) getLogger().warning(w); List problems = settings.problems(); @@ -115,6 +118,7 @@ public final class BungeePlugin extends Plugin { OutageTracker.Decision d = tracker.report(server, online, now); if (d == OutageTracker.Decision.RECOVERED) { getLogger().info("Server \"" + server + "\" ist wieder erreichbar."); + resolveTicket(server, now); } else if (d == OutageTracker.Decision.OPEN_TICKET) { final long since = tracker.offlineSince(server); getLogger().warning("Server \"" + server + "\" ist nicht erreichbar. Es wird ein Ticket angelegt."); @@ -127,13 +131,37 @@ public final class BungeePlugin extends Plugin { public void run() { Map v = Template.vars(server, "BungeeCord-Proxy", since > 0 ? since : now); TicketResult r = client.create(Template.apply(settings.subjectOffline, v), Template.apply(settings.messageOffline, v)); - if (r.ok) getLogger().info("Ticket " + r.number + " angelegt."); + if (r.ok) { + openTickets.put(server, r.number, since > 0 ? since : now); + getLogger().info("Ticket " + r.number + " angelegt."); + } else getLogger().warning("Ticket nicht angelegt: " + r.error); } }); } } + /** Server wieder erreichbar: das beim Ausfall angelegte Ticket in DeskHub auf "Gelöst" setzen. */ + private void resolveTicket(final String server, final long now) { + final String number = openTickets.get(server); + if (number == null || !settings.resolveOnRecovery || !settings.problems().isEmpty()) return; + final long since = openTickets.since(server); + getProxy().getScheduler().runAsync(this, new Runnable() { + @Override + public void run() { + Map v = Template.vars(server, "BungeeCord-Proxy", now); + v.put("downtime", since > 0 ? Template.formatDuration(now - since) : "unbekannt"); + TicketResult r = client.resolve(number, Template.apply(settings.messageRecovered, v)); + if (r.ok || r.httpCode == 404) { + openTickets.remove(server); + getLogger().info(r.ok ? "Ticket " + number + " gelöst." : "Ticket " + number + " gibt es in DeskHub nicht mehr, wird nicht weiter verfolgt."); + } else { + getLogger().warning("Ticket " + number + " konnte nicht gelöst werden: " + r.error + " (bitte von Hand lösen)"); + } + } + }); + } + private final class McCommand extends Command { McCommand() { super("deskhubmc", "deskhubmc.admin", "dhmc"); diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/OpenTickets.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/OpenTickets.java new file mode 100644 index 00000000..59c3ada0 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/OpenTickets.java @@ -0,0 +1,78 @@ +package de.deskhub.minecraft.core; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; + +/** + * Merkt sich je Server die Nummer des Tickets, das für den aktuellen Ausfall angelegt wurde, damit es bei "wieder erreichbar" gelöst + * werden kann. Wird in einer kleinen Datei gespeichert, damit ein Neustart des Proxys während des Ausfalls das Ticket nicht vergisst. + */ +public final class OpenTickets { + private final File file; + private final Properties props = new Properties(); + + public OpenTickets(File file) { + this.file = file; + if (file.isFile()) { + try { + InputStream in = new FileInputStream(file); + try { + props.load(in); + } finally { + in.close(); + } + } catch (IOException e) { + props.clear(); + } + } + } + + /** Ticketnummer des laufenden Ausfalls oder null. */ + public synchronized String get(String server) { + String v = props.getProperty(server); + if (v == null) return null; + int i = v.indexOf(';'); + return i < 0 ? v : v.substring(0, i); + } + + /** Beginn des Ausfalls in Millisekunden (0 = unbekannt). */ + public synchronized long since(String server) { + String v = props.getProperty(server); + int i = v == null ? -1 : v.indexOf(';'); + if (i < 0) return 0; + try { + return Long.parseLong(v.substring(i + 1)); + } catch (NumberFormatException e) { + return 0; + } + } + + public synchronized void put(String server, String number, long sinceMillis) { + props.setProperty(server, number + ";" + sinceMillis); + save(); + } + + public synchronized void remove(String server) { + if (props.remove(server) != null) save(); + } + + private void save() { + try { + File dir = file.getParentFile(); + if (dir != null && !dir.isDirectory() && !dir.mkdirs()) return; + OutputStream out = new FileOutputStream(file); + try { + props.store(out, "DeskHub-Minecraft: offene Ausfall-Tickets (Server = Ticketnummer;Beginn)"); + } finally { + out.close(); + } + } catch (IOException e) { + // Speichern ist nachrangig: Das Ticket wird dann nur nach einem Proxy-Neustart nicht automatisch gelöst + } + } +} diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Settings.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Settings.java index c3030287..c0d6bf15 100644 --- a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Settings.java +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Settings.java @@ -28,6 +28,8 @@ public final class Settings { public final String messageShutdown; public final String subjectTest; public final String messageTest; + public final boolean resolveOnRecovery; + public final String messageRecovered; public final int monitorIntervalSeconds; public final int monitorFailures; @@ -65,6 +67,9 @@ public final class Settings { this.subjectTest = c.getString("ticket.test.subject", "[Minecraft] Testticket von {server}"); this.messageTest = c.getString("ticket.test.message", "Das ist ein Testticket des DeskHub-Minecraft-Plugins. Die Verbindung zu DeskHub funktioniert.\n\nServer: {server}\nZeit: {time}"); + this.resolveOnRecovery = c.getBoolean("ticket.resolve-on-recovery", true); + this.messageRecovered = c.getString("ticket.recovered.message", "Der Minecraft-Server \"{server}\" ist seit {time} wieder erreichbar (Ausfall: {downtime}). Das Ticket wurde automatisch gelöst."); + this.monitorIntervalSeconds = clamp(c.getInt("monitor.interval-seconds", 30), 5, 3600); this.monitorFailures = clamp(c.getInt("monitor.failures-before-ticket", 3), 1, 100); this.cooldownMinutes = clamp(c.getInt("monitor.cooldown-minutes", 30), 0, 10080); diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketClient.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketClient.java index cf9b08b3..96943a94 100644 --- a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketClient.java +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketClient.java @@ -8,7 +8,7 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -/** Legt über die öffentliche Ticket-API von DeskHub (POST /api/public/v1/tickets.php) ein Ticket an. Blockierend: nie im Haupt-Thread aufrufen. */ +/** Legt über die öffentliche Ticket-API von DeskHub (POST /api/public/v1/tickets.php) ein Ticket an oder löst es wieder. Blockierend: nie im Haupt-Thread aufrufen. */ public final class TicketClient { private static final int MAX_RESPONSE = 65536; private final Settings settings; @@ -32,7 +32,21 @@ public final class TicketClient { return b.append('}').toString(); } + /** Baut den JSON-Körper für "Ticket lösen" (öffentlich für Tests). */ + public String buildResolveBody(String number, String message) { + return "{\"action\":\"resolve\",\"number\":" + Json.quote(number) + ",\"email\":" + Json.quote(settings.requesterEmail) + ",\"message\":" + Json.quote(clip(message, 2000)) + "}"; + } + public TicketResult create(String subject, String message) { + return send(buildBody(subject, message)); + } + + /** Setzt ein vom Plugin angelegtes Ticket auf "Gelöst" (Server wieder erreichbar). */ + public TicketResult resolve(String number, String message) { + return send(buildResolveBody(number, message)); + } + + private TicketResult send(String json) { if (!settings.problems().isEmpty()) { return TicketResult.failure(0, "Einstellungen unvollständig: " + settings.problems().get(0)); } @@ -49,7 +63,7 @@ public final class TicketClient { c.setRequestProperty("Accept", "application/json"); c.setRequestProperty("X-API-Key", settings.apiKey); c.setRequestProperty("User-Agent", userAgent); - byte[] data = buildBody(subject, message).getBytes(StandardCharsets.UTF_8); + byte[] data = json.getBytes(StandardCharsets.UTF_8); c.setFixedLengthStreamingMode(data.length); OutputStream out = c.getOutputStream(); try { diff --git a/minecraft-plugin/src/main/resources/config.yml b/minecraft-plugin/src/main/resources/config.yml index 828fed45..3b68918a 100644 --- a/minecraft-plugin/src/main/resources/config.yml +++ b/minecraft-plugin/src/main/resources/config.yml @@ -47,6 +47,11 @@ ticket: Ausfalldauer: ca. {downtime} Bitte die Logdateien prüfen. + # Ist der Server wieder erreichbar (BungeeCord) bzw. neu gestartet (Spigot/Paper), wird das Ticket automatisch auf "Gelöst" gesetzt und bekommt diese Notiz. + # Ausschalten mit false (dann bleibt das Ticket offen, bis ihr es selbst löst). + resolve-on-recovery: true + recovered: + message: 'Der Minecraft-Server "{server}" ist seit {time} wieder erreichbar (Ausfall: {downtime}). Das Ticket wurde automatisch gelöst.' shutdown: subject: "[Minecraft] {server} wurde heruntergefahren" message: 'Der Minecraft-Server "{server}" wurde um {time} heruntergefahren.' diff --git a/minecraft-plugin/src/test/java/de/deskhub/minecraft/core/CoreTest.java b/minecraft-plugin/src/test/java/de/deskhub/minecraft/core/CoreTest.java index 5b54a2ed..0f55e2d3 100644 --- a/minecraft-plugin/src/test/java/de/deskhub/minecraft/core/CoreTest.java +++ b/minecraft-plugin/src/test/java/de/deskhub/minecraft/core/CoreTest.java @@ -149,6 +149,57 @@ class CoreTest { assertDoesNotThrow(() -> new HeartbeatFile(f).readPrevious()); } + @Test + void ticketIsResolvedWhenServerIsBack() throws Exception { + final List bodies = new ArrayList(); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/api/public/v1/tickets.php", ex -> { + bodies.add(readAll(ex.getRequestBody())); + byte[] out = "{\"number\":\"T-4711\",\"status\":\"resolved\"}".getBytes(StandardCharsets.UTF_8); + ex.sendResponseHeaders(200, out.length); + ex.getResponseBody().write(out); + ex.close(); + }); + server.createContext("/weg/api/public/v1/tickets.php", ex -> { + byte[] out = "{\"error\":{\"code\":\"NOT_FOUND\",\"message\":\"Ticket nicht gefunden.\"}}".getBytes(StandardCharsets.UTF_8); + ex.sendResponseHeaders(404, out.length); + ex.getResponseBody().write(out); + ex.close(); + }); + server.start(); + try { + String base = "http://127.0.0.1:" + server.getAddress().getPort(); + TicketResult r = new TicketClient(new Settings(cfg(valid(base))), "1").resolve("T-4711", "Server \"Lobby\" wieder erreichbar"); + assertTrue(r.ok, r.toString()); + String b = bodies.get(0); + assertTrue(b.contains("\"action\":\"resolve\"") && b.contains("\"number\":\"T-4711\"") && b.contains("\"email\":\"mc@example.de\""), b); + assertTrue(b.contains("\"message\":\"Server \\\"Lobby\\\" wieder erreichbar\""), b); + TicketResult gone = new TicketClient(new Settings(cfg(valid(base + "/weg"))), "1").resolve("T-1", "x"); + assertFalse(gone.ok); + assertEquals(404, gone.httpCode); + } finally { + server.stop(0); + } + Map off = valid("https://x.de"); + assertTrue(new Settings(cfg(off)).resolveOnRecovery); + off.put("ticket.resolve-on-recovery", false); + assertFalse(new Settings(cfg(off)).resolveOnRecovery); + } + + @Test + void openTicketsSurviveRestart() throws Exception { + java.io.File f = java.io.File.createTempFile("offene-tickets", ".properties"); + f.deleteOnExit(); + OpenTickets a = new OpenTickets(f); + a.put("lobby", "T-9", 123456L); + OpenTickets b = new OpenTickets(f); + assertEquals("T-9", b.get("lobby")); + assertEquals(123456L, b.since("lobby")); + assertEquals(null, b.get("andere")); + b.remove("lobby"); + assertEquals(null, new OpenTickets(f).get("lobby")); + } + @Test void ticketIsSentToDeskHub() throws Exception { final List bodies = new ArrayList();