|
| 1 | +package com.innygr.icreateordernetworking.mythic; |
| 2 | + |
| 3 | +import com.google.gson.JsonObject; |
| 4 | +import com.google.gson.JsonParser; |
| 5 | +import net.neoforged.fml.loading.FMLPaths; |
| 6 | +import org.slf4j.Logger; |
| 7 | +import org.slf4j.LoggerFactory; |
| 8 | + |
| 9 | +import java.io.IOException; |
| 10 | +import java.net.URI; |
| 11 | +import java.net.http.HttpClient; |
| 12 | +import java.net.http.HttpRequest; |
| 13 | +import java.net.http.HttpResponse; |
| 14 | +import java.nio.charset.StandardCharsets; |
| 15 | +import java.nio.file.Files; |
| 16 | +import java.nio.file.Path; |
| 17 | +import java.time.Duration; |
| 18 | +import java.util.concurrent.ExecutorService; |
| 19 | +import java.util.concurrent.Executors; |
| 20 | + |
| 21 | +/** |
| 22 | + * Server-only Discord notifier for the optional Mythic build. |
| 23 | + * |
| 24 | + * A Discord webhook is used instead of a bot token: it is configured only on |
| 25 | + * the dedicated server and is never sent to clients or embedded in the JAR. |
| 26 | + */ |
| 27 | +public final class DiscordBridge { |
| 28 | + private static final Logger LOGGER = LoggerFactory.getLogger("iCreateOrderNetworking/MythicDiscord"); |
| 29 | + private static final Path CONFIG_PATH = FMLPaths.CONFIGDIR.get().resolve("icreate-order-networking-mythic.json"); |
| 30 | + private static final HttpClient HTTP = HttpClient.newBuilder() |
| 31 | + .connectTimeout(Duration.ofSeconds(5)) |
| 32 | + .build(); |
| 33 | + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(runnable -> { |
| 34 | + Thread thread = new Thread(runnable, "icreate-mythic-discord"); |
| 35 | + thread.setDaemon(true); |
| 36 | + return thread; |
| 37 | + }); |
| 38 | + |
| 39 | + private static volatile String webhookUrl = ""; |
| 40 | + private static volatile String serverName = "Minecraft server"; |
| 41 | + private static volatile boolean enabled; |
| 42 | + private static volatile boolean stopped; |
| 43 | + |
| 44 | + private DiscordBridge() {} |
| 45 | + |
| 46 | + static void configure(boolean isEnabled, String webhook, String configuredServerName) { |
| 47 | + enabled = isEnabled; |
| 48 | + webhookUrl = webhook == null ? "" : webhook.strip(); |
| 49 | + serverName = configuredServerName == null || configuredServerName.isBlank() |
| 50 | + ? "Minecraft server" : configuredServerName.strip(); |
| 51 | + stopped = false; |
| 52 | + } |
| 53 | + |
| 54 | + public static boolean isConfigured() { |
| 55 | + return enabled && isHttpsWebhook(webhookUrl); |
| 56 | + } |
| 57 | + |
| 58 | + public static void sendEvent(String eventType, String details) { |
| 59 | + if (stopped || !isConfigured()) return; |
| 60 | + String safeType = sanitize(eventType, 80); |
| 61 | + String safeDetails = sanitize(details, 1500); |
| 62 | + JsonObject payload = new JsonObject(); |
| 63 | + payload.addProperty("content", "**[" + serverName + "] " + safeType + "**\n" + safeDetails); |
| 64 | + JsonObject allowedMentions = new JsonObject(); |
| 65 | + allowedMentions.add("parse", new com.google.gson.JsonArray()); |
| 66 | + payload.add("allowed_mentions", allowedMentions); |
| 67 | + |
| 68 | + HttpRequest request; |
| 69 | + try { |
| 70 | + request = HttpRequest.newBuilder(URI.create(webhookUrl)) |
| 71 | + .timeout(Duration.ofSeconds(8)) |
| 72 | + .header("Content-Type", "application/json; charset=utf-8") |
| 73 | + .POST(HttpRequest.BodyPublishers.ofString(payload.toString(), StandardCharsets.UTF_8)) |
| 74 | + .build(); |
| 75 | + } catch (IllegalArgumentException exception) { |
| 76 | + LOGGER.warn("Mythic Discord webhook URL is invalid; event was not sent."); |
| 77 | + return; |
| 78 | + } |
| 79 | + |
| 80 | + EXECUTOR.submit(() -> { |
| 81 | + try { |
| 82 | + HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); |
| 83 | + if (response.statusCode() < 200 || response.statusCode() >= 300) { |
| 84 | + LOGGER.warn("Mythic Discord webhook returned HTTP {}; event was not delivered.", response.statusCode()); |
| 85 | + } |
| 86 | + } catch (InterruptedException exception) { |
| 87 | + Thread.currentThread().interrupt(); |
| 88 | + } catch (IOException | RuntimeException exception) { |
| 89 | + LOGGER.warn("Unable to deliver Mythic Discord event: {}", exception.getMessage()); |
| 90 | + } |
| 91 | + }); |
| 92 | + } |
| 93 | + |
| 94 | + static void shutdown() { |
| 95 | + stopped = true; |
| 96 | + } |
| 97 | + |
| 98 | + private static boolean isHttpsWebhook(String value) { |
| 99 | + if (value == null || value.isBlank()) return false; |
| 100 | + try { |
| 101 | + URI uri = URI.create(value); |
| 102 | + return "https".equalsIgnoreCase(uri.getScheme()) |
| 103 | + && uri.getHost() != null |
| 104 | + && uri.getHost().equalsIgnoreCase("discord.com") |
| 105 | + && uri.getPath() != null |
| 106 | + && uri.getPath().startsWith("/api/webhooks/"); |
| 107 | + } catch (IllegalArgumentException exception) { |
| 108 | + return false; |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + private static String sanitize(String value, int maxLength) { |
| 113 | + if (value == null) return "n/a"; |
| 114 | + String normalized = value.replace("@everyone", "@\u200beveryone") |
| 115 | + .replace("@here", "@\u200bhere") |
| 116 | + .replace("\r", " ") |
| 117 | + .strip(); |
| 118 | + return normalized.length() > maxLength ? normalized.substring(0, maxLength) : normalized; |
| 119 | + } |
| 120 | +} |
0 commit comments