From 8385fdabf2f35ffa44b162dc8a52a25134c64023 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Wed, 21 Oct 2026 16:45:36 +0000 Subject: [PATCH] Upload via GUI (67 Dateien) --- minecraft-plugin/.gitignore | 2 + minecraft-plugin/README.md | 88 +++++++ .../dist/DeskHub-Minecraft-1.0.0.jar | Bin 0 -> 44027 bytes minecraft-plugin/integration-test/pom.xml | 59 +++++ .../de/deskhub/minecraft/PaperPluginTest.java | 162 ++++++++++++ .../default-testCompile/createdFiles.lst | 1 + .../default-testCompile/inputFiles.lst | 1 + .../2026-10-21T13-17-31_719-jvmRun1.dump | 17 ++ ...2026-10-21T13-17-31_719-jvmRun1.dumpstream | 3 + ...T-de.deskhub.minecraft.PaperPluginTest.xml | 121 +++++++++ .../de.deskhub.minecraft.PaperPluginTest.txt | 4 + .../deskhub/minecraft/PaperPluginTest.class | Bin 0 -> 10062 bytes minecraft-plugin/pom.xml | 93 +++++++ .../minecraft/bukkit/BukkitConfig.java | 35 +++ .../minecraft/bukkit/BukkitPlugin.java | 159 +++++++++++ .../minecraft/bungee/BungeeConfig.java | 35 +++ .../minecraft/bungee/BungeePlugin.java | 186 +++++++++++++ .../deskhub/minecraft/core/ConfigSource.java | 14 + .../deskhub/minecraft/core/HeartbeatFile.java | 102 +++++++ .../java/de/deskhub/minecraft/core/Json.java | 74 ++++++ .../deskhub/minecraft/core/OutageTracker.java | 75 ++++++ .../de/deskhub/minecraft/core/Settings.java | 107 ++++++++ .../de/deskhub/minecraft/core/Template.java | 48 ++++ .../deskhub/minecraft/core/TicketClient.java | 93 +++++++ .../deskhub/minecraft/core/TicketResult.java | 29 ++ .../src/main/resources/bungee.yml | 5 + .../src/main/resources/config.yml | 83 ++++++ .../src/main/resources/plugin.yml | 16 ++ .../de/deskhub/minecraft/core/CoreTest.java | 248 ++++++++++++++++++ minecraft-plugin/target/classes/bungee.yml | 5 + minecraft-plugin/target/classes/config.yml | 83 ++++++ .../minecraft/bukkit/BukkitConfig.class | Bin 0 -> 1262 bytes .../minecraft/bukkit/BukkitPlugin$1.class | Bin 0 -> 995 bytes .../minecraft/bukkit/BukkitPlugin$2.class | Bin 0 -> 3056 bytes .../minecraft/bukkit/BukkitPlugin$3$1.class | Bin 0 -> 1432 bytes .../minecraft/bukkit/BukkitPlugin$3.class | Bin 0 -> 2402 bytes .../minecraft/bukkit/BukkitPlugin.class | Bin 0 -> 9767 bytes .../minecraft/bungee/BungeeConfig.class | Bin 0 -> 1229 bytes .../minecraft/bungee/BungeePlugin$1.class | Bin 0 -> 696 bytes .../minecraft/bungee/BungeePlugin$2.class | Bin 0 -> 1546 bytes .../minecraft/bungee/BungeePlugin$3.class | Bin 0 -> 2430 bytes .../bungee/BungeePlugin$McCommand$1.class | Bin 0 -> 2659 bytes .../bungee/BungeePlugin$McCommand.class | Bin 0 -> 4913 bytes .../minecraft/bungee/BungeePlugin.class | Bin 0 -> 9590 bytes .../deskhub/minecraft/core/ConfigSource.class | Bin 0 -> 440 bytes .../core/HeartbeatFile$Previous.class | Bin 0 -> 701 bytes .../minecraft/core/HeartbeatFile.class | Bin 0 -> 3666 bytes .../de/deskhub/minecraft/core/Json.class | Bin 0 -> 2878 bytes .../minecraft/core/OutageTracker$1.class | Bin 0 -> 239 bytes .../core/OutageTracker$Decision.class | Bin 0 -> 1304 bytes 50 files changed, 1948 insertions(+) create mode 100644 minecraft-plugin/.gitignore create mode 100644 minecraft-plugin/README.md create mode 100644 minecraft-plugin/dist/DeskHub-Minecraft-1.0.0.jar create mode 100644 minecraft-plugin/integration-test/pom.xml create mode 100644 minecraft-plugin/integration-test/src/test/java/de/deskhub/minecraft/PaperPluginTest.java create mode 100644 minecraft-plugin/integration-test/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst create mode 100644 minecraft-plugin/integration-test/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst create mode 100644 minecraft-plugin/integration-test/target/surefire-reports/2026-10-21T13-17-31_719-jvmRun1.dump create mode 100644 minecraft-plugin/integration-test/target/surefire-reports/2026-10-21T13-17-31_719-jvmRun1.dumpstream create mode 100644 minecraft-plugin/integration-test/target/surefire-reports/TEST-de.deskhub.minecraft.PaperPluginTest.xml create mode 100644 minecraft-plugin/integration-test/target/surefire-reports/de.deskhub.minecraft.PaperPluginTest.txt create mode 100644 minecraft-plugin/integration-test/target/test-classes/de/deskhub/minecraft/PaperPluginTest.class create mode 100644 minecraft-plugin/pom.xml create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitConfig.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitPlugin.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeeConfig.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeePlugin.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/ConfigSource.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/HeartbeatFile.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Json.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/OutageTracker.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Settings.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Template.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketClient.java create mode 100644 minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketResult.java create mode 100644 minecraft-plugin/src/main/resources/bungee.yml create mode 100644 minecraft-plugin/src/main/resources/config.yml create mode 100644 minecraft-plugin/src/main/resources/plugin.yml create mode 100644 minecraft-plugin/src/test/java/de/deskhub/minecraft/core/CoreTest.java create mode 100644 minecraft-plugin/target/classes/bungee.yml create mode 100644 minecraft-plugin/target/classes/config.yml create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitConfig.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin$1.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin$2.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin$3$1.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin$3.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeeConfig.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$1.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$2.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$3.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$McCommand$1.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$McCommand.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/core/ConfigSource.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/core/HeartbeatFile$Previous.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/core/HeartbeatFile.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/core/Json.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/core/OutageTracker$1.class create mode 100644 minecraft-plugin/target/classes/de/deskhub/minecraft/core/OutageTracker$Decision.class diff --git a/minecraft-plugin/.gitignore b/minecraft-plugin/.gitignore new file mode 100644 index 00000000..d3cbc9a4 --- /dev/null +++ b/minecraft-plugin/.gitignore @@ -0,0 +1,2 @@ +target/ +integration-test/target/ diff --git a/minecraft-plugin/README.md b/minecraft-plugin/README.md new file mode 100644 index 00000000..48addc35 --- /dev/null +++ b/minecraft-plugin/README.md @@ -0,0 +1,88 @@ +# DeskHub-Minecraft + +Legt in **DeskHub** automatisch ein Ticket an, wenn dein Minecraft-Server offline geht. **Eine Jar** für **Spigot, Paper und BungeeCord**. Abteilung und Priorität stellst du in der `config.yml` ein. + +## Wie der Ausfall erkannt wird + +Ein Server, der abstürzt, kann nicht mehr selbst "Hilfe" rufen. Deshalb gibt es zwei Wege, die du am besten kombinierst: + +| Wo das Plugin liegt | Was es tut | Wann das Ticket kommt | +|---|---|---| +| **BungeeCord-Proxy** (empfohlen) | Prüft deine Server von außen (Server-Ping, alle 30 s) | Nach 3 Fehlschlägen in Folge, also nach rund 1,5 Minuten, in Echtzeit | +| **Spigot / Paper** (jeder Server) | Schreibt regelmäßig ein Lebenszeichen und markiert es beim normalen Beenden | Beim **nächsten Start**, wenn der Server zuvor abgestürzt, hart beendet oder vom Strom getrennt wurde | + +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. + +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. +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. + +Der Absender der Tickets (`requester-email`) bekommt von DeskHub wie bei jedem Ticket eine Eingangsbestätigung. Nimm deshalb eine Adresse, die bei dir ankommt (z. B. eine Sammeladresse für Technik). + +## Befehle + +| Befehl | Wirkung | +|---|---| +| `/deskhubmc test` | Testticket anlegen | +| `/deskhubmc status` | Einstellungen und (auf BungeeCord) den Zustand der Server anzeigen | +| `/deskhubmc reload` | `config.yml` neu laden | + +Alias: `/dhmc`. Berechtigung: `deskhubmc.admin` (Standard: nur Operatoren; in der Konsole immer erlaubt). + +## Wichtige Einstellungen + +```yaml +deskhub: + url: "https://support.example.de" + api-key: "dh_…" + requester-email: "minecraft@example.de" +ticket: + department-id: 4 # 0 = Standard von DeskHub + priority-id: 3 +monitor: # nur BungeeCord + interval-seconds: 30 + failures-before-ticket: 3 + cooldown-minutes: 30 + servers: [] # leer = alle Server des Proxys +backend: # nur Spigot/Paper + ticket-on-crash: true + ticket-on-shutdown: false +``` + +Betreff und Text der Tickets sind in der `config.yml` anpassbar (Platzhalter `{server}`, `{time}`, `{source}`, `{lastSeen}`, `{downtime}`). + +## Sicherheit + +- Der API-Schlüssel steht in der `config.yml`. Dateirechte so setzen, dass nur der Server-Benutzer sie lesen kann, und die Datei nie weitergeben. Bei Verdacht den Schlüssel in DeskHub löschen und einen neuen erzeugen. +- Nutze **https**. Bei `http://` warnt das Plugin, weil der Schlüssel dann unverschlüsselt übertragen würde. +- Das Plugin folgt keinen Weiterleitungen, damit der Schlüssel nie an eine andere Adresse geht. +- DeskHub begrenzt die API auf 60 Tickets pro Minute und wendet eure Ticket-Regeln an. + +## Bauen + +Voraussetzungen: JDK 17 oder neuer, Maven. + +```bash +cd minecraft-plugin +mvn package # Tests und Jar: target/DeskHub-Minecraft-1.0.0.jar +``` + +Zusätzlicher Test des Spigot/Paper-Teils gegen einen simulierten Paper-Server (braucht JDK 21): + +```bash +mvn install -DskipTests +cd integration-test +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. +- **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.0.0.jar b/minecraft-plugin/dist/DeskHub-Minecraft-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..789ff6454e2d5a747fe404d7f52a72956c36b006 GIT binary patch literal 44027 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 literal 0 HcmV?d00001 diff --git a/minecraft-plugin/integration-test/pom.xml b/minecraft-plugin/integration-test/pom.xml new file mode 100644 index 00000000..72011bf9 --- /dev/null +++ b/minecraft-plugin/integration-test/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + de.deskhub + deskhub-minecraft-integration-test + 1.0.0 + jar + + UTF-8 + 21 + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + + io.papermc.paper + paper-api + 1.21.11-R0.1-SNAPSHOT + test + + + org.mockbukkit.mockbukkit + mockbukkit-v1.21 + 4.116.3 + test + + + de.deskhub + deskhub-minecraft + 1.0.0 + test + + ** + + + + org.junit.jupiter + junit-jupiter + 6.0.3 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + + 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 new file mode 100644 index 00000000..bbf54d1e --- /dev/null +++ b/minecraft-plugin/integration-test/src/test/java/de/deskhub/minecraft/PaperPluginTest.java @@ -0,0 +1,162 @@ +package de.deskhub.minecraft; + +import com.sun.net.httpserver.HttpServer; +import de.deskhub.minecraft.bukkit.BukkitPlugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +/** Spigot/Paper-Teil des fertigen Plugins gegen einen simulierten Server und eine simulierte DeskHub-API. */ +class PaperPluginTest { + private static final String KEY = "dh_" + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4"; + private ServerMock server; + private BukkitPlugin plugin; + private HttpServer http; + private final List bodies = new CopyOnWriteArrayList<>(); + private final List keys = new CopyOnWriteArrayList<>(); + + @BeforeEach + void setUp() throws Exception { + http = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + http.createContext("/api/public/v1/tickets.php", ex -> { + keys.add(ex.getRequestHeaders().getFirst("X-API-Key")); + ByteArrayOutputStream b = new ByteArrayOutputStream(); + try (InputStream in = ex.getRequestBody()) { in.transferTo(b); } + bodies.add(b.toString(StandardCharsets.UTF_8)); + byte[] out = ("{\"number\":\"T-" + bodies.size() + "\",\"status\":\"open\"}").getBytes(StandardCharsets.UTF_8); + ex.sendResponseHeaders(201, out.length); + ex.getResponseBody().write(out); + ex.close(); + }); + http.start(); + server = MockBukkit.mock(); + plugin = MockBukkit.load(BukkitPlugin.class); + server.getPluginManager().disablePlugin(plugin); + } + + @AfterEach + void tearDown() { + http.stop(0); + MockBukkit.unmock(); + } + + private void configure(String extra) throws Exception { + String cfg = "deskhub:\n url: \"http://127.0.0.1:" + http.getAddress().getPort() + "\"\n api-key: \"" + KEY + "\"\n requester-email: \"mc@example.de\"\n" + + "ticket:\n department-id: 4\n priority-id: 3\n" + extra; + Files.writeString(new File(plugin.getDataFolder(), "config.yml").toPath(), cfg); + } + + private File heartbeat() { + return new File(plugin.getDataFolder(), "lebenszeichen.properties"); + } + + private void writeHeartbeat(boolean running, boolean clean, long lastBeat) throws Exception { + Properties p = new Properties(); + p.setProperty("running", String.valueOf(running)); + p.setProperty("clean", String.valueOf(clean)); + p.setProperty("lastBeat", String.valueOf(lastBeat)); + try (var out = Files.newOutputStream(heartbeat().toPath())) { p.store(out, null); } + } + + private void enable() { + server.getPluginManager().enablePlugin(plugin); + server.getScheduler().waitAsyncTasksFinished(); + } + + @Test + void startsWithoutConfigAndCreatesNoTicket() { + // Standard-config: URL/Schlüssel fehlen -> Plugin läuft, meldet das Problem, legt nichts an + enable(); + assertTrue(plugin.isEnabled()); + assertTrue(bodies.isEmpty()); + assertTrue(heartbeat().isFile(), "Lebenszeichen wird geschrieben"); + } + + @Test + void crashOfPreviousRunCreatesTicketOnStart() throws Exception { + configure(""); + writeHeartbeat(true, false, System.currentTimeMillis() - 5 * 60_000L); + enable(); + assertEquals(1, bodies.size(), "genau ein Ticket"); + 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); + } + + @Test + void cleanShutdownCreatesNoTicketOnNextStart() throws Exception { + configure(""); + enable(); + server.getPluginManager().disablePlugin(plugin); // ordnungsgemäß beenden + assertFalse(Files.readString(heartbeat().toPath()).contains("running=true")); + configure(""); + enable(); + assertTrue(bodies.isEmpty(), "nach sauberem Beenden kein Ticket"); + } + + @Test + void firstEverStartCreatesNoTicket() throws Exception { + configure(""); + Files.deleteIfExists(heartbeat().toPath()); + enable(); + assertTrue(bodies.isEmpty()); + } + + @Test + void crashTicketCanBeSwitchedOff() throws Exception { + configure("backend:\n ticket-on-crash: false\n"); + writeHeartbeat(true, false, System.currentTimeMillis() - 60_000L); + enable(); + assertTrue(bodies.isEmpty()); + } + + @Test + void shutdownTicketOnlyWhenEnabled() throws Exception { + configure("backend:\n ticket-on-shutdown: true\n"); + enable(); + server.getPluginManager().disablePlugin(plugin); + assertEquals(1, bodies.size()); + assertTrue(bodies.get(0).contains("heruntergefahren"), bodies.get(0)); + } + + @Test + void testCommandCreatesTicketAndNeedsPermission() throws Exception { + configure(""); + enable(); + var op = server.addPlayer(); + op.setOp(false); + server.dispatchCommand(op, "deskhubmc test"); + server.getScheduler().waitAsyncTasksFinished(); + assertTrue(bodies.isEmpty(), "ohne Berechtigung kein Ticket"); + server.dispatchCommand(server.getConsoleSender(), "deskhubmc test"); + server.getScheduler().waitAsyncTasksFinished(); + server.getScheduler().performOneTick(); + assertEquals(1, bodies.size()); + assertTrue(bodies.get(0).contains("Testticket"), bodies.get(0)); + } + + @Test + void crashWithBrokenConfigDoesNotCrashPlugin() throws Exception { + Files.writeString(new File(plugin.getDataFolder(), "config.yml").toPath(), "deskhub:\n url: \"\"\n"); + writeHeartbeat(true, false, 1); + assertDoesNotThrow(this::enable); + assertTrue(plugin.isEnabled()); + assertTrue(bodies.isEmpty()); + } +} diff --git a/minecraft-plugin/integration-test/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/minecraft-plugin/integration-test/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst new file mode 100644 index 00000000..99379325 --- /dev/null +++ b/minecraft-plugin/integration-test/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst @@ -0,0 +1 @@ +de\deskhub\minecraft\PaperPluginTest.class diff --git a/minecraft-plugin/integration-test/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/minecraft-plugin/integration-test/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 00000000..923be78d --- /dev/null +++ b/minecraft-plugin/integration-test/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst @@ -0,0 +1 @@ +C:\Users\Viper\Desktop\deskhub\minecraft-plugin\integration-test\src\test\java\de\deskhub\minecraft\PaperPluginTest.java diff --git a/minecraft-plugin/integration-test/target/surefire-reports/2026-10-21T13-17-31_719-jvmRun1.dump b/minecraft-plugin/integration-test/target/surefire-reports/2026-10-21T13-17-31_719-jvmRun1.dump new file mode 100644 index 00000000..2fcee214 --- /dev/null +++ b/minecraft-plugin/integration-test/target/surefire-reports/2026-10-21T13-17-31_719-jvmRun1.dump @@ -0,0 +1,17 @@ +# Created at 2026-10-21T13:17:32.125 +java.lang.NoClassDefFoundError: org/junit/platform/engine/OutputDirectoryCreator + at org.apache.maven.surefire.junitplatform.TestPlanScannerFilter.accept(TestPlanScannerFilter.java:47) + at org.apache.maven.surefire.api.util.DefaultScanResult.applyFilter(DefaultScanResult.java:87) + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.scanClasspath(JUnitPlatformProvider.java:142) + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) +Caused by: java.lang.ClassNotFoundException: org.junit.platform.engine.OutputDirectoryCreator + at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) + at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) + at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526) + ... 8 more + + diff --git a/minecraft-plugin/integration-test/target/surefire-reports/2026-10-21T13-17-31_719-jvmRun1.dumpstream b/minecraft-plugin/integration-test/target/surefire-reports/2026-10-21T13-17-31_719-jvmRun1.dumpstream new file mode 100644 index 00000000..52ecf703 --- /dev/null +++ b/minecraft-plugin/integration-test/target/surefire-reports/2026-10-21T13-17-31_719-jvmRun1.dumpstream @@ -0,0 +1,3 @@ +# Created at 2026-10-21T13:17:32.130 +org/junit/platform/engine/OutputDirectoryCreator + diff --git a/minecraft-plugin/integration-test/target/surefire-reports/TEST-de.deskhub.minecraft.PaperPluginTest.xml b/minecraft-plugin/integration-test/target/surefire-reports/TEST-de.deskhub.minecraft.PaperPluginTest.xml new file mode 100644 index 00000000..a754425a --- /dev/null +++ b/minecraft-plugin/integration-test/target/surefire-reports/TEST-de.deskhub.minecraft.PaperPluginTest.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/minecraft-plugin/integration-test/target/surefire-reports/de.deskhub.minecraft.PaperPluginTest.txt b/minecraft-plugin/integration-test/target/surefire-reports/de.deskhub.minecraft.PaperPluginTest.txt new file mode 100644 index 00000000..e926b1bb --- /dev/null +++ b/minecraft-plugin/integration-test/target/surefire-reports/de.deskhub.minecraft.PaperPluginTest.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: de.deskhub.minecraft.PaperPluginTest +------------------------------------------------------------------------------- +Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.317 s -- in de.deskhub.minecraft.PaperPluginTest diff --git a/minecraft-plugin/integration-test/target/test-classes/de/deskhub/minecraft/PaperPluginTest.class b/minecraft-plugin/integration-test/target/test-classes/de/deskhub/minecraft/PaperPluginTest.class new file mode 100644 index 0000000000000000000000000000000000000000..2bdcb084f057d075605cfdcaec026cc6c50d6d67 GIT binary patch literal 10062 zcmcIq33L?Km3>cJHBFgC0)sH%rp=-SsoBAj!AL?FfrJQ>!7Mf|b$_V~byqc2)e^Fe z9S1um@opS1iPt!Zv&VK!BOqR~IkDr}_sN-=%s!Ku$*h@7W-@W^ud41Yt@fF7=5UZ| z`Ty8A4U6gh7R4i^9@vLPD!-=(-nUQYeprt#a#j>=KPTh9Gq)_P$nn|icm#H+1 zW-~2Mh*(0{gUM`PEUgF_sk6vV(rlLW=BiXF ziIokC5nG{oOpDxAhHzpX_|ap=2Zhs;NLa$QLv%UQjJ4}FH?G0IwPC8Fg;L*Yrps$Q z>aNB;j-?v|%^kG|!gK{KR%nSzOX*6cWpOhdv$IA_l21BL#unC)uwuJ#+2dY9-nb<+ zM=-VeBqtRG#5>=Yn5*G3m7;VNG?e_9uKEDmLbL*Qh-n!;mdW;|^muG&ZOqZ7CG5sb zGE)hcuTrUoY9Ve?GZHC?+ga23K^)RNt=zj@Vpg-XTfIuFse$P-&me84;fRxP=+Q|` zJ)Kf{n$t4&sk@A0Dy@+*Ty8s>h0t3^>lNCd(ni{Zs57mBSQ=5$mmM6`o#15`p0>JA zVQsmDsivm3bLtJA%jGuLQ(IKJnwnrzFDjT;ez3g?T?0;&8s8Vvlgi5qsn0f*uBGje zGG%HBriP$J+ftfs!%I?Ze|yGFr|tkQ~PHs5{Su!Zfkuh z;Yq}_q-JV+c_^@7r5@^q$!*6(8kW_#=&+6ssB};UTSeB8AtM9-kV=Q?2vZm;`^dwz z6v|GGOfYH{-Kf${bc{)nrZO$03Xry=l9)7=`Y6t{Agv7wgnC?a4(d)4R|s*#0XVLz z>6i#$KF1)YDTGv4T|sm+wo^}uFg4SFN=edT7I5C7IoeJ$m5?5qlNZ`w+Z-Y!ML2F6 z{rW)TNIDg!1f?Z}8bBsL3iE3xkq3C1po~f<2*fKKbD!oUCHCe`gRT7#2}Qt3HY)@R zEc9%af6<*0Mt8>1O(RT0G_24`l}6|mrmC^)q++nj%wbuDJUC~%F%QUjYA&0Am~BbT z>JcZh!ibCJBi;)mzqrU6$u*XnOD9CPLEMz+6NY_@(BnyAG-fOl)yzTpQs{Q3dG0p1 zUhXTr3DYSm4bh$OyOlKz6rM1hrn?lnTcwZEJ!497UK3?VOJzlOKhyl0!;^Sf()^f8 zAE$d^0~^ugaT{UU5`+rB&IK&sKM9ib%0E1RKM*yZ5}FaB2bn^E8K)JMPD=Z*6!j4) z&FG2ESXGFb;(gqCKyZc<;pzfa(; zAR*+{Df*O3PtntEJuoeVe|<4f$E=;i4hYz1={bd-SLxF-%RLW{W8&Jyw1q;S$p@mF za=l5*9F`~(rWfdQD!oXbM~Z2QL{B!8k^P7Ob}tM%q_Cq|f=(xWQKc_Qj8lbCL@SBE zUs36+^fkOd!uBB464{i@*XC&v)y^-BdG(~Q(TCNS=@o^(uF|Vg)6G-E>a=u_&cmAS zwAdp?yjQaa?VacvZAd7iAg9uK7c4z}B=lTR=^|Z1VCZ(ct0Zg!*gG7i*XVVH-cadH z`X*CEigP@R1{6OtNNUS%Z(3kP%@2m8-TvVY$;*sBH%AfUdb%NJ6_Uz76vy2P-< zlZ8)+ev^r$>c6eh@6hi;PY*n^G7#qneVz`sOjjKOlfKMe^BWk>7O7L z0#quBcGtCjV4?t|{4>+~MDm!nwr^d0ePV;y*uP2J+_xoubz+TJ+rLg*-?t&YF|kQ( z?%$$a-M1#bHnC2u@82M0`d2B_zsbTkk?_PPwFg4r|Cac z`WgKf5DlJjG+l<$6+xn7szUAI5dAld8qHX`iQ+OM$*d32FMtihniVks{7yt*62e8F zW`*dNAlH($`?XXmp=E{D6p3q%k&Tfq-N-sJh=yQTW-dVl1N#zQ9LUzwQ=?FT+$s|^ zqaQILE&~O>c5HPJ{;&^MsI17und$j#Cp;!XJOeX~p~aICTg&zdOQa*MLKq2QLb1WBoNXSe1N`)gTFOwFmpBf1#?3G1EINg~L zV`78t-03zQk2n}*rFbh;UdgLa@X&u|G$)Q0E}h2A1iT&7fnqe2k1YM~K%UsO1>%1w)88pg{Uokuj3BQcaO)G+ap~yh-KFyak{v zPKr1(gQ@2OPo$FQzJcpb-I9fGWf0%;p)@hN0OZ`Mau@Ff zkRzeSRV=?VtUatSdXyzDzN0m29q|u-g3VQ!yQx$ft4HNtz8=X@v_Y$xKxeuD2FR1> zFbb_;bC?hE4GJGp`7mRs@)~`x39LboIhJPF{le-snW}y5#20ouCrBkI17w&J>)I(T(mIYqM&n;RnPLU$|o6qEO+}2iPT40 zU4rx+qGwP0AtcjQ(q<;G zb9N_A5%*@(C~Unl*(F0WuB8rWmM-u8hhK-yogqBtT&=l>c%U~CB(5me~^+RY* zvA(4rRnK2ujx#k&gN5V$1NdoXk=~bkW*Q&=Oh_S;Sb;#uFB&(ELV(K<|M9^tE#up3 z1-xT9wmfhEb$AQ2e~Xc5bB{TJoL-;Ew0H_>CH{l#O1BN@?%!vLA>GW{u#->1BhYPt zuRQdSNx8}$G<*C$U>vJ%V=t8US1;+8kk*bm`dcqP=jlQG>(G&v$MeE zgmwXDS>?(njoWIO=$$-qG87!m1IX#oG%*mL{nq~ z9_1$m_rdxw&!LLh<#aB@qfC)oqlODu(WYo`14ma!k!()Z#w|{cjOeM^6?`$wm-rhB zzozo*{02G&x1hMVqZ>nrnOLVA7_gPruf-kH0t(Me2k+hu)_Z@GXIk%Y-Co{lnuFQQ zBpqe|amQ@z9m$B`@(=#fV>$iaU}^Lzw_$+l!QkHvlYBGG-{Nm8{2f^;zl$+qf=Rq; ziDbf2>CU`Sp7{SagI}eO1k9FilLC*%tAzQj!!U&9p55cHkiUb*J!d(gd1po2r!o9|K0rxjPI^lPv{8oo`x65aI2k!B? zEpR^v&+)+!Pd<#RE-J-oHPIDy=ZWhsk#guf%{WKl;^mx|@w$ZX#Lrn+Otg^7XbDIz z<&WS_^|teAz6*E6cY}1POZtAWk)-#4R@I@(`K9M+LFp(hI!BSZS1D4tyzU~cB+60U zL3y8}#=22jdyY2ORW^^();Fp79JS!h<{Y)-S4)oe6h&Ez&y$4DY?aVCN)1#?jrfEV zqigVUH?0kbw#O5VKZ^Bb(E1+!7_OxLAIERvdvRUj3cL+Al>)0$oYEY9Xq2uma`cqn zNFmw;pKK0rP<;-0y7z%bi99><={n#m4cF+Yc|(ql)?cKXVf;&!IOM6;UwQmIrE+Ah zE`5#cQOX`1&$)@_Lc0dq>T-^F6!T`MM!sK02i7d%2XIwV)EBbgAlLVIv$O$ZnHyw% zD_FlIg?7CR)8Kl0K*6~_n-Y0G^;#d3AZ^hp!l=kL2jgL0sRr zxOn}*vQc_SKCjEsSt#l0_*hd#b$N9K=jfBw6>rhX>WUma^CB&6QsnJC|5j}ZRV&q@ z9DTN0dE;d$vz6LuKh9?l;oFOb-=(h9i{M5=BFwzc{d}A!aG2_QVh4NnX2d_+I5Zqx5c(&E-f~!Z))r>IJLo zX%QVjf*uU8TjjAUWYFVSPy7T}dnx}zWTiBpU03-VIr^=-GUWU3&7&WR9Q}!urpy!K z&(Y`q)hPXqyeqvxKQ6(E?CH+RpWuFucRx!1QeXM+Ir{07G^f7u=kA44_9B&))=DTv;c9kl8(^^q+A;!<|cfZOH!OHO1P?B?uqPM zasyqPhpDKuZ)E75e zkU{3=D2HX5%E&3i%lH_vLBj#oWRYq^NVVYr|LFVhf2zR$7@|dHz>2ymo>dGNo|6xk z=|s2{n{T6e_*k@%Zl~pRM?i!X?<2y~1rg35$E66nAws1TVS>wwAv?70J&R zOmo_AHQIw!%`d1`ygpx2fk$D9L>GA@(I{UHL!he|501rP_dNI%{*vfvT0zgC#dwys z&~r$*=K~&`HsR)!$l=GYneBj9S*6Z^@tpb`Z>{3C9JgQKJtcJT5_cau&mZc-^Zk1p zR_FM@4GnM6RSh{l`XViED#J~cccY@#mQ|O_idgmrNQGCw!Z+qP?$MDCE)XF0A`cL` zZ;tP+pG$8yync6%4Uo4$W(dD;k=1k=t-_zDya2tf!C$E4tLaL&wl7D1t)Rt-+C5lx z6n}nn0*%(KREKXc^@!xvt`#=G_{*q{Kf|AOo02oW1ED8>j$cH6Ev6JO_4D9%f)=0x ze*s8csHf`)m2{@U+Z66raN6h9Q~ZTO@ad%W^ot<#Wtg*szrtU|? + + 4.0.0 + + de.deskhub + deskhub-minecraft + 1.0.0 + jar + DeskHub-Minecraft + Legt in DeskHub ein Ticket an, wenn ein Minecraft-Server offline geht. Eine Jar für Spigot, Paper und BungeeCord. + + + UTF-8 + + 8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + sonatype-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + + + + + + org.spigotmc + spigot-api + 1.16.5-R0.1-SNAPSHOT + provided + + + net.md-5 + bungeecord-api + 1.16-R0.4 + provided + + + + net.md-5 + brigadier + + + + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + + + DeskHub-Minecraft-${project.version} + + + src/main/resources + true + + plugin.yml + bungee.yml + + + + src/main/resources + false + + plugin.yml + bungee.yml + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitConfig.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitConfig.java new file mode 100644 index 00000000..61bc806d --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitConfig.java @@ -0,0 +1,35 @@ +package de.deskhub.minecraft.bukkit; + +import de.deskhub.minecraft.core.ConfigSource; +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.List; + +/** Die config.yml von Spigot/Paper für den gemeinsamen Teil des Plugins. */ +final class BukkitConfig implements ConfigSource { + private final FileConfiguration cfg; + + BukkitConfig(FileConfiguration cfg) { + this.cfg = cfg; + } + + @Override + public String getString(String path, String def) { + return cfg.getString(path, def); + } + + @Override + public int getInt(String path, int def) { + return cfg.getInt(path, def); + } + + @Override + public boolean getBoolean(String path, boolean def) { + return cfg.getBoolean(path, def); + } + + @Override + public List getStringList(String path) { + return cfg.getStringList(path); + } +} 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 new file mode 100644 index 00000000..d25bcfb4 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bukkit/BukkitPlugin.java @@ -0,0 +1,159 @@ +package de.deskhub.minecraft.bukkit; + +import de.deskhub.minecraft.core.HeartbeatFile; +import de.deskhub.minecraft.core.Settings; +import de.deskhub.minecraft.core.Template; +import de.deskhub.minecraft.core.TicketClient; +import de.deskhub.minecraft.core.TicketResult; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +/** + * Spigot/Paper: Der Server meldet sich selbst. Ein Server, der abstürzt, kann das nicht mehr selbst melden. Deshalb schreibt das Plugin + * regelmäßig ein Lebenszeichen und markiert es beim ordnungsgemäßen Beenden. Findet es beim nächsten Start ein Lebenszeichen ohne diese + * Markierung, legt es ein Ticket "Server war nicht erreichbar" an. Für Meldungen in Echtzeit läuft das Plugin zusätzlich auf dem Proxy + * (BungeeCord), der die Server von außen prüft. + */ +public class BukkitPlugin extends JavaPlugin implements CommandExecutor { + private Settings settings; + private TicketClient client; + private HeartbeatFile heartbeat; + private BukkitTask beatTask; + + @Override + public void onEnable() { + saveDefaultConfig(); + load(); + if (getCommand("deskhubmc") != null) getCommand("deskhubmc").setExecutor(this); + heartbeat = new HeartbeatFile(new File(getDataFolder(), "lebenszeichen.properties")); + HeartbeatFile.Previous previous = heartbeat.readPrevious(); + long now = System.currentTimeMillis(); + try { + heartbeat.start(now); + } catch (IOException e) { + getLogger().log(Level.WARNING, "Lebenszeichen-Datei nicht schreibbar: " + e.getMessage() + " (Abstürze werden nicht erkannt)"); + } + if (previous.crashed() && settings.ticketOnCrash) reportCrash(previous, now); + long period = settings.heartbeatSeconds * 20L; + beatTask = Bukkit.getScheduler().runTaskTimerAsynchronously(this, new Runnable() { + @Override + public void run() { + try { + heartbeat.beat(System.currentTimeMillis()); + } catch (IOException ignored) { + // schon beim Start gemeldet + } + } + }, period, period); + } + + @Override + public void onDisable() { + if (beatTask != null) beatTask.cancel(); + long now = System.currentTimeMillis(); + try { + if (heartbeat != null) heartbeat.markClean(now); + } catch (IOException e) { + getLogger().log(Level.WARNING, "Lebenszeichen-Datei nicht schreibbar: " + e.getMessage()); + } + if (settings != null && settings.ticketOnShutdown && settings.problems().isEmpty()) { + // 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)); + getLogger().info(r.ok ? "Ticket " + r.number + " angelegt (Herunterfahren)." : "Ticket nicht angelegt: " + r.error); + } + } + + private void load() { + reloadConfig(); + settings = new Settings(new BukkitConfig(getConfig())); + client = new TicketClient(settings, getDescription().getVersion()); + for (String w : settings.warnings()) getLogger().warning(w); + List problems = settings.problems(); + if (!problems.isEmpty()) { + getLogger().warning("Es werden keine Tickets angelegt, bis die config.yml stimmt:"); + for (String p : problems) getLogger().warning(" - " + p); + } + } + + private String serverName() { + if (!settings.serverName.isEmpty()) return settings.serverName; + return "Minecraft-Server (Port " + Bukkit.getPort() + ")"; + } + + private void reportCrash(final HeartbeatFile.Previous previous, final long now) { + if (!settings.problems().isEmpty()) { + getLogger().warning("Der Server war zuvor nicht ordnungsgemäß beendet worden, aber es kann kein Ticket angelegt werden (config.yml prüfen)."); + return; + } + getLogger().warning("Der Server wurde zuvor nicht ordnungsgemäß beendet. Es wird ein Ticket angelegt."); + Bukkit.getScheduler().runTaskAsynchronously(this, new Runnable() { + @Override + public void run() { + Map v = Template.vars(serverName(), "Plugin auf dem Server", now); + 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); + } + }); + } + + @Override + public boolean onCommand(final CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("deskhubmc.admin")) { + sender.sendMessage("§cDafür fehlt dir die Berechtigung (deskhubmc.admin)."); + return true; + } + String sub = args.length > 0 ? args[0].toLowerCase() : "help"; + if (sub.equals("reload")) { + load(); + sender.sendMessage(settings.problems().isEmpty() ? "§aEinstellungen neu geladen." : "§eEinstellungen neu geladen, aber unvollständig: " + settings.problems().get(0)); + } else if (sub.equals("status")) { + sender.sendMessage("§6DeskHub-Minecraft §7(Spigot/Paper)"); + sender.sendMessage("§7DeskHub: §f" + (settings.url.isEmpty() ? "nicht eingestellt" : settings.url)); + sender.sendMessage("§7Abteilung: §f" + idText(settings.departmentId) + " §7Priorität: §f" + idText(settings.priorityId)); + sender.sendMessage("§7Konfiguration: " + (settings.problems().isEmpty() ? "§aOK" : "§c" + settings.problems().get(0))); + sender.sendMessage("§7Absturz-Ticket: §f" + (settings.ticketOnCrash ? "an" : "aus") + " §7Herunterfahren-Ticket: §f" + (settings.ticketOnShutdown ? "an" : "aus")); + } else if (sub.equals("test")) { + if (!settings.problems().isEmpty()) { + sender.sendMessage("§cKein Test möglich: " + settings.problems().get(0)); + return true; + } + sender.sendMessage("§7Testticket wird angelegt …"); + Bukkit.getScheduler().runTaskAsynchronously(this, new Runnable() { + @Override + public void run() { + Map v = Template.vars(serverName(), "Befehl /deskhubmc test", System.currentTimeMillis()); + final TicketResult r = client.create(Template.apply(settings.subjectTest, v), Template.apply(settings.messageTest, v)); + Bukkit.getScheduler().runTask(BukkitPlugin.this, new Runnable() { + @Override + public void run() { + sender.sendMessage(r.ok ? "§aTicket " + r.number + " wurde angelegt." : "§cFehlgeschlagen: " + r.error); + } + }); + } + }); + } else { + sender.sendMessage("§6/deskhubmc test §7- Testticket anlegen"); + sender.sendMessage("§6/deskhubmc status §7- Einstellungen anzeigen"); + sender.sendMessage("§6/deskhubmc reload §7- config.yml neu laden"); + } + return true; + } + + private static String idText(int id) { + return id > 0 ? String.valueOf(id) : "Standard"; + } +} diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeeConfig.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeeConfig.java new file mode 100644 index 00000000..c7754e76 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeeConfig.java @@ -0,0 +1,35 @@ +package de.deskhub.minecraft.bungee; + +import de.deskhub.minecraft.core.ConfigSource; +import net.md_5.bungee.config.Configuration; + +import java.util.List; + +/** Die config.yml von BungeeCord für den gemeinsamen Teil des Plugins. */ +final class BungeeConfig implements ConfigSource { + private final Configuration cfg; + + BungeeConfig(Configuration cfg) { + this.cfg = cfg; + } + + @Override + public String getString(String path, String def) { + return cfg.getString(path, def); + } + + @Override + public int getInt(String path, int def) { + return cfg.getInt(path, def); + } + + @Override + public boolean getBoolean(String path, boolean def) { + return cfg.getBoolean(path, def); + } + + @Override + public List getStringList(String path) { + return cfg.getStringList(path); + } +} 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 new file mode 100644 index 00000000..01352058 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/bungee/BungeePlugin.java @@ -0,0 +1,186 @@ +package de.deskhub.minecraft.bungee; + +import de.deskhub.minecraft.core.OutageTracker; +import de.deskhub.minecraft.core.Settings; +import de.deskhub.minecraft.core.Template; +import de.deskhub.minecraft.core.TicketClient; +import de.deskhub.minecraft.core.TicketResult; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.Callback; +import net.md_5.bungee.api.ServerPing; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.scheduler.ScheduledTask; +import net.md_5.bungee.config.Configuration; +import net.md_5.bungee.config.ConfigurationProvider; +import net.md_5.bungee.config.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +/** + * BungeeCord: Der Proxy prüft die Server von außen (Ping) und meldet einen Ausfall in Echtzeit, auch wenn der Server abgestürzt ist + * und sich selbst nicht mehr melden könnte. Ein Ticket entsteht erst nach mehreren Fehlschlägen in Folge, höchstens einmal je Ausfall + * und je Wartezeit. + */ +public final class BungeePlugin extends Plugin { + private Settings settings; + private TicketClient client; + private OutageTracker tracker; + private ScheduledTask watchdog; + + @Override + public void onEnable() { + load(); + getProxy().getPluginManager().registerCommand(this, new McCommand()); + } + + @Override + public void onDisable() { + if (watchdog != null) watchdog.cancel(); + } + + private void load() { + if (watchdog != null) watchdog.cancel(); + try { + File file = new File(getDataFolder(), "config.yml"); + if (!file.exists()) { + if (!getDataFolder().isDirectory() && !getDataFolder().mkdirs()) throw new IOException("Ordner nicht anlegbar: " + getDataFolder()); + InputStream in = getResourceAsStream("config.yml"); + try { + Files.copy(in, file.toPath()); + } finally { + in.close(); + } + } + Configuration cfg = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file); + settings = new Settings(new BungeeConfig(cfg)); + } catch (IOException e) { + getLogger().log(Level.SEVERE, "config.yml konnte nicht gelesen werden: " + e.getMessage()); + settings = new Settings(new BungeeConfig(new Configuration())); + } + client = new TicketClient(settings, getDescription().getVersion()); + tracker = new OutageTracker(settings.monitorFailures, settings.cooldownMinutes, settings.ignoreNeverOnline); + for (String w : settings.warnings()) getLogger().warning(w); + List problems = settings.problems(); + if (!problems.isEmpty()) { + getLogger().warning("Es werden keine Tickets angelegt, bis die config.yml stimmt:"); + for (String p : problems) getLogger().warning(" - " + p); + } + watchdog = getProxy().getScheduler().schedule(this, new Runnable() { + @Override + public void run() { + checkServers(); + } + }, 10, settings.monitorIntervalSeconds, TimeUnit.SECONDS); + } + + private List monitored() { + List list = new ArrayList(); + Map all = getProxy().getServers(); + if (settings.monitorServers.isEmpty()) { + list.addAll(all.values()); + } else { + for (String name : settings.monitorServers) { + ServerInfo info = all.get(name); + if (info != null) list.add(info); + else getLogger().warning("monitor.servers: Server \"" + name + "\" ist in der Proxy-Konfiguration nicht vorhanden."); + } + } + return list; + } + + private void checkServers() { + for (final ServerInfo info : monitored()) { + info.ping(new Callback() { + @Override + public void done(ServerPing result, Throwable error) { + handle(info.getName(), error == null && result != null); + } + }); + } + } + + private void handle(final String server, boolean online) { + final long now = System.currentTimeMillis(); + OutageTracker.Decision d = tracker.report(server, online, now); + if (d == OutageTracker.Decision.RECOVERED) { + getLogger().info("Server \"" + server + "\" ist wieder erreichbar."); + } 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."); + if (!settings.problems().isEmpty()) { + getLogger().warning("Kein Ticket möglich: " + settings.problems().get(0)); + return; + } + getProxy().getScheduler().runAsync(this, new Runnable() { + @Override + 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."); + else getLogger().warning("Ticket nicht angelegt: " + r.error); + } + }); + } + } + + private final class McCommand extends Command { + McCommand() { + super("deskhubmc", "deskhubmc.admin", "dhmc"); + } + + @Override + public void execute(final CommandSender sender, String[] args) { + String sub = args.length > 0 ? args[0].toLowerCase() : "help"; + if (sub.equals("reload")) { + load(); + send(sender, settings.problems().isEmpty() ? "§aEinstellungen neu geladen." : "§eEinstellungen neu geladen, aber unvollständig: " + settings.problems().get(0)); + } else if (sub.equals("status")) { + send(sender, "§6DeskHub-Minecraft §7(BungeeCord)"); + send(sender, "§7DeskHub: §f" + (settings.url.isEmpty() ? "nicht eingestellt" : settings.url)); + send(sender, "§7Abteilung: §f" + idText(settings.departmentId) + " §7Priorität: §f" + idText(settings.priorityId)); + send(sender, "§7Konfiguration: " + (settings.problems().isEmpty() ? "§aOK" : "§c" + settings.problems().get(0))); + send(sender, "§7Prüfung alle §f" + settings.monitorIntervalSeconds + " s§7, Ticket nach §f" + settings.monitorFailures + " §7Fehlschlägen, Wartezeit §f" + settings.cooldownMinutes + " Min."); + Map snap = tracker.snapshot(); + if (snap.isEmpty()) send(sender, "§7Noch keine Prüfung durchgeführt."); + for (Map.Entry e : snap.entrySet()) send(sender, " §7- §f" + e.getKey() + ": " + (e.getValue() ? "§aonline" : "§coffline")); + } else if (sub.equals("test")) { + if (!settings.problems().isEmpty()) { + send(sender, "§cKein Test möglich: " + settings.problems().get(0)); + return; + } + send(sender, "§7Testticket wird angelegt …"); + getProxy().getScheduler().runAsync(BungeePlugin.this, new Runnable() { + @Override + public void run() { + Map v = Template.vars("BungeeCord-Proxy", "Befehl /deskhubmc test", System.currentTimeMillis()); + TicketResult r = client.create(Template.apply(settings.subjectTest, v), Template.apply(settings.messageTest, v)); + send(sender, r.ok ? "§aTicket " + r.number + " wurde angelegt." : "§cFehlgeschlagen: " + r.error); + } + }); + } else { + send(sender, "§6/deskhubmc test §7- Testticket anlegen"); + send(sender, "§6/deskhubmc status §7- Einstellungen und Serverzustand"); + send(sender, "§6/deskhubmc reload §7- config.yml neu laden"); + } + } + } + + private static void send(CommandSender sender, String text) { + sender.sendMessage(TextComponent.fromLegacyText(text)); + } + + private static String idText(int id) { + return id > 0 ? String.valueOf(id) : "Standard"; + } +} diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/ConfigSource.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/ConfigSource.java new file mode 100644 index 00000000..c9eeb738 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/ConfigSource.java @@ -0,0 +1,14 @@ +package de.deskhub.minecraft.core; + +import java.util.List; + +/** Zugriff auf die config.yml, unabhängig davon, ob Bukkit (Spigot/Paper) oder BungeeCord sie geladen hat. Pfade sind durch Punkte getrennt. */ +public interface ConfigSource { + String getString(String path, String def); + + int getInt(String path, int def); + + boolean getBoolean(String path, boolean def); + + List getStringList(String path); +} diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/HeartbeatFile.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/HeartbeatFile.java new file mode 100644 index 00000000..6a96af78 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/HeartbeatFile.java @@ -0,0 +1,102 @@ +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.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Properties; + +/** + * Lebenszeichen-Datei des Servers: Solange der Server läuft, wird sie regelmäßig aktualisiert; beim ordnungsgemäßen Beenden wird sie als + * "sauber beendet" markiert. Findet das Plugin beim nächsten Start eine Datei ohne diese Markierung, ist der Server abgestürzt (oder sein + * Prozess wurde beendet, oder der Strom fiel aus) und das Plugin legt ein Ticket an. + */ +public final class HeartbeatFile { + /** Zustand der Datei beim Start. */ + public static final class Previous { + public final boolean existed; + public final boolean clean; + public final long lastBeat; + + Previous(boolean existed, boolean clean, long lastBeat) { + this.existed = existed; + this.clean = clean; + this.lastBeat = lastBeat; + } + + /** Ob der letzte Lauf abgestürzt ist. */ + public boolean crashed() { + return existed && !clean; + } + } + + private final File file; + private long startedAt; + + public HeartbeatFile(File file) { + this.file = file; + } + + /** Liest den Zustand des letzten Laufs. Eine fehlende oder unlesbare Datei gilt als "kein früherer Lauf". */ + public Previous readPrevious() { + if (!file.isFile()) return new Previous(false, true, 0); + Properties p = new Properties(); + try { + InputStream in = new FileInputStream(file); + try { + p.load(in); + } finally { + in.close(); + } + } catch (IOException e) { + return new Previous(false, true, 0); + } + boolean clean = "true".equals(p.getProperty("clean")) || !"true".equals(p.getProperty("running")); + long last = 0; + try { + last = Long.parseLong(p.getProperty("lastBeat", "0")); + } catch (NumberFormatException ignored) { + // bleibt 0 + } + return new Previous(true, clean, last); + } + + /** Beim Start: Lauf beginnt. */ + public void start(long nowMillis) throws IOException { + startedAt = nowMillis; + write(true, false, nowMillis); + } + + /** Regelmäßig: Server läuft noch. */ + public void beat(long nowMillis) throws IOException { + write(true, false, nowMillis); + } + + /** Beim ordnungsgemäßen Beenden. */ + public void markClean(long nowMillis) throws IOException { + write(false, true, nowMillis); + } + + private void write(boolean running, boolean clean, long now) throws IOException { + File dir = file.getParentFile(); + if (dir != null && !dir.isDirectory() && !dir.mkdirs()) throw new IOException("Ordner nicht anlegbar: " + dir); + Properties p = new Properties(); + p.setProperty("running", Boolean.toString(running)); + p.setProperty("clean", Boolean.toString(clean)); + p.setProperty("lastBeat", Long.toString(now)); + p.setProperty("startedAt", Long.toString(startedAt)); + File tmp = new File(file.getPath() + ".tmp"); + OutputStream out = new FileOutputStream(tmp); + try { + p.store(out, "DeskHub-Minecraft Lebenszeichen, nicht bearbeiten"); + } finally { + out.close(); + } + // Erst vollständig schreiben, dann ersetzen: ein Absturz mitten im Schreiben darf die Datei nicht zerstören + Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } +} diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Json.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Json.java new file mode 100644 index 00000000..ec6f73c1 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Json.java @@ -0,0 +1,74 @@ +package de.deskhub.minecraft.core; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Minimaler JSON-Baustein: Texte maskieren und zwei Werte aus der Antwort von DeskHub lesen (keine Fremdbibliothek nötig). */ +public final class Json { + private static final Pattern NUMBER = Pattern.compile("\"number\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\""); + private static final Pattern MESSAGE = Pattern.compile("\"message\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\""); + + private Json() { + } + + /** Maskiert einen Text für JSON (mit Anführungszeichen drumherum). */ + public static String quote(String s) { + StringBuilder b = new StringBuilder(s.length() + 2).append('"'); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + switch (ch) { + case '"': b.append("\\\""); break; + case '\\': b.append("\\\\"); break; + case '\n': b.append("\\n"); break; + case '\r': b.append("\\r"); break; + case '\t': b.append("\\t"); break; + case '\b': b.append("\\b"); break; + case '\f': b.append("\\f"); break; + default: + if (ch < 0x20) b.append(String.format("\\u%04x", (int) ch)); + else b.append(ch); + } + } + return b.append('"').toString(); + } + + /** Ticketnummer aus {"number":"T-123","status":"open"} oder null. */ + public static String ticketNumber(String body) { + return first(NUMBER, body); + } + + /** Fehlertext aus {"error":{"code":"..","message":".."}} oder null. */ + public static String errorMessage(String body) { + return first(MESSAGE, body); + } + + private static String first(Pattern p, String body) { + if (body == null) return null; + Matcher m = p.matcher(body); + return m.find() ? unescape(m.group(1)) : null; + } + + private static String unescape(String s) { + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + if (ch != '\\' || i + 1 >= s.length()) { b.append(ch); continue; } + char n = s.charAt(++i); + switch (n) { + case 'n': b.append('\n'); break; + case 'r': b.append('\r'); break; + case 't': b.append('\t'); break; + case 'b': b.append('\b'); break; + case 'f': b.append('\f'); break; + case 'u': + if (i + 5 <= s.length()) { + try { b.append((char) Integer.parseInt(s.substring(i + 1, i + 5), 16)); i += 4; break; } catch (RuntimeException e) { /* unten als Zeichen übernehmen */ } + } + b.append('u'); + break; + default: b.append(n); + } + } + return b.toString(); + } +} diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/OutageTracker.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/OutageTracker.java new file mode 100644 index 00000000..437b6f59 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/OutageTracker.java @@ -0,0 +1,75 @@ +package de.deskhub.minecraft.core; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Entscheidet anhand der Prüfergebnisse (online/offline) je Server, wann ein Ticket angelegt wird: erst nach mehreren Fehlschlägen in Folge + * (kein Ticket bei einem einzelnen Aussetzer), höchstens einmal pro Ausfall und höchstens einmal je Wartezeit (kein Ticket-Gewitter, wenn + * ein Server ständig an- und ausgeht). Server, die seit dem Start nie online waren, werden auf Wunsch ignoriert. + */ +public final class OutageTracker { + public enum Decision { NONE, OPEN_TICKET, RECOVERED } + + private static final class State { + int failures; + boolean seenOnline; + boolean outageOpen; + long lastTicketAt = -1; + long offlineSince; + boolean online; + } + + private final int failuresNeeded; + private final long cooldownMillis; + private final boolean ignoreNeverOnline; + private final Map states = new HashMap(); + + public OutageTracker(int failuresNeeded, int cooldownMinutes, boolean ignoreNeverOnline) { + this.failuresNeeded = Math.max(1, failuresNeeded); + this.cooldownMillis = Math.max(0, cooldownMinutes) * 60000L; + this.ignoreNeverOnline = ignoreNeverOnline; + } + + public synchronized Decision report(String server, boolean online, long nowMillis) { + State s = states.get(server); + if (s == null) { + s = new State(); + states.put(server, s); + } + s.online = online; + if (online) { + s.seenOnline = true; + boolean recovered = s.outageOpen; + s.failures = 0; + s.outageOpen = false; + s.offlineSince = 0; + return recovered ? Decision.RECOVERED : Decision.NONE; + } + if (ignoreNeverOnline && !s.seenOnline) return Decision.NONE; + if (s.failures == 0) s.offlineSince = nowMillis; + s.failures++; + if (s.failures >= failuresNeeded && !s.outageOpen) { + s.outageOpen = true; + if (s.lastTicketAt < 0 || nowMillis - s.lastTicketAt >= cooldownMillis) { + s.lastTicketAt = nowMillis; + return Decision.OPEN_TICKET; + } + } + return Decision.NONE; + } + + /** Zeitpunkt des ersten fehlgeschlagenen Prüfens des aktuellen Ausfalls (0 = Server ist online). */ + public synchronized long offlineSince(String server) { + State s = states.get(server); + return s == null ? 0 : s.offlineSince; + } + + /** Server => online? (für den Befehl "status") */ + public synchronized Map snapshot() { + Map m = new LinkedHashMap(); + for (Map.Entry e : states.entrySet()) m.put(e.getKey(), e.getValue().online); + return m; + } +} 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 new file mode 100644 index 00000000..c3030287 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Settings.java @@ -0,0 +1,107 @@ +package de.deskhub.minecraft.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** Alle Einstellungen aus der config.yml, geprüft und mit Vorgabewerten. Ändert sich nach dem Laden nicht mehr. */ +public final class Settings { + private static final Pattern API_KEY = Pattern.compile("^dh_[a-f0-9]{48}$"); + private static final Pattern EMAIL = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"); + + public final String url; + public final String apiKey; + public final String requesterName; + public final String requesterEmail; + public final int timeoutSeconds; + + public final int departmentId; + public final int priorityId; + public final int helpTopicId; + + public final String subjectOffline; + public final String messageOffline; + public final String subjectCrash; + public final String messageCrash; + public final String subjectShutdown; + public final String messageShutdown; + public final String subjectTest; + public final String messageTest; + + public final int monitorIntervalSeconds; + public final int monitorFailures; + public final int cooldownMinutes; + public final boolean ignoreNeverOnline; + public final List monitorServers; + + public final int heartbeatSeconds; + public final boolean ticketOnCrash; + public final boolean ticketOnShutdown; + public final String serverName; + + public Settings(ConfigSource c) { + String u = c.getString("deskhub.url", "").trim(); + while (u.endsWith("/")) u = u.substring(0, u.length() - 1); + this.url = u; + this.apiKey = c.getString("deskhub.api-key", "").trim(); + this.requesterName = clip(c.getString("deskhub.requester-name", "Minecraft-Überwachung").trim(), 120); + this.requesterEmail = c.getString("deskhub.requester-email", "").trim(); + this.timeoutSeconds = clamp(c.getInt("deskhub.timeout-seconds", 10), 2, 60); + + this.departmentId = Math.max(0, c.getInt("ticket.department-id", 0)); + this.priorityId = Math.max(0, c.getInt("ticket.priority-id", 0)); + this.helpTopicId = Math.max(0, c.getInt("ticket.help-topic-id", 0)); + + this.subjectOffline = c.getString("ticket.offline.subject", "[Minecraft] {server} ist offline"); + this.messageOffline = c.getString("ticket.offline.message", + "Der Minecraft-Server \"{server}\" ist seit {time} nicht mehr erreichbar.\n\nErkannt von: {source}\nBitte den Server prüfen und neu starten."); + this.subjectCrash = c.getString("ticket.crash.subject", "[Minecraft] {server} war nicht erreichbar (Absturz oder Ausfall)"); + this.messageCrash = c.getString("ticket.crash.message", + "Der Minecraft-Server \"{server}\" wurde nicht ordnungsgemäß beendet (Absturz, Prozess beendet oder Stromausfall).\n\n" + + "Letztes Lebenszeichen: {lastSeen}\nNeu gestartet: {time}\nAusfalldauer: ca. {downtime}\n\nBitte die Logdateien prüfen."); + this.subjectShutdown = c.getString("ticket.shutdown.subject", "[Minecraft] {server} wurde heruntergefahren"); + this.messageShutdown = c.getString("ticket.shutdown.message", "Der Minecraft-Server \"{server}\" wurde um {time} heruntergefahren."); + 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.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); + this.ignoreNeverOnline = c.getBoolean("monitor.ignore-never-online", true); + List servers = new ArrayList(); + for (String s : c.getStringList("monitor.servers")) if (s != null && !s.trim().isEmpty()) servers.add(s.trim()); + this.monitorServers = Collections.unmodifiableList(servers); + + this.heartbeatSeconds = clamp(c.getInt("backend.heartbeat-seconds", 10), 2, 600); + this.ticketOnCrash = c.getBoolean("backend.ticket-on-crash", true); + this.ticketOnShutdown = c.getBoolean("backend.ticket-on-shutdown", false); + this.serverName = clip(c.getString("backend.server-name", "").trim(), 60); + } + + /** Probleme, die das Anlegen von Tickets verhindern (leer = alles in Ordnung). */ + public List problems() { + List p = new ArrayList(); + if (url.isEmpty()) p.add("deskhub.url fehlt (z. B. https://support.example.de)"); + else if (!url.startsWith("https://") && !url.startsWith("http://")) p.add("deskhub.url muss mit https:// beginnen"); + if (apiKey.isEmpty()) p.add("deskhub.api-key fehlt (Adminportal, Verwalten, API-Schlüssel, Recht \"Tickets anlegen\")"); + else if (!API_KEY.matcher(apiKey).matches()) p.add("deskhub.api-key sieht nicht wie ein DeskHub-Schlüssel aus (dh_ und 48 Zeichen)"); + if (!EMAIL.matcher(requesterEmail).matches()) p.add("deskhub.requester-email fehlt oder ist ungültig"); + return p; + } + + /** Hinweise, die das Anlegen nicht verhindern. */ + public List warnings() { + List w = new ArrayList(); + if (url.startsWith("http://")) w.add("deskhub.url verwendet http: der API-Schlüssel würde unverschlüsselt übertragen. Bitte https:// verwenden."); + return w; + } + + private static int clamp(int v, int min, int max) { + return Math.max(min, Math.min(max, v)); + } + + private static String clip(String s, int max) { + return s.length() > max ? s.substring(0, max) : s; + } +} diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Template.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Template.java new file mode 100644 index 00000000..32bafcc1 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/Template.java @@ -0,0 +1,48 @@ +package de.deskhub.minecraft.core; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +/** Ersetzt Platzhalter wie {server} oder {time} in den Texten der config.yml. Unbekannte Platzhalter bleiben stehen. */ +public final class Template { + private Template() { + } + + public static String apply(String template, Map vars) { + String out = template == null ? "" : template; + for (Map.Entry e : vars.entrySet()) { + out = out.replace("{" + e.getKey() + "}", e.getValue() == null ? "" : e.getValue()); + } + return out; + } + + /** Standard-Platzhalter für ein Ereignis. */ + public static Map vars(String server, String source, long timeMillis) { + Map v = new LinkedHashMap(); + v.put("server", server); + v.put("source", source); + v.put("time", formatTime(timeMillis)); + return v; + } + + public static String formatTime(long millis) { + SimpleDateFormat f = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.GERMANY); + f.setTimeZone(TimeZone.getDefault()); + return f.format(new Date(millis)); + } + + /** "5 Minuten", "2 Std. 10 Min." usw. */ + public static String formatDuration(long millis) { + long s = Math.max(0, millis) / 1000; + if (s < 90) return s + " Sekunden"; + long m = s / 60; + if (m < 90) return m + " Minuten"; + long h = m / 60; + if (h < 48) return h + " Std. " + (m % 60) + " Min."; + return (h / 24) + " Tage " + (h % 24) + " Std."; + } +} 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 new file mode 100644 index 00000000..cf9b08b3 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketClient.java @@ -0,0 +1,93 @@ +package de.deskhub.minecraft.core; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +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. */ +public final class TicketClient { + private static final int MAX_RESPONSE = 65536; + private final Settings settings; + private final String userAgent; + + public TicketClient(Settings settings, String pluginVersion) { + this.settings = settings; + this.userAgent = "DeskHub-Minecraft/" + pluginVersion; + } + + /** Baut den JSON-Körper der Anfrage (öffentlich für Tests). */ + public String buildBody(String subject, String message) { + StringBuilder b = new StringBuilder("{"); + b.append("\"name\":").append(Json.quote(settings.requesterName)); + b.append(",\"email\":").append(Json.quote(settings.requesterEmail)); + b.append(",\"subject\":").append(Json.quote(clip(subject, 200))); + b.append(",\"message\":").append(Json.quote(clip(message, 20000))); + if (settings.departmentId > 0) b.append(",\"departmentId\":").append(settings.departmentId); + if (settings.priorityId > 0) b.append(",\"priorityId\":").append(settings.priorityId); + if (settings.helpTopicId > 0) b.append(",\"helpTopicId\":").append(settings.helpTopicId); + return b.append('}').toString(); + } + + public TicketResult create(String subject, String message) { + if (!settings.problems().isEmpty()) { + return TicketResult.failure(0, "Einstellungen unvollständig: " + settings.problems().get(0)); + } + HttpURLConnection c = null; + try { + URL url = new URL(settings.url + "/api/public/v1/tickets.php"); + c = (HttpURLConnection) url.openConnection(); + c.setRequestMethod("POST"); + c.setInstanceFollowRedirects(false); // ein Redirect würde den API-Schlüssel an einen anderen Ort schicken + c.setConnectTimeout(settings.timeoutSeconds * 1000); + c.setReadTimeout(settings.timeoutSeconds * 1000); + c.setDoOutput(true); + c.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + 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); + c.setFixedLengthStreamingMode(data.length); + OutputStream out = c.getOutputStream(); + try { + out.write(data); + } finally { + out.close(); + } + int code = c.getResponseCode(); + String body = read(code >= 400 ? c.getErrorStream() : c.getInputStream()); + if (code == 201 || code == 200) { + String number = Json.ticketNumber(body); + return number != null ? TicketResult.success(number, code) : TicketResult.failure(code, "Antwort ohne Ticketnummer"); + } + String why = Json.errorMessage(body); + if (code >= 300 && code < 400) why = "Weiterleitung nicht erlaubt, bitte die genaue https-Adresse von DeskHub eintragen"; + if (code == 401) why = "API-Schlüssel ungültig oder IP nicht freigegeben"; + return TicketResult.failure(code, why != null ? why : "unbekannter Fehler"); + } catch (IOException e) { + return TicketResult.failure(0, "keine Verbindung zu DeskHub: " + e.getClass().getSimpleName() + (e.getMessage() != null ? " (" + e.getMessage() + ")" : "")); + } finally { + if (c != null) c.disconnect(); + } + } + + private static String read(InputStream in) throws IOException { + if (in == null) return ""; + try { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[2048]; + int n; + while ((n = in.read(chunk)) > 0 && buf.size() < MAX_RESPONSE) buf.write(chunk, 0, n); + return new String(buf.toByteArray(), StandardCharsets.UTF_8); + } finally { + in.close(); + } + } + + private static String clip(String s, int max) { + return s.length() > max ? s.substring(0, max) : s; + } +} diff --git a/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketResult.java b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketResult.java new file mode 100644 index 00000000..cdde37f1 --- /dev/null +++ b/minecraft-plugin/src/main/java/de/deskhub/minecraft/core/TicketResult.java @@ -0,0 +1,29 @@ +package de.deskhub.minecraft.core; + +/** Ergebnis eines Versuchs, in DeskHub ein Ticket anzulegen. */ +public final class TicketResult { + public final boolean ok; + public final String number; + public final int httpCode; + public final String error; + + private TicketResult(boolean ok, String number, int httpCode, String error) { + this.ok = ok; + this.number = number; + this.httpCode = httpCode; + this.error = error; + } + + public static TicketResult success(String number, int httpCode) { + return new TicketResult(true, number, httpCode, null); + } + + public static TicketResult failure(int httpCode, String error) { + return new TicketResult(false, null, httpCode, error); + } + + @Override + public String toString() { + return ok ? "Ticket " + number : "Fehler (HTTP " + httpCode + "): " + error; + } +} diff --git a/minecraft-plugin/src/main/resources/bungee.yml b/minecraft-plugin/src/main/resources/bungee.yml new file mode 100644 index 00000000..f3a5791f --- /dev/null +++ b/minecraft-plugin/src/main/resources/bungee.yml @@ -0,0 +1,5 @@ +name: DeskHub-Minecraft +main: de.deskhub.minecraft.bungee.BungeePlugin +version: ${project.version} +author: M_Viper +description: Legt in DeskHub ein Ticket an, wenn ein Minecraft-Server offline geht. diff --git a/minecraft-plugin/src/main/resources/config.yml b/minecraft-plugin/src/main/resources/config.yml new file mode 100644 index 00000000..828fed45 --- /dev/null +++ b/minecraft-plugin/src/main/resources/config.yml @@ -0,0 +1,83 @@ +# ────────────────────────────────────────────────────────────────────────────── +# DeskHub-Minecraft: legt in DeskHub ein Ticket an, wenn ein Minecraft-Server offline geht. +# Eine Jar für Spigot, Paper und BungeeCord. +# +# • Auf dem BungeeCord-Proxy: prüft die Server von außen und meldet einen Ausfall in Echtzeit (empfohlen). +# • Auf Spigot/Paper: erkennt beim nächsten Start, dass der Server abgestürzt oder abgeschaltet worden war. +# Am besten beides einsetzen: Proxy meldet sofort, der Server liefert die Gewissheit nach dem Neustart. +# +# Nach Änderungen: /deskhubmc reload Testen: /deskhubmc test +# ────────────────────────────────────────────────────────────────────────────── + +deskhub: + # Adresse eurer DeskHub-Installation, ohne Pfad. Bitte https verwenden (der Schlüssel wird mitgeschickt). + url: "https://support.example.de" + # API-Schlüssel: DeskHub-Adminportal, Verwalten, API-Schlüssel, neuer Schlüssel mit dem Recht "Tickets anlegen". + # Tipp: Im Schlüssel die IP-Adresse des Minecraft-Servers eintragen, dann ist er nur von dort nutzbar. + api-key: "" + # Absender der Tickets (es kommt keine Mail dorthin, aber DeskHub verlangt eine gültige Adresse) + requester-name: "Minecraft-Überwachung" + requester-email: "minecraft@example.de" + # Wartezeit auf DeskHub in Sekunden + timeout-seconds: 10 + +ticket: + # Zahlen aus DeskHub. 0 = Standard von DeskHub. + # Zu finden im Adminportal: Abteilungen / Prioritäten öffnen, die Zahl steht am Ende der Adresse (…?id=3). + department-id: 0 + priority-id: 0 + # Optional: Hilfethema (legt Abteilung und Priorität automatisch fest, wenn die beiden oben 0 sind) + help-topic-id: 0 + + # Texte. Platzhalter: {server} {time} {source} {lastSeen} {downtime} + offline: + subject: "[Minecraft] {server} ist offline" + message: | + Der Minecraft-Server "{server}" ist seit {time} nicht mehr erreichbar. + + Erkannt von: {source} + Bitte den Server prüfen und neu starten. + crash: + subject: "[Minecraft] {server} war nicht erreichbar (Absturz oder Ausfall)" + message: | + Der Minecraft-Server "{server}" wurde nicht ordnungsgemäß beendet (Absturz, Prozess beendet oder Stromausfall). + + Letztes Lebenszeichen: {lastSeen} + Neu gestartet: {time} + Ausfalldauer: ca. {downtime} + + Bitte die Logdateien prüfen. + shutdown: + subject: "[Minecraft] {server} wurde heruntergefahren" + message: 'Der Minecraft-Server "{server}" wurde um {time} heruntergefahren.' + test: + subject: "[Minecraft] Testticket von {server}" + message: | + Das ist ein Testticket des DeskHub-Minecraft-Plugins. Die Verbindung zu DeskHub funktioniert. + + Server: {server} + Zeit: {time} + +# Nur BungeeCord: Der Proxy prüft die Server von außen. +monitor: + # Alle wie viele Sekunden geprüft wird + interval-seconds: 30 + # Wie oft die Prüfung hintereinander fehlschlagen muss, bevor ein Ticket entsteht (verhindert Tickets bei kurzen Aussetzern) + failures-before-ticket: 3 + # Frühestens nach so vielen Minuten gibt es für denselben Server wieder ein Ticket (verhindert Ticket-Flut, wenn ein Server ständig ausfällt) + cooldown-minutes: 30 + # true: Server, die seit dem Start des Proxys noch nie online waren, werden ignoriert (z. B. absichtlich abgeschaltete Server) + ignore-never-online: true + # Welche Server überwacht werden (Namen aus der Proxy-Konfiguration). Leer = alle. + servers: [] + +# Nur Spigot/Paper: Der Server meldet sich selbst. +backend: + # Wie oft das Lebenszeichen geschrieben wird (Sekunden) + heartbeat-seconds: 10 + # Ticket beim nächsten Start, wenn der Server zuvor abgestürzt oder hart beendet worden war + ticket-on-crash: true + # Ticket auch beim normalen Herunterfahren (/stop, Neustart). Meist unerwünscht, deshalb aus. + ticket-on-shutdown: false + # Name des Servers in den Tickets. Leer = "Minecraft-Server (Port 25565)" + server-name: "" diff --git a/minecraft-plugin/src/main/resources/plugin.yml b/minecraft-plugin/src/main/resources/plugin.yml new file mode 100644 index 00000000..0b96819f --- /dev/null +++ b/minecraft-plugin/src/main/resources/plugin.yml @@ -0,0 +1,16 @@ +name: DeskHub-Minecraft +main: de.deskhub.minecraft.bukkit.BukkitPlugin +version: ${project.version} +api-version: '1.13' +author: M_Viper +description: Legt in DeskHub ein Ticket an, wenn der Minecraft-Server abgestuerzt oder offline war. +commands: + deskhubmc: + description: DeskHub-Minecraft verwalten (test, status, reload) + usage: /deskhubmc + aliases: [dhmc] + permission: deskhubmc.admin +permissions: + deskhubmc.admin: + description: DeskHub-Minecraft verwalten + default: op 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 new file mode 100644 index 00000000..5b54a2ed --- /dev/null +++ b/minecraft-plugin/src/test/java/de/deskhub/minecraft/core/CoreTest.java @@ -0,0 +1,248 @@ +package de.deskhub.minecraft.core; + +import com.sun.net.httpserver.HttpServer; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class CoreTest { + private static final String KEY = "dh_" + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4"; + + /** Einstellungen aus einer einfachen Map (wie eine config.yml). */ + private static ConfigSource cfg(final Map m) { + return new ConfigSource() { + public String getString(String p, String d) { return m.containsKey(p) ? String.valueOf(m.get(p)) : d; } + public int getInt(String p, int d) { return m.containsKey(p) ? ((Number) m.get(p)).intValue() : d; } + public boolean getBoolean(String p, boolean d) { return m.containsKey(p) ? (Boolean) m.get(p) : d; } + @SuppressWarnings("unchecked") + public List getStringList(String p) { return m.containsKey(p) ? (List) m.get(p) : Collections.emptyList(); } + }; + } + + private static Map valid(String url) { + Map m = new HashMap(); + m.put("deskhub.url", url); + m.put("deskhub.api-key", KEY); + m.put("deskhub.requester-email", "mc@example.de"); + return m; + } + + @Test + void settingsValidation() { + assertTrue(new Settings(cfg(valid("https://support.example.de/"))).problems().isEmpty()); + assertEquals("https://support.example.de", new Settings(cfg(valid("https://support.example.de///"))).url); + assertFalse(new Settings(cfg(new HashMap())).problems().isEmpty()); + Map bad = valid("ftp://x"); + assertTrue(new Settings(cfg(bad)).problems().get(0).contains("https://")); + bad = valid("https://x.de"); + bad.put("deskhub.api-key", "falsch"); + assertTrue(new Settings(cfg(bad)).problems().get(0).contains("api-key")); + bad = valid("https://x.de"); + bad.put("deskhub.requester-email", "keine-mail"); + assertTrue(new Settings(cfg(bad)).problems().get(0).contains("requester-email")); + assertFalse(new Settings(cfg(valid("http://x.de"))).warnings().isEmpty(), "http wird bemängelt"); + assertTrue(new Settings(cfg(valid("https://x.de"))).warnings().isEmpty()); + } + + @Test + void settingsLimits() { + Map m = valid("https://x.de"); + m.put("monitor.interval-seconds", 1); + m.put("monitor.failures-before-ticket", 0); + m.put("ticket.department-id", -4); + m.put("backend.heartbeat-seconds", 100000); + Settings s = new Settings(cfg(m)); + assertEquals(5, s.monitorIntervalSeconds); + assertEquals(1, s.monitorFailures); + assertEquals(0, s.departmentId); + assertEquals(600, s.heartbeatSeconds); + } + + @Test + void jsonQuotingAndParsing() { + assertEquals("\"a\\\"b\\\\c\\nd\\u0001\"", Json.quote("a\"b\\c\nd\u0001")); + assertEquals("T-123", Json.ticketNumber("{\"number\":\"T-123\",\"status\":\"open\"}")); + assertNull(Json.ticketNumber("{}")); + assertEquals("Fehler \"x\"", Json.errorMessage("{\"error\":{\"code\":\"X\",\"message\":\"Fehler \\\"x\\\"\"}}")); + assertEquals("\u00e4", Json.errorMessage("{\"error\":{\"message\":\"\\u00e4\"}}")); + } + + @Test + void templates() { + Map v = Template.vars("Lobby", "Proxy", 0); + assertEquals("Lobby ist offline {unbekannt}", Template.apply("{server} ist offline {unbekannt}", v)); + assertEquals("30 Sekunden", Template.formatDuration(30000)); + assertEquals("5 Minuten", Template.formatDuration(5 * 60000)); + assertEquals("3 Std. 5 Min.", Template.formatDuration((3 * 60 + 5) * 60000L)); + assertEquals("3 Tage 2 Std.", Template.formatDuration((3 * 24 + 2) * 3600000L)); + } + + @Test + void outageTrackerRules() { + OutageTracker t = new OutageTracker(3, 30, true); + long now = 1_000_000; + // Server war nie online: ignoriert + for (int i = 0; i < 5; i++) assertEquals(OutageTracker.Decision.NONE, t.report("neu", false, now += 1000)); + // normal online, dann Ausfall: erst nach 3 Fehlschlägen ein Ticket, nur einmal + assertEquals(OutageTracker.Decision.NONE, t.report("lobby", true, now)); + assertEquals(OutageTracker.Decision.NONE, t.report("lobby", false, now += 1000)); + long since = now; + assertEquals(OutageTracker.Decision.NONE, t.report("lobby", false, now += 1000)); + assertEquals(OutageTracker.Decision.OPEN_TICKET, t.report("lobby", false, now += 1000)); + assertEquals(since, t.offlineSince("lobby")); + assertEquals(OutageTracker.Decision.NONE, t.report("lobby", false, now += 1000)); + // Aussetzer: kurz online und wieder weg -> kein zweites Ticket innerhalb der Wartezeit + assertEquals(OutageTracker.Decision.RECOVERED, t.report("lobby", true, now += 1000)); + for (int i = 0; i < 3; i++) assertEquals(OutageTracker.Decision.NONE, t.report("lobby", false, now += 1000)); + // nach der Wartezeit gibt es wieder ein Ticket + assertEquals(OutageTracker.Decision.RECOVERED, t.report("lobby", true, now += 1000)); // der unterdrückte Ausfall endet + now += 31 * 60000L; + t.report("lobby", false, now); + t.report("lobby", false, now += 1000); + assertEquals(OutageTracker.Decision.OPEN_TICKET, t.report("lobby", false, now += 1000)); + // ein einzelner Aussetzer reicht nicht + OutageTracker t2 = new OutageTracker(3, 0, true); + t2.report("a", true, 1); + assertEquals(OutageTracker.Decision.NONE, t2.report("a", false, 2)); + assertEquals(OutageTracker.Decision.NONE, t2.report("a", true, 3)); + assertEquals(1, t2.snapshot().size()); + // Server, die nie online waren, gelten mit ignore=false auch + OutageTracker t3 = new OutageTracker(1, 0, false); + assertEquals(OutageTracker.Decision.OPEN_TICKET, t3.report("x", false, 1)); + } + + @Test + void heartbeatDetectsCrash() throws IOException { + File dir = Files.createTempDirectory("mc").toFile(); + File f = new File(dir, "sub/lebenszeichen.properties"); + HeartbeatFile h = new HeartbeatFile(f); + assertFalse(h.readPrevious().existed, "kein früherer Lauf"); + h.start(1000); + h.beat(2000); + // Server stürzt ab: kein markClean -> nächster Start erkennt es + HeartbeatFile h2 = new HeartbeatFile(f); + HeartbeatFile.Previous p = h2.readPrevious(); + assertTrue(p.crashed()); + assertEquals(2000, p.lastBeat); + h2.start(5000); + h2.markClean(6000); + // ordentlich beendet: kein Absturz + HeartbeatFile h3 = new HeartbeatFile(f); + assertFalse(h3.readPrevious().crashed()); + assertTrue(h3.readPrevious().existed); + // kaputte Datei bricht nichts + Files.write(f.toPath(), new byte[]{0, 1, 2}); + assertDoesNotThrow(() -> new HeartbeatFile(f).readPrevious()); + } + + @Test + void ticketIsSentToDeskHub() throws Exception { + final List bodies = new ArrayList(); + final List keys = new ArrayList(); + final List agents = new ArrayList(); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/api/public/v1/tickets.php", ex -> { + keys.add(ex.getRequestHeaders().getFirst("X-API-Key")); + agents.add(ex.getRequestHeaders().getFirst("User-Agent")); + bodies.add(readAll(ex.getRequestBody())); + byte[] out = "{\"number\":\"T-4711\",\"status\":\"open\"}".getBytes(StandardCharsets.UTF_8); + ex.getResponseHeaders().add("Content-Type", "application/json"); + ex.sendResponseHeaders(201, out.length); + ex.getResponseBody().write(out); + ex.close(); + }); + server.createContext("/fehler/api/public/v1/tickets.php", ex -> { + byte[] out = "{\"error\":{\"code\":\"INVALID_PRIORITY\",\"message\":\"Die Priorität ist ungültig.\"}}".getBytes(StandardCharsets.UTF_8); + ex.sendResponseHeaders(422, out.length); + ex.getResponseBody().write(out); + ex.close(); + }); + server.createContext("/weiter/api/public/v1/tickets.php", ex -> { + ex.getResponseHeaders().add("Location", "http://evil.example/"); + ex.sendResponseHeaders(302, -1); + ex.close(); + }); + server.createContext("/auth/api/public/v1/tickets.php", ex -> { + byte[] out = "{\"error\":{\"code\":\"UNAUTHENTICATED\",\"message\":\"x\"}}".getBytes(StandardCharsets.UTF_8); + ex.sendResponseHeaders(401, out.length); + ex.getResponseBody().write(out); + ex.close(); + }); + server.start(); + try { + String base = "http://127.0.0.1:" + server.getAddress().getPort(); + Map m = valid(base); + m.put("ticket.department-id", 3); + m.put("ticket.priority-id", 2); + TicketClient c = new TicketClient(new Settings(cfg(m)), "1.0.0"); + TicketResult r = c.create("[Minecraft] Lobby \"offline\"", "Zeile 1\nZeile 2 ä ö ü"); + assertTrue(r.ok, r.toString()); + assertEquals("T-4711", r.number); + assertEquals(KEY, keys.get(0)); + assertEquals("DeskHub-Minecraft/1.0.0", agents.get(0)); + String b = bodies.get(0); + assertTrue(b.contains("\"departmentId\":3") && b.contains("\"priorityId\":2") && !b.contains("helpTopicId"), b); + assertTrue(b.contains("\"subject\":\"[Minecraft] Lobby \\\"offline\\\"\""), b); + assertTrue(b.contains("\"message\":\"Zeile 1\\nZeile 2 ä ö ü\""), b); + assertTrue(b.contains("\"email\":\"mc@example.de\""), b); + + // ohne Abteilung/Priorität werden sie nicht mitgeschickt (DeskHub nimmt den Standard) + TicketClient plain = new TicketClient(new Settings(cfg(valid(base))), "1.0.0"); + assertTrue(plain.create("a", "b").ok); + assertFalse(bodies.get(1).contains("departmentId") || bodies.get(1).contains("priorityId")); + + // Fehlermeldung von DeskHub wird weitergegeben + r = new TicketClient(new Settings(cfg(valid(base + "/fehler"))), "1").create("a", "b"); + assertFalse(r.ok); + assertEquals(422, r.httpCode); + assertEquals("Die Priorität ist ungültig.", r.error); + // Weiterleitungen werden nicht verfolgt (sonst ginge der Schlüssel woandershin) + r = new TicketClient(new Settings(cfg(valid(base + "/weiter"))), "1").create("a", "b"); + assertFalse(r.ok); + assertEquals(302, r.httpCode); + r = new TicketClient(new Settings(cfg(valid(base + "/auth"))), "1").create("a", "b"); + assertEquals(401, r.httpCode); + assertTrue(r.error.contains("API-Schlüssel")); + } finally { + server.stop(0); + } + // Server nicht erreichbar + Map m = valid("http://127.0.0.1:1"); + m.put("deskhub.timeout-seconds", 2); + TicketResult down = new TicketClient(new Settings(cfg(m)), "1").create("a", "b"); + assertFalse(down.ok); + assertEquals(0, down.httpCode); + assertTrue(down.error.contains("keine Verbindung")); + // unvollständige Einstellungen: keine Anfrage + TicketResult incomplete = new TicketClient(new Settings(cfg(new HashMap())), "1").create("a", "b"); + assertFalse(incomplete.ok); + assertTrue(incomplete.error.contains("unvollständig")); + // lange Texte werden gekürzt + char[] longText = new char[30000]; + Arrays.fill(longText, 'x'); + String body = new TicketClient(new Settings(cfg(valid("https://x.de"))), "1").buildBody(new String(longText), new String(longText)); + assertTrue(body.length() < 20300 + 400); + } + + private static String readAll(InputStream in) throws IOException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int n; + while ((n = in.read(buf)) > 0) b.write(buf, 0, n); + return new String(b.toByteArray(), StandardCharsets.UTF_8); + } +} diff --git a/minecraft-plugin/target/classes/bungee.yml b/minecraft-plugin/target/classes/bungee.yml new file mode 100644 index 00000000..1b2d5501 --- /dev/null +++ b/minecraft-plugin/target/classes/bungee.yml @@ -0,0 +1,5 @@ +name: DeskHub-Minecraft +main: de.deskhub.minecraft.bungee.BungeePlugin +version: 1.0.0 +author: M_Viper +description: Legt in DeskHub ein Ticket an, wenn ein Minecraft-Server offline geht. diff --git a/minecraft-plugin/target/classes/config.yml b/minecraft-plugin/target/classes/config.yml new file mode 100644 index 00000000..828fed45 --- /dev/null +++ b/minecraft-plugin/target/classes/config.yml @@ -0,0 +1,83 @@ +# ────────────────────────────────────────────────────────────────────────────── +# DeskHub-Minecraft: legt in DeskHub ein Ticket an, wenn ein Minecraft-Server offline geht. +# Eine Jar für Spigot, Paper und BungeeCord. +# +# • Auf dem BungeeCord-Proxy: prüft die Server von außen und meldet einen Ausfall in Echtzeit (empfohlen). +# • Auf Spigot/Paper: erkennt beim nächsten Start, dass der Server abgestürzt oder abgeschaltet worden war. +# Am besten beides einsetzen: Proxy meldet sofort, der Server liefert die Gewissheit nach dem Neustart. +# +# Nach Änderungen: /deskhubmc reload Testen: /deskhubmc test +# ────────────────────────────────────────────────────────────────────────────── + +deskhub: + # Adresse eurer DeskHub-Installation, ohne Pfad. Bitte https verwenden (der Schlüssel wird mitgeschickt). + url: "https://support.example.de" + # API-Schlüssel: DeskHub-Adminportal, Verwalten, API-Schlüssel, neuer Schlüssel mit dem Recht "Tickets anlegen". + # Tipp: Im Schlüssel die IP-Adresse des Minecraft-Servers eintragen, dann ist er nur von dort nutzbar. + api-key: "" + # Absender der Tickets (es kommt keine Mail dorthin, aber DeskHub verlangt eine gültige Adresse) + requester-name: "Minecraft-Überwachung" + requester-email: "minecraft@example.de" + # Wartezeit auf DeskHub in Sekunden + timeout-seconds: 10 + +ticket: + # Zahlen aus DeskHub. 0 = Standard von DeskHub. + # Zu finden im Adminportal: Abteilungen / Prioritäten öffnen, die Zahl steht am Ende der Adresse (…?id=3). + department-id: 0 + priority-id: 0 + # Optional: Hilfethema (legt Abteilung und Priorität automatisch fest, wenn die beiden oben 0 sind) + help-topic-id: 0 + + # Texte. Platzhalter: {server} {time} {source} {lastSeen} {downtime} + offline: + subject: "[Minecraft] {server} ist offline" + message: | + Der Minecraft-Server "{server}" ist seit {time} nicht mehr erreichbar. + + Erkannt von: {source} + Bitte den Server prüfen und neu starten. + crash: + subject: "[Minecraft] {server} war nicht erreichbar (Absturz oder Ausfall)" + message: | + Der Minecraft-Server "{server}" wurde nicht ordnungsgemäß beendet (Absturz, Prozess beendet oder Stromausfall). + + Letztes Lebenszeichen: {lastSeen} + Neu gestartet: {time} + Ausfalldauer: ca. {downtime} + + Bitte die Logdateien prüfen. + shutdown: + subject: "[Minecraft] {server} wurde heruntergefahren" + message: 'Der Minecraft-Server "{server}" wurde um {time} heruntergefahren.' + test: + subject: "[Minecraft] Testticket von {server}" + message: | + Das ist ein Testticket des DeskHub-Minecraft-Plugins. Die Verbindung zu DeskHub funktioniert. + + Server: {server} + Zeit: {time} + +# Nur BungeeCord: Der Proxy prüft die Server von außen. +monitor: + # Alle wie viele Sekunden geprüft wird + interval-seconds: 30 + # Wie oft die Prüfung hintereinander fehlschlagen muss, bevor ein Ticket entsteht (verhindert Tickets bei kurzen Aussetzern) + failures-before-ticket: 3 + # Frühestens nach so vielen Minuten gibt es für denselben Server wieder ein Ticket (verhindert Ticket-Flut, wenn ein Server ständig ausfällt) + cooldown-minutes: 30 + # true: Server, die seit dem Start des Proxys noch nie online waren, werden ignoriert (z. B. absichtlich abgeschaltete Server) + ignore-never-online: true + # Welche Server überwacht werden (Namen aus der Proxy-Konfiguration). Leer = alle. + servers: [] + +# Nur Spigot/Paper: Der Server meldet sich selbst. +backend: + # Wie oft das Lebenszeichen geschrieben wird (Sekunden) + heartbeat-seconds: 10 + # Ticket beim nächsten Start, wenn der Server zuvor abgestürzt oder hart beendet worden war + ticket-on-crash: true + # Ticket auch beim normalen Herunterfahren (/stop, Neustart). Meist unerwünscht, deshalb aus. + ticket-on-shutdown: false + # Name des Servers in den Tickets. Leer = "Minecraft-Server (Port 25565)" + server-name: "" diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitConfig.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..c46e86616cf15d906f41a0e87d232b3517d84bda GIT binary patch literal 1262 zcmb7DO>fgc5Ph4ZH8xH^0;GHdXrYuiDcvIVfRxfhD^*0Y=piVF+#JVAwsCFbtocW*pC^{yqSIT{PWlM9{>)pZy<-fhJuMDEHkW~J0BeGJHddT zb*Vd5rChHB5_J#Rc3#$A3P0?&<{zU19_G!l{@C&x(` z^u>U(-9GIv?Jyer32>87apXu52D~qPkDt<(W=!1;11nh7ux4T%B?f-Ydn zK;>Um?Y|F9osC70wdhh^+%|CsWg>(Cn*lLX&OmEgO%;3B#68?+Fevso41LcDvg12e zN5xl6Jj5f0;$^e87*mPuj1trKVkvyCq8b`D8Fn&9;f9gNQ$kx|9JwCDLG{`*u-XiT zldwnGt2T}KZG6%7qIXW$Cr`-^UB_=bkx=&3S&&09W~kYjU1bQQK^5LRa;WeYMnvlA zd3^@fq}8F3w!|QCWE@fXtdY($bhrq4LNsHA^7Rj;qmT((Pg?)%vYf(E|Ngn$&1g(+3-jZNN5CdGld*8DYvY+w9I7{Y5@E z@uTs*Kmhc4;t zg*_@;Xpt3OCL>Qo$77;$EA)BL<&iY;B$+dQU|65Nz^NwMp?yS&VkmYr_lIqMI^nWA zVG{Ym?3vd1r}H+=VANAn`9T;<)o%+u2zw4(W2i!?nv-FjeWgUS7w|Y1F;>Xv1u)zN zE0v7d$^RMp&Er00EA-l>*ktVb+Gnu(S6F8$<^i2~!T=3Cq;q+U!YZm15k6&Ogs8!p zaJ@k`MV0HHz|K)T$I=&E`-oiYEM+R9ggb;W+-2nG+(r?PDf@_={v_2gT;|`gPMzKs dGDjBGQQ;be8LVT2?i_t2o79|Xg{89e`~_Q7@k;;z literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin$2.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin$2.class new file mode 100644 index 0000000000000000000000000000000000000000..c84ff34492538ab2e373a27cf2dabb8949ca049c GIT binary patch literal 3056 zcmb_e>vt1H6#q?Mo2G-bffi7Nh_$pSkhWMvf~YMZ)D$YIMZgD}WSVZX*^RriL43dP z{T3hi;47a!rsZBZM9DCB4**lMW@9+NZo%!RRUw#8{KYnaQ z0~$3naWtcaA(0o!yl{&%UN%`WERl2cX_?G=#iAjTBYrnwdIiJk9%5*bGltteK)cdw z-Ih}NA{?!VGH4au?6&L+3~WrvImbHO!N8Tg?1+kCdoIIJ{nK-{Ba+92?#QgrE zZP>0M&T$WRFzl#npbyBTD3wiJ3WlbN?htzWdRJ;%DH#jx@uXoU$MrH9)6EbubysGD zuv*cBof>*M`c&WsR@YxYAmLFX6e#6NKgTZYCiJFlr=-hCqeS+0_EwGX1GNZBlA+bJ zvZAP4mTbcyhBWNu#NA4O{xl%;nBmLB#TT^hEf2y>9*ArOJ86xgdMFnL=PSRKYmBUco1)HH-8iOpSjgyT_+ zENL53+9v)dZ5bgYG0>&#Jd7h6j&eMPW4Gj4bqi&y>-jk~>getaL#zhrFop`qF^g!37pc9;h4me3_WW@mdZfR zAx;wePONHAEgSplCwP!~e#lM<*E1ync^Xe?IKy!k=hk3Pu#KU?E>bl-t0Mjk$Fq2j z$X^gLZ5Il{Vc6PRo2Mp8DQwALHl;yl*!17BGU6F#UR99u=!hUsRX5;PTuG)m3ZS61 zP*j9Ple_D{P@5=0q{1_OFK0Q5DjY4ASIP<}f)bH5PNi0uq9FGuEjC9P&r@beJJ`># zvFb=Ix>2~08a$2)=u+6cl-$cbrP^j#)3z!ZE6LV36#?;UQ|P5-tKoab!dq#P1n(uxPkRQ`ODv?wuKG_^0-88mw!Yj-od-{MO)Rg zkLKT_aRWBe=zYJXBDM;W>=8rwIz4ZP2JKpaU})hCxMl zmY^zP4krFY$%ic-01?90f#2}~J|wU%VFDlFV}kAzI)jrWnERAQpONGoT5e)D8Z`AA z^cxMI-=x{9h~D0VB=V&_i&C|E)rEiKoQBie$)qnyMFTG5E9#plwqN54jbbEIL{Wb| J_zpi{%fHGFSAPHi literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin$3$1.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/bukkit/BukkitPlugin$3$1.class new file mode 100644 index 0000000000000000000000000000000000000000..dbb638813d8c8ef3069a14a4cfc68280e70e8ddb GIT binary patch literal 1432 zcma)6TT|0e5dKbENLzxGa`OUOv0__FBciAjQK_J4t4NtT`j)20HZ)C`T<}jgqpyxX zfM;hAMxF72M}L&Mrv*`4M>3PMdp3K%{dTi^zJ34v6~I*#Lh!*aAt0j*-3;jpS1R18 z*SxaQ&`qw{>PuHCd-b~RDvM3AYIs%MjL*cU8M@t??g%BFKmNh2h#8a-LYSdzOEuy) zL!vW)X4za>*R?u#*SO;uZZ-@Vy%J8y=tG1dvY~FNilLfSSP9K)G*q*q zVWMNKwu~5jR-J*pkZ_h^C1K1|#OEMBj5`^hB$|Bcjn|n5hNh)^im1kFYjH=BIUUf5qz?6&( z6o#N{wHPxDCfWlM0`4UU+>mh- za}2TnRi2?o{H6kT9D-yR>QsV~gn3Hss5Wb!X{u#|{Ga`!`#Sc0_PCvzEHMOfR)qre z2>rj0cSe+ncrTfnVL8->1@6|Yii~?==;RgCNjE~+v+#f?Bvq=M7fzwnI=C@j=r)>nouH`pOJG0C%;WbS+vq-0k;~vwczl$m+ zkz`07ax3EsyDl7t%f*h<7o3;Z=VXNeB^qf%`jv5jw5WZVzx!ev@FvN zV%th$4(}?stl~Xnh`h+yjE*C;ifDH&?g@s(A$REbt?Ves(VeY+D86S}T8Vdw{4}nr zxQ6TrKg*Jz%75bL9H6Gr5?;YFDcrzYUd22XPQ2Za-gf^dZ)M?mlpdD^c^#U9MHNf9 zaW0dCBMein9|*2OIb=wWvM1AXQ^g8yFt$C`qVRjlI^hO6W8N)=)_B2dHjnQ(%N>Z>;wt^H70LBjTg>svBmpJ78mLB;2| zO}ZT_=CExrs14zjji#vkmMr*eHaA@I?%++o%^ZKJE4ah35E)=(W6CTgv59-KOKdUh zk1v?avOaFOx#|>(C@I)hv4dTP%uv8*hQgpYzFpyNi{Z=caP-}vscK+9NTZG@!vzhMn;_(1%e)x8YosdP$*Er z_Z!2(DK$j%eokhF71PK1a1?Cr0Ttmr+ZIm2;;t)P>QXVfQ%Q`7rBd=z256*T0`%h` zM*q_&(^#QdfyTJp$+(Hm)aJO3E0hne)H9$xF=onpAY5)Qe?cz?c!<>S?=T>XKMN6-*5b_pTF@ofQ#iDGf{?e4G|p`m?5Z-Tdi>`Jv5ljbMO77Kl%))FzW!iE_EVtWCT7tRt4P7^zBW7#DbOu^`JeNfMlji6+2Q`A3 zgO=%feU?d98>TsGYi`*VJ7Mv>xtOP6zK#VrSFkkY4&>A@>?@$vKXe;U&MV{!+p)d% zg7W%?y^7yD9gEcFbJFIBwb|-7GYM}~%IUWU)a>~>7OUAvB4x&-Fvw}B`D9GZFVk@W zmQ&gRDlnBynoeABY5nQKHgpv*7YkOl226$fD9sA2)KIUZ0gVFV!!`)2XrGQlRxIPC zT*1ovQfGwJbIaNr_G(zg#Z$XOk&ZRA2y|bKcB)lrU5UL~$3<8pm_@NSo1VEPm55tp zVNTA@;o9~n-52L3tUk+0AF=G%pyjj-yQyKz^=vC$iDq1?jJHm3L8!Fc&Jrr5y_#^j zTHi){OW0gNBVdHeoYmD{$b$ zY0pZMhgimS8D711(%NAs5;l*bUu{#IY|(KIwhAiJ9zB~LRKHEBew&~+r_Iit9fxAp zuxFE` zMIR&GsNc|+_D;U#9X@lA;TrqRoXq1a5SWJ4C5BT>a+DY8j4V!9<-<{y3zibNW;^R(J_L9f@M?F z%ONl$((}Q5r?W{>63Bk#T8DKU!LJZ7Tx&Sxdf^;#YHE^2C@>KebYjKUZ8|=Jk1{Co zxZPvLQcj!!Ae|M=TI%22O5BCJHQb})UfjoUDL}L!O)*eu*&f0|Jd;rVUQd57SVfl# zX2P_VUrBd;K+sr{{St|A`9+nu9}g<%__*(fXS$JaS%pvHQyL!9@o9X9iSSI=6V$mG zr`JpmDQIvvqz^j`j+8?%N*osS6d`q(s)A(DUf@`}Gmc~SG0$z=Hmz3FIes>MM8~6e zj1I37sTyUZOs7tNV--G&qZ*#j@ubRhOQw-8AUPvuIx#DuK<+6WPb-j{nKa#@O$p0% zlyiJu#}|~t&dK>=&tS%jrw$V3()f~&XYpn7$TUFNlJuCGB9QB{(_VWOM)ACcujqI| zh0^SaNLAAe6g%CK9QF=J@seO(pgP0M^c!(2Y4rG&Mbi<5j#iZS?e7$>9Wn zj37N?x@4$S0xz&jSijIH6Kw4;hiQT|j_G(ESwVF=(|5BK^Kzn7%F`(*7LV%~#~aKO zL`mfvmy`g>N#~TTrP6py$2a_=lL1oZj;_Qz_@;(m)o}tRiQpxD|Fl9-)s9S}N-Nz{ zvQ&^M+NaB~L4~>7O0&44ntmPcYWNKuzp1?BL-`4|3CdDKQGAQG52zW6&hO~>T_y7j zCzI^6+$eruu*hJMX(g-y#OF%CV;?Khd5oQ9St{*YNNBoL&n+~PK$;2-JuV@0sS za@`d3-T=O%<2`(rzF|B4DLPvTWdxPv_jLT3iXhe=s#*++NDY5}Mg{vs$@!N${tAB` zlmWr0U~Mrh5@}hVW?WO14z2wx%KXYKSRD>Ce|{f7REYOCs{Sd2fiv2!WJ=?26@kAK zG)DTFjd0&2OU4gKQeGtfoP>;uI-mOick@h6=7e&!mzymXZ(wXf7S7C zDvhr!K&I*T1=aXo%T4=*)jRkPCB%OUE}u?_Tna4&;4shwRs6S(|M7wApy@iQDN-2q ze>&dBPl#qVoy_#h@cBh())|fy(LTV>H2hr0DTQb?6VQ=cLeCyF)7{n~k84GADU))# zox_329n?-ATiP|LD6UKW0*X#6nl4f0v|7k)RM^g4(UHy-OG6g>Mk9!WbUt2I8GUxz zh})JCxJb+4WWq>$b~5R;MadA#jb=tkT4t*zFIy=QJRg;F7*RWNZD?}LRV0Oer?mq`b-wEKokbddG(#ALt3s3j7lAAiiW71Cpf&>azk`CD4201 zGm>({!Y}2^a+EC@8p~MfR&kLVZO?`pv z!|utq`hDDx#e&_1$7e|POc-b}l-6xGZj?B)MH9A^XI{pvsgxzco^yd*Ah>A?cP>%? zv~N56O3$x+4VEQ!WWZA>)js5()ND&pL5?pv@qfNrx_5#ObX|SVusx9ST6dXj_Zh%D zXjTq~YumV_Ut!5kU3RIATb^+fQQ6HlA;`-NmjTO{(u+zjhq-q{AKOhOb$<6=U9MA& zh#t3w*=Hx&jd#YGfn=X9`{e*xwd1`$f=&`A#P+DLL#@3WouGVYP)0B?QrQMs8F zjp;;%{du{mbV(;&tQaY-S&BCe(}6r{B`S3B1w3QBk18B7k|(nR2{z)27g}!#DfM#P z^ZUU7fpVpe@q^o+(`4jqkY{bBWaN-S9@eNk&jNXh3lIN>thFdqSusR~y=f!;D0i)y zj2TKwrHy8zKnl}gS!Ypcx3Rr0ULSa5u%p=bO@}v6{w|ubrFd5mg2Ao=+*c9h$CC=q zJ9N2Id6G6_CNkE}{;1r|gx}-wuFrJin%v9gqM$U1g_nDiF5YOkO{qAm>FO@#%5J}0 z?N!~K>I_mbGqKlnZS^}GjCh0UI;pWVaF@b(GL>>V{Em?slXT|=eFJaQ3T~PRJZ!yk z7h`KmllQxUDQ*Dhu1<<|yYMP&1|!=}(er0f9kC%cK_T}uQ_f~P9f+qKt$I=m!|in7 zDcq=_7YSPihEcJO4iD$_bV)JgD}tYejmhaF>sdKHMW!ceFmg_olvI4qDIn>0qw4y5 zVyzk~am;#ZmSTry7xWip#z|fgWU2OPf}=2dhFEprbV(ZaD=-dI-m+=Pg$L4$Lrz$B zO|zV`R3un35u75Xt3CkKmN=cRz&2NtukyZQ>Il%}1wx%oFR3(RG4`g*SFc_z*fWLo zOS$%Ry=9S`lJBxZB%V9zB6ZUGvpUH)bfN!DP+Oy@?K&%JQ#afBhGT;MT1CJ2|A~H( zY{IWPJR|7ORAa?ly0PFZQHzq88MM=JSf?b+blRf5qq%!Q2VM{CinC241aKuJivFgfeYkg@*tlg zAJ1);1Bm)ZyxN$>LJ~Nyv1UmYOW(z;;Dy5%Hol3beQ%<9AAhvgtR2H8W4P>fR5u<& zbr$Vi$8psL^=@R*F^ZJ~lrxKQfL_XuSM+_*IH@6)*p{ z@dCTt7q|_xDcl9r-*O6dAwM{+z*?+CJLZ zL+8r*RP>{HMZZXz>dN*#%J=ZQ(A8rcj|-^1tNEk)T~w(Dwg0(MEblI%!{$JRf%dpo zdlwI?$1yx}6nCh%EWS97=LARLQeRayFD^NbuXb}rf^}bSK8~^A^-cAf#oK|k7DkTY z*N$RY&Sbyko9q~VTbbf_)zZfGA;6_bj377;rl8S{$dP27{iZVhsM!&_9?Y;^zXCyr!oBd z>w(I%_^)yNub``G96wcdIk#zCMe8>uvO`&TRaRz3T=1`#HQj?YWwX!e} z?iKC|!d+c}0sAoz2MRE5O~9tlN;za67x9}+c7I@d`(s^ z8O4lRxvZ^XVMVR9W#x*a*s2h`sZct?YOP#7iWPaKZlof^73JfyxlH+Dpj-cpQF&6) z+p6fvIXrrsKirhx-xcmZCfzifPygCcR0fvpE}%5Ar6<2-&v8-NpjNKW58V(95!EIg zq%y^6qQnYCu_jhb5``0|6CB8$>1Uf-v3&&&akN&F`8i@KRW>n~qce&-m10u9qOUJx z>Bw=*Bz!Pvc1UgnkIcX79T z5BJIs`PJ}!JX%(U$I7bkc-dSW^<8*<;E6dGE}JjU%9mBx^6fbpRpH0C=L3A;+gIcT zM%OC&vAoE!3gzI^jhY438;Po?i2XBEdXy$jUgCfzU*lf_u{52+vXXPb;)r&Nskzi% ze;#{IshBB2x?G)wJ^tabk}L8AN}pdypniG+WhJKz37lJUMV^BXBQWcy*u1!o|6PdJ zEg_n1p)C}MR@Xg^%DUrnyYJ3-O(0}Y0X|iLj@2~$M`;!X)YMqjMk;4cR9+FL?@6^J zOA$qB>RR$ORZDX7*M;T9q|K~7&TRARl09uRbK#k79+TrGHp>}ordZ>?aBs+)d@iHk VCeb8s5z1BR%zv^Zd}e~@F93(0kd*)c literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeeConfig.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeeConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..787a77165757788823c0206ffb22128929f2c9e8 GIT binary patch literal 1229 zcma)5O>fgc5Ph4Zac!J_1W5S^&_Y_`q|`ltgp|@lD^*0Y;1HBUE@_;@-f1 z-MtiU$dEsgo(xYJvQ?|CV;#B%!^Ab14Aq{ndt&l#7GI1Ms7!1Om`Tj_7FCE{pItpJl zaS!(y3RlHCazZI8DJf>J_Bpg z>d-s3GyM^3@!BKL zL%Lni_Y9pz?HgFF{uSD1vNCu?`v$VKo5-O6ovaF3r38(A9FRqEF)KBt#(2j0lHydP zQTqa|emF(p6Ilsoo-iP!;@VA6b6k&6jlUbP=%y;CA*Eea`Y&sFit8U2Sa()nt;MV~ z&i`ZGS!6A*z*>)4Y3S)xwZ5QJsj9~cOmTNnfW0{}^(z5%Bmx}9jXj|fkK&vC8+CL4 AY5)KL literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$1.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$1.class new file mode 100644 index 0000000000000000000000000000000000000000..1c40ab6d5b6b750f346128751ba3cab02fd82992 GIT binary patch literal 696 zcma)3%TB^T6g>l#Lahp@_?h&GqVS45<23-Q*BHCCx4)fk%x2X z_(9~!-j#^^fooz*4OH_3?lPE{Qi|}*=TRggY}1nSV3-Fh7S*C@_Zj-8QKsr5-A*W1 z6{E5H4%Ya9{+ePcbmj;H++c~$$`FNRtWZYyRE-g073N6mh@xWT8*gBrm>DY>Df$-5=#L^bGZqfww!27X6jW#Cx)h~geW`GOJZnx7cV{D2aA zk5KS4uF5>YRvtM^KpnF%Te;%x+h@6A$l`7bOAHIp$a|6eNuFR@F4=&BSp`c{PARx| zA_I4=q)ID`J|&2&9f&I(h>>*WJ7%*Val^lC$s~xwcG;m$x%csatO2ayA;}P}tw&gI K`LR|hr2YZ4@Pm5* literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$3.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$3.class new file mode 100644 index 0000000000000000000000000000000000000000..73ed849eb9c4c44e8a45b1d18260a0c76b53c4fb GIT binary patch literal 2430 zcmb_eTUT2}6#gaz4y03{DGe2C8`@YyxCBe7S}0l&v=}auh=SLXlOZ{9I47P3n)w1@(1{zwC*`4Tw;RaT4p7)=dx$^{`R-`On(3Km)`)~#G^KZ(4-*D z(To;`R6(Q*!r3T!`Shl43e8s6+;rYEi$bI)eX(eGMcqt{FtoTO-AN47uFPq-jY)g0 z9Bqg&L@KJ0aD-hEHbXSCu2$5vp_;{X*0puB=<9NhvuI~%^L2Go6AWxtp3=#2POd0& z#k95}IFBv`7dT?*X6QURZXv%eG?)Ax*G=7>U}#GAEw|!1QVJkVS$1J?(YEeyNANr@ zDR_b7GOjROZHP;=Y?021&9b4o3CBr{3=;*p+ zt9;bvT|aJej7m@1Hz~wwQ5+h11+OZ2jpH`P7_K*Dt1gg~dYUc#WSKwFqvIa@J3~vt z@eG&Zb_Wv*UgvlNcTeF@u#F*PZBQMp%H*5kcoWl9VMXC)tYT3XFbrkXeCQn~vdDy+dRGuyXJ-cMNtI;tZ>)iLi!Ef*Xf%~+ahEUUII z$JNQOyv-Q~8ea|#ZlJ^nRX|%-FPf_B*<`56_DssEiZpo?Q+dhJEM?URkdBp0HLDq7 zwH|Jl54;?$J$bwyd;QD=**}|gRc(WO@zE-{U-Lo)>7w8ZQmpZuax80kwkBqD$;!Fo zPvDT$B&^e>W>^lfIVaqbMHD~4L(0>VY8K?4ScBwQmGtm?rYYBG^89PQ!O+`taTe?-4r*u&(nm|1y>x1;y= z@D8mkkcci(vxnRsK6rv_f7AOU(EIE_MU(Ktknmyz0eDD{_qUkFBe?hukMX?^Hy!|} z!o7?wl<_%%oW>$-I0V_Hy_{x(HII4~ni)j+86C_N(g-QoqVF?0${*7|CpixPffWVY e3hrE?l`m;i2wzc@rCYuDhMWQMNw+RZ`uqi$#g4K7 literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$McCommand$1.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/bungee/BungeePlugin$McCommand$1.class new file mode 100644 index 0000000000000000000000000000000000000000..b536e7083541e40a87519b22be1d9391ff62b74e GIT binary patch literal 2659 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 literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/core/HeartbeatFile$Previous.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/core/HeartbeatFile$Previous.class new file mode 100644 index 0000000000000000000000000000000000000000..0cf1cbd1663982bedb53c4267351ce61d0faeed0 GIT binary patch literal 701 zcmb7BO>fgc5PcKdaT1)g384^@0s$miA21+pAcUxdXca!Bq8xHtI}35^+Q@b){tEm7 zE?kg^kT~!I_)&D%CC zc%-9JP=6SwarsDK?d(pQXrSq$<>4GW!JWAd=DIjuEW_X=PIZ*4H)RlIxelIbm6xGb z<`n#tUN+Z)jR_53EKfq6zgA(wOmC7!Dw(Q0HurVXE*CLv?@j)<)d%dA^AroyD`%ex z+J~izj-RV{Ywgxyw#*}KCZzjc6W=zA6nOh-s`EWgQs~0PSa9nf`{4X%OT%4mrN;lV z&^3E!)HO@1kgFn^R|kCvE-*^SbclWb8^j;3;QFH#+WvTj%`X*ui|1Cw8NkM6G%-X6 zBWi8qBHw&wS&vH?5KUx!rHYKR@#~v!qLZ+GxOUzrRxJN3+z;pw+8^QCpI}>`S!2-^ ls8^hE<#eX{$7#e@2esZ~p~W^YHC(|}Vv8jWT*Gw?eglDIh(G`U literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/core/HeartbeatFile.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/core/HeartbeatFile.class new file mode 100644 index 0000000000000000000000000000000000000000..930e4f84cdc1d2efe2a4ad2c4b263957872bcfad GIT binary patch literal 3666 zcmb7GTYD4N6oOZQV=Sh7@EsZbF{=(5F5&f2Ci)(sgDuvV8ga(Fe_F&e?nI+uHk_ zzyIg`4*@)mk0NM5Kt@nSBSI3kUDf8bL|!vz5@VUGde)H;I%$}Ob6P^6BR(01g0PH; ziY-tj9LVX3oL-unEoTx1!_>33cEw3#En80v>6-0ibj=ww^16iJ75CMgb`2O-LM*2u zXvS6<+f-~vRDxQ+Ny|pbQ>Nu<89OB$zE67(jobRXVU$go)~I}?trYlR4QW1oO)ld!#GZ7lIgp}$|n zgZL!l&0AVdLhGtf9ZX8dK8#0X98l3A^1j12SSKZ+F`Ktax{S_xfw6%XvwG1nEK|Wj z8glHiuHcX>t!t)m;;@P%cvQlc8QrOdcU{6GRbSldG9JU6NyWVw>X#y<#Zei@R6K#> zYZ&3_Fp>HTWi4Nl&{B`+@g)_XR*;k++hx}a8a{My7zyLqkiZkGKM9zt?HdE7c#m% zXxRnLsU=x3{%I9yj1scskkopvmu(@9M7T4iVq9>m@tSQox}ftp6%#lwp-JRkvvsrD z0NXmMG1PN^QpE*9{4GZ5jA66gEc?0unNl$=)z9C^`Rg>~HWOUAabP?Yh40JPbItHtZHtUT#2 z6sI@FLFiUED$1B=woCPG@&6OB8gm5wcu_dQ-fHO>*~F-Vw5aB1RD4#1FT0d*W)-}` zzCGj=iefh}<8vGo{s0gYZ>;Rpjhxr8Pr(-@967^DJ5&fbNjGDTdV7{du`gM|l4Y72m|S z$ScPh*PK}iyZpkMlomrjA=XnezT*d3+vvB7*T>vR7RLAReHm}4_yOLO(C#LqCO6?| zW=^wntMVL6=LW{pz5N512QCgzoF6{R3Gkp7!v9bs3l{Jr89!F>6a3U4sx<|XdQP=q z!J22HrmBgmZM$)-v*E5?TDQ3Br$1lUV$ejtm7|HRX;#vkI_I^FI3}ZME34%vHQNy1 z)y1GQYtXNF`aVy;6i-r~RyqF=l<+u3yOMAzJ7 z4hdp1yTc~uY(HZt3Bo9TqIsezyib9@sJI}x|9Eon9RgbrZG8TWUxSnoa*@s@NS$4G zAm4Ij-{P|w0X{nr9Z2z}x(yo{P>@;+Oc9hbi5ljS%;4cHV|uLDK@7 zSFmF$5DYJ&<=qvuPA%aR=@smr5&}!uJKA-yf_DBMT14cmcz54j#MaR|t!P(Qw7Y^t z1&=LYyZAccf62i`1OunSeO=L1^i%~;E~Bq+5sk@4p{Usz=*aU8-?y0sT4@g%=2 zyo8rA#GSXeI>?J3DA9rhC5~|aF<1Uk9HZ7zB>6sx6V!YHJvff1xl%xiD5r?iL!{Gq z6}@hfPBZ*C?|Z+-BGc8w6#WLjbrW~9nz+3%@jLvU2`gY9?YzTX3w!VfmMDNf(vzcz zwA^Evcnwr!w934tNizQQFNFGJ#JB7Y^ho#Ua?snBf?{__)ZrH1Bbcb|FzYNT*woo} z2P2)qK-&_|t}pK(g`OfohU&`OR4Z>2{!EV~+$Ic7Fe(XmY6g0$29yoh;xZ3jRb*u#q>osa2>trlBjz*7##rOukcc%_@azH( zi1)<>w5=dMwSr4z>GRP~Rq$!Co~vMH33GR=>`jJZq3DU-%P=b_UEu%jJ*)py`Mmmc1%io#!2kdN literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/core/Json.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/core/Json.class new file mode 100644 index 0000000000000000000000000000000000000000..69129e4effea9a931fb016fa37d0246cc7ef72ec GIT binary patch literal 2878 zcmaJ@U2_vv7=BKgvYWUqA($f2YE4DkEHo&H(v(st0g5eE%7>K*Y&P3=Ax*+&gQ)lk z>Wo+Fon9e2(;1vm{FvC$kqf=hQD>a-$_uXe0bV)d=z#j1-Gl~P$n@=b&)IXH_v?Aj z?vMX`bs4}$yb(qKO$vefN`#DxxEu>A?vtaV;(j^0D!S#ERgsY6q>8n4B2!tje&gvddXZGHUc-ZUNMPB# z|HB2>nz9@L<&=@HSYwj{ZS=RnvDywVwv8Gd#wNl}7M&S`v29)?gu@Mo{h}=r72y;- zs$nzw1XQ@$qfD(qRl^kAlZEj9|1*)rAIlfcZ41WhFK==5&Z88n;eXtb%FP z)7Wr))IY><#_+g;Cp7HC_#$R%`$)z~Te6tF^OQVuK*K>CVhX05Vx=SyySetYSY@&% zG(3gFtW~98l})2$3Ebc4=&jQto8oB=M{raiRIX&pUXOY_yNPHwjP=6MkcCNOCBrFO zWT4!=um;0D!wTrgs(C)|s}x*&#!8~0R#|f6gEY6hdJb`c1p$$c-b4taGKv#UmDwmBZ>o&$%hM{7P z_)AAf-@Tr1`;Dx$7#%5^M*g7T*m7Q547$^HS)g;|_QX%IjwdTcmoNd7mzo%&*py)k z&qo(#kPosq+QhqN{VLRMP$IUByM}pU)F{<_(Ola+VY$B33?y63Ju9GDj#G3-{m2PQ zDhVcIVgJ<$%jaxVwcpIwZu9VXvErE4PA~5v*~h)Is=ToJ`APs~mF#|Mt7M09RJg{M z1Oy74ZQv+4HYdJ>_>@WjMgEuX6W|U6p`k=g^Ecrn9L_>0hFLJ-Vuj_CvEU_2@mmHlauF^|diQ+U09$J-Wu%QhM|OUwcrG_W0TZ9g)rpSgl9( z;5P_NGSstL=CDn=?H5(n z99Yv(P!%tFrb2jWvGeK0&LffVTTuSIw>>gcP#ol+(x;9wF z{y983i=dcKXREirmqbdk^y@wRP9K9b7$g?aJWts%O!F&vl{REjTF|uQ8dfM68&t@i zu)J%1H^dc>2JsHHF|HwPdLle1^Gc4s1sepYDCvWsDbERXuXbBpXePzy@umH+j9zXBJ>xQ sNW!Z)cjF3M$6(}dS#)v^)1M<1&3j{7UT31=I#YQp#Ff{$9>DAW0S+^B+yDRo literal 0 HcmV?d00001 diff --git a/minecraft-plugin/target/classes/de/deskhub/minecraft/core/OutageTracker$1.class b/minecraft-plugin/target/classes/de/deskhub/minecraft/core/OutageTracker$1.class new file mode 100644 index 0000000000000000000000000000000000000000..a27f74a12024f2b4fc1b62eb49543001e52a7a0b GIT binary patch literal 239 zcma)1OA5k36w9lhY85P=z=a@g90YG53NEAzy})$(?8iEVnbyO(@Bki4oVs&oAju{n z=sPcC~kAYk3B>(h7Z>Z#m+` z`nBet6IaimZF?T~qx