-
Notifications
You must be signed in to change notification settings - Fork 0
Fix Discord webhook implementation: move to per-response design, resolve editorconfig violations #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mc1.21.1
Are you sure you want to change the base?
Changes from all commits
dfbca11
9b6a8c4
14a6808
5f158e4
4bc1a49
7d7a5d4
771ca75
43e711c
58f8d1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,7 @@ | |
| */ | ||
| public class Response implements StringSupplier { | ||
|
|
||
| public static final int VERSION = 2; | ||
| public static final int VERSION = 3; | ||
| public final int version = VERSION; | ||
|
|
||
| /** | ||
|
|
@@ -40,6 +40,11 @@ public class Response implements StringSupplier { | |
| */ | ||
| public transient @Nullable String sendingString; | ||
|
|
||
| /** | ||
| * The original message that triggered this response. | ||
| */ | ||
| public transient @Nullable net.minecraft.network.chat.Component triggerMessage; | ||
|
|
||
| // Options | ||
|
|
||
| /** | ||
|
|
@@ -66,6 +71,12 @@ public class Response implements StringSupplier { | |
| public int cooldownTicks; | ||
| public static final int cooldownTicksDefault = 0; | ||
|
|
||
| /** | ||
| * The Discord webhook URL (only used when type is DISCORD). | ||
| */ | ||
| public String webhookUrl; | ||
| public static final String webhookUrlDefault = ""; | ||
|
|
||
| /** | ||
| * Controls how {@link Response#string} is processed. | ||
| */ | ||
|
|
@@ -83,7 +94,11 @@ public enum Type { | |
| /** | ||
| * Convert into a pair of keys for use by the CommandKeys mod. | ||
| */ | ||
| COMMANDKEYS("K"); | ||
| COMMANDKEYS("K"), | ||
| /** | ||
| * Send as a Discord webhook message. | ||
| */ | ||
| DISCORD("🪝"); | ||
|
|
||
| public final String icon; | ||
|
|
||
|
|
@@ -100,17 +115,26 @@ public Response() { | |
| string = stringDefault; | ||
| delayTicks = delayTicksDefault; | ||
| type = Type.values()[0]; | ||
| webhookUrl = webhookUrlDefault; | ||
| } | ||
|
|
||
| /** | ||
| * Not validated. | ||
| */ | ||
| Response(boolean enabled, String string, Type type, int delayTicks, int cooldownTicks) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Response(boolean enabled, String string, Type type, int delayTicks, int cooldownTicks, String webhookUrl) { |
||
| Response( | ||
| boolean enabled, | ||
| String string, | ||
| Type type, | ||
| int delayTicks, | ||
| int cooldownTicks, | ||
| String webhookUrl | ||
| ) { | ||
| this.enabled = enabled; | ||
| this.string = string; | ||
| this.type = type; | ||
| this.delayTicks = delayTicks; | ||
| this.cooldownTicks = cooldownTicks; | ||
| this.webhookUrl = webhookUrl; | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -126,6 +150,8 @@ public Response() { | |
| Response validate() { | ||
| if (delayTicks < 0) | ||
| delayTicks = delayTicksDefault; | ||
| if (webhookUrl == null) | ||
| webhookUrl = webhookUrlDefault; | ||
| return this; | ||
| } | ||
|
|
||
|
|
@@ -179,7 +205,21 @@ public Response deserialize( | |
| silent | ||
| ); | ||
|
|
||
| return new Response(enabled, string, type, delayTicks, cooldownTicks).validate(); | ||
| String webhookUrl = JsonUtil.getOrDefault( | ||
| obj, | ||
| "webhookUrl", | ||
| webhookUrlDefault, | ||
| silent | ||
| ); | ||
|
|
||
| return new Response( | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return new Response(enabled, string, type, delayTicks, cooldownTicks, webhookUrl).validate(); |
||
| enabled, | ||
| string, | ||
| type, | ||
| delayTicks, | ||
| cooldownTicks, | ||
| webhookUrl | ||
| ).validate(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,5 +143,6 @@ private static class Controls2 extends Entry { | |
| )); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| /* | ||
| * Copyright 2026 TerminalMC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package dev.terminalmc.chatnotify.util; | ||
|
|
||
| import com.google.gson.JsonObject; | ||
| import dev.terminalmc.chatnotify.ChatNotify; | ||
| import net.minecraft.client.Minecraft; | ||
| import net.minecraft.network.chat.Component; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| import java.net.URI; | ||
| import java.net.http.HttpClient; | ||
| import java.net.http.HttpRequest; | ||
| import java.net.http.HttpResponse; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.time.Duration; | ||
|
|
||
| /** | ||
| * Handles sending messages to Discord webhooks. | ||
| */ | ||
| public class DiscordWebhookHandler { | ||
|
|
||
| private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() | ||
| .connectTimeout(Duration.ofSeconds(10)) | ||
| .build(); | ||
|
|
||
| private DiscordWebhookHandler() { | ||
| } | ||
|
|
||
| /** | ||
| * Sends a message to a Discord webhook asynchronously. | ||
| * | ||
| * @param webhookUrl the Discord webhook URL | ||
| * @param content the message content to send | ||
| * @param triggerMessage the original message that triggered the notification | ||
| */ | ||
| public static void sendAsync( | ||
| String webhookUrl, | ||
| String content, | ||
| @Nullable Component triggerMessage | ||
| ) { | ||
| if (webhookUrl == null || webhookUrl.isBlank()) { | ||
| ChatNotify.LOG.warn("Discord webhook URL is empty, skipping webhook send"); | ||
| return; | ||
| } | ||
|
|
||
| if (content == null || content.isBlank()) { | ||
| ChatNotify.LOG.warn("Discord webhook content is empty, skipping webhook send"); | ||
| return; | ||
| } | ||
|
|
||
| // Validate webhook URL format | ||
| if (!isValidWebhookUrl(webhookUrl)) { | ||
| ChatNotify.LOG.error("Invalid Discord webhook URL format: {}", webhookUrl); | ||
| return; | ||
| } | ||
|
|
||
| // Build JSON payload with embed | ||
| JsonObject embed = new JsonObject(); | ||
| embed.addProperty("title", "Chat triggered"); | ||
|
|
||
| // Use the trigger message content if available, otherwise use the response content | ||
| String description = triggerMessage != null | ||
| ? triggerMessage.getString() | ||
| : content; | ||
| embed.addProperty("description", description); | ||
| embed.addProperty("color", 3303592); // #3268a8 | ||
|
|
||
| JsonObject footer = new JsonObject(); | ||
| // Get server/world name | ||
| Minecraft mc = Minecraft.getInstance(); | ||
| String serverName = "Minecraft"; | ||
| if (mc.level != null) { | ||
| if (mc.getCurrentServer() != null) { | ||
| // Multiplayer - use server name | ||
| serverName = mc.getCurrentServer().name; | ||
| } else if (mc.getSingleplayerServer() != null) { | ||
| // Singleplayer - use world name | ||
| serverName = mc.getSingleplayerServer() | ||
| .getWorldData() | ||
| .getLevelSettings() | ||
| .levelName(); | ||
| } | ||
| } | ||
| footer.addProperty("text", serverName); | ||
| embed.add("footer", footer); | ||
|
|
||
| JsonObject payload = new JsonObject(); | ||
| com.google.gson.JsonArray embeds = new com.google.gson.JsonArray(); | ||
| embeds.add(embed); | ||
| payload.add("embeds", embeds); | ||
|
|
||
| String jsonPayload = payload.toString(); | ||
|
|
||
| HttpRequest request = HttpRequest.newBuilder() | ||
| .uri(URI.create(webhookUrl)) | ||
| .timeout(Duration.ofSeconds(10)) | ||
| .header("Content-Type", "application/json") | ||
| .POST(HttpRequest.BodyPublishers.ofString(jsonPayload, StandardCharsets.UTF_8)) | ||
| .build(); | ||
|
|
||
| // Send asynchronously | ||
| HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) | ||
| .thenApply(HttpResponse::statusCode) | ||
| .thenAccept(statusCode -> { | ||
| if (statusCode >= 200 && statusCode < 300) { | ||
| ChatNotify.LOG.info("Successfully sent Discord webhook message"); | ||
| } else if (statusCode == 429) { | ||
| ChatNotify.LOG.warn("Discord webhook rate limited (HTTP {})", statusCode); | ||
| } else { | ||
| ChatNotify.LOG.error( | ||
| "Failed to send Discord webhook message (HTTP {})", | ||
| statusCode | ||
| ); | ||
| } | ||
| }) | ||
| .exceptionally(throwable -> { | ||
| ChatNotify.LOG.error("Error sending Discord webhook message", throwable); | ||
| return null; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Validates that a webhook URL is a valid Discord webhook URL. | ||
| * | ||
| * @param webhookUrl the URL to validate | ||
| * @return true if the URL is a valid Discord webhook URL | ||
| */ | ||
| private static boolean isValidWebhookUrl(String webhookUrl) { | ||
| try { | ||
| URI uri = URI.create(webhookUrl); | ||
| String scheme = uri.getScheme(); | ||
| String host = uri.getHost(); | ||
| String path = uri.getPath(); | ||
|
|
||
| // Discord webhook URLs must use HTTPS for secure transmission | ||
| // Format: https://discord.com/api/webhooks/... | ||
| // or: https://discordapp.com/api/webhooks/... | ||
| return "https".equals(scheme) | ||
| && (host != null | ||
| && (host.equals("discord.com") | ||
| || host.equals("discordapp.com"))) | ||
| && path != null && path.startsWith("/api/webhooks/"); | ||
| } catch (Exception e) { | ||
| return false; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change to "D" for discord