From dfbca11fa8e9e072f7f9e5f38bf2bbfb04cf7446 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:35:12 +0000 Subject: [PATCH 1/6] Initial plan From 9b6a8c4654524b3bad3819a45dfee622811d88c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:58:01 +0000 Subject: [PATCH 2/6] Add Discord webhook support to notification responses Co-authored-by: kln581 <79673692+kln581@users.noreply.github.com> --- .../chatnotify/config/Response.java | 27 +++- .../util/DiscordWebhookHandler.java | 115 ++++++++++++++++++ .../chatnotify/util/ResponseUtil.java | 2 + 3 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java diff --git a/common/src/main/java/dev/terminalmc/chatnotify/config/Response.java b/common/src/main/java/dev/terminalmc/chatnotify/config/Response.java index 9e29cd30..12750bf9 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/config/Response.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/config/Response.java @@ -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; /** @@ -66,6 +66,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 +89,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 +110,19 @@ 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) { + 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 @@ -179,7 +191,14 @@ public Response deserialize( silent ); - return new Response(enabled, string, type, delayTicks, cooldownTicks).validate(); + String webhookUrl = JsonUtil.getOrDefault( + obj, + "webhookUrl", + webhookUrlDefault, + silent + ); + + return new Response(enabled, string, type, delayTicks, cooldownTicks, webhookUrl).validate(); } } } diff --git a/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java b/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java new file mode 100644 index 00000000..f1be813d --- /dev/null +++ b/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java @@ -0,0 +1,115 @@ +/* + * 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 java.io.IOException; +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 + */ + public static void sendAsync(String webhookUrl, String content) { + 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 + JsonObject payload = new JsonObject(); + payload.addProperty("content", content); + + 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 host = uri.getHost(); + String path = uri.getPath(); + + // Discord webhook URLs should be https://discord.com/api/webhooks/... or https://discordapp.com/api/webhooks/... + return (host != null && (host.equals("discord.com") || host.equals("discordapp.com"))) && + path != null && path.startsWith("/api/webhooks/"); + } catch (Exception e) { + return false; + } + } +} diff --git a/common/src/main/java/dev/terminalmc/chatnotify/util/ResponseUtil.java b/common/src/main/java/dev/terminalmc/chatnotify/util/ResponseUtil.java index 8e7be3f4..d800576c 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/util/ResponseUtil.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/util/ResponseUtil.java @@ -60,6 +60,8 @@ public static void tickResponses(Minecraft mc) { if (res.sendingString != null && !res.sendingString.isBlank()) { if (res.type.equals(Response.Type.COMMANDKEYS)) { CommandKeysWrapper.trySend(res.sendingString); + } else if (res.type.equals(Response.Type.DISCORD)) { + DiscordWebhookHandler.sendAsync(res.webhookUrl, res.sendingString); } else { sending.add(res.sendingString); } From 14a6808f65b844bba49decfcca04b955707b1539 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:59:38 +0000 Subject: [PATCH 3/6] Add GUI support for Discord webhook URL configuration Co-authored-by: kln581 <79673692+kln581@users.noreply.github.com> --- .../gui/widget/list/FilterList.java | 23 +++++++++++++++++++ .../assets/chatnotify/lang/en_us.json | 3 +++ 2 files changed, 26 insertions(+) diff --git a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java index 8a733ff0..5c2e39bf 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java @@ -670,6 +670,29 @@ public ResponseOptions( ? message.string.split("-")[1] : ""); elements.add(keyField2); + } else if (message.type.equals(Response.Type.DISCORD)) { + // Discord webhook - show message field and webhook URL field + int discordMsgFieldWidth = msgFieldWidth / 2 - fieldSpacing / 2; + int webhookFieldWidth = msgFieldWidth / 2 - fieldSpacing / 2; + + // Message field + MultiLineTextField msgField = + new MultiLineTextField(movingX, 0, discordMsgFieldWidth, height * 2); + msgField.setCharacterLimit(256); + msgField.setValue(message.string); + msgField.setValueListener((val) -> message.string = val.strip()); + msgField.setHint(localized("option", "notif.response.discord.message").copy()); + elements.add(msgField); + movingX += discordMsgFieldWidth + fieldSpacing; + + // Webhook URL field + MultiLineTextField webhookField = + new MultiLineTextField(movingX, 0, webhookFieldWidth, height * 2); + webhookField.setCharacterLimit(256); + webhookField.setValue(message.webhookUrl); + webhookField.setValueListener((val) -> message.webhookUrl = val.strip()); + webhookField.setHint(localized("option", "notif.response.discord.webhook_url").copy()); + elements.add(webhookField); } else { // Response field MultiLineTextField msgField = diff --git a/common/src/main/resources/assets/chatnotify/lang/en_us.json b/common/src/main/resources/assets/chatnotify/lang/en_us.json index 1ce5b19f..2d840097 100644 --- a/common/src/main/resources/assets/chatnotify/lang/en_us.json +++ b/common/src/main/resources/assets/chatnotify/lang/en_us.json @@ -172,8 +172,11 @@ "option.chatnotify.notif.response.list.tooltip.warning": "Warning: Can crash the game, use with caution.", "option.chatnotify.notif.response.time.tooltip": "Time in ticks to wait (after the previous message, if any) before sending.", "option.chatnotify.notif.response.type.COMMANDKEYS.tooltip": "CommandKeys response.\nIf the CommandKeys mod is loaded (version 2.3.1 or later), this response will trigger all macros with keybinds matching the key IDs you set.", + "option.chatnotify.notif.response.type.DISCORD.tooltip": "Discord webhook response.\nSends a message to the specified Discord webhook URL when this notification is activated.", "option.chatnotify.notif.response.type.NORMAL.tooltip": "Normal response.", "option.chatnotify.notif.response.type.REGEX.tooltip": "Regex response.\nUse (1), (2) etc in the message to access regex capturing groups from the trigger.", + "option.chatnotify.notif.response.discord.message": "Message", + "option.chatnotify.notif.response.discord.webhook_url": "Webhook URL", "option.chatnotify.notif.sound": "Sound", "option.chatnotify.notif.sound.field.tooltip": "Notification Sound", "option.chatnotify.notif.sound.open.minecraft_volume.tooltip": "Open Minecraft's volume settings", From 5f158e4fbbee240d614dd9cbe28e3c1e19eeb8cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:00:34 +0000 Subject: [PATCH 4/6] Address code review feedback: add HTTPS validation and null safety Co-authored-by: kln581 <79673692+kln581@users.noreply.github.com> --- .../terminalmc/chatnotify/gui/widget/list/FilterList.java | 2 +- .../terminalmc/chatnotify/util/DiscordWebhookHandler.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java index 5c2e39bf..4a08bb52 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java @@ -689,7 +689,7 @@ public ResponseOptions( MultiLineTextField webhookField = new MultiLineTextField(movingX, 0, webhookFieldWidth, height * 2); webhookField.setCharacterLimit(256); - webhookField.setValue(message.webhookUrl); + webhookField.setValue(message.webhookUrl != null ? message.webhookUrl : ""); webhookField.setValueListener((val) -> message.webhookUrl = val.strip()); webhookField.setHint(localized("option", "notif.response.discord.webhook_url").copy()); elements.add(webhookField); diff --git a/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java b/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java index f1be813d..499657e5 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java @@ -102,11 +102,14 @@ public static void sendAsync(String webhookUrl, String content) { 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 should be https://discord.com/api/webhooks/... or https://discordapp.com/api/webhooks/... - return (host != null && (host.equals("discord.com") || host.equals("discordapp.com"))) && + // Discord webhook URLs must use HTTPS for secure transmission + // and should be 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; From 4bc1a4957fa5075b70c738a062bd7d54c9a749b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:03:04 +0000 Subject: [PATCH 5/6] Update documentation for Discord webhook feature Co-authored-by: kln581 <79673692+kln581@users.noreply.github.com> --- README.md | 16 +++++++++++++--- changelog.md | 4 ++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5f3876e0..bc4989c9 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Plays a ping sound when your name is mentioned, with options to create custom al - Adjust sound volume and pitch per-notification. - Customize message highlighting with a color picker and format controls. - Use regex patterns, inclusion and exclusion triggers for fine-grained control. -- Add automatic response messages or trigger [CommandKeys](https://modrinth.com/project/commandkeys) - macros. +- Add automatic response messages, trigger [CommandKeys](https://modrinth.com/project/commandkeys) + macros, or send notifications to Discord webhooks. @@ -133,7 +133,17 @@ match the message. #### Response -Response messages will be sent in chat when the notification is activated. +Response messages can be sent when the notification is activated. ChatNotify supports multiple response types: + +- **Normal**: Sends a message directly in chat +- **Regex**: Uses regex capture groups from the trigger in the response message +- **CommandKeys**: Triggers [CommandKeys](https://modrinth.com/project/commandkeys) macros +- **Discord**: Sends a message to a Discord webhook (useful for monitoring chat events remotely) + +For Discord webhooks, you'll need to: +1. Create a webhook in your Discord server (Server Settings > Integrations > Webhooks) +2. Copy the webhook URL +3. Select "Discord" as the response type and paste the webhook URL Use with caution, as you can easily make a notification send a response which triggers the notification again in a loop, which will spam chat and then crash the game. diff --git a/changelog.md b/changelog.md index 4e6d546a..e2718978 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Added Discord webhook support for notification responses + ## 2.6.6 - Fixed format code translation not resetting format on color change From 7d7a5d4dd73aa300f9bf7619529dabeab15d785f Mon Sep 17 00:00:00 2001 From: kln581 Date: Mon, 2 Feb 2026 23:05:53 -0800 Subject: [PATCH 6/6] Move Discord webhook settings to Detection tab --- README.md | 3 +- changelog.md | 2 +- .../terminalmc/chatnotify/config/Config.java | 36 ++++++++++++- .../chatnotify/config/Response.java | 26 +++------- .../gui/widget/list/FilterList.java | 24 +-------- .../gui/widget/list/root/ControlList.java | 1 + .../gui/widget/list/root/DetectionList.java | 50 +++++++++++++++++++ .../util/DiscordWebhookHandler.java | 39 +++++++++++++-- .../chatnotify/util/ResponseUtil.java | 10 +++- .../chatnotify/util/text/MessageUtil.java | 14 +++++- .../assets/chatnotify/lang/en_us.json | 8 +-- .../assets/chatnotify/lang/ru_ru.json | 4 ++ .../assets/chatnotify/lang/zh_cn.json | 4 ++ .../assets/chatnotify/lang/zh_tw.json | 4 ++ gradle.properties | 2 +- 15 files changed, 174 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index bc4989c9..3baf2722 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,8 @@ Response messages can be sent when the notification is activated. ChatNotify sup For Discord webhooks, you'll need to: 1. Create a webhook in your Discord server (Server Settings > Integrations > Webhooks) 2. Copy the webhook URL -3. Select "Discord" as the response type and paste the webhook URL +3. In ChatNotify, open Detection > Sender Detection and enable Discord Webhook +4. Paste the webhook URL into the Webhook URL field Use with caution, as you can easily make a notification send a response which triggers the notification again in a loop, which will spam chat and then crash the game. diff --git a/changelog.md b/changelog.md index e2718978..c27aaad8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 2.6.7 - Added Discord webhook support for notification responses diff --git a/common/src/main/java/dev/terminalmc/chatnotify/config/Config.java b/common/src/main/java/dev/terminalmc/chatnotify/config/Config.java index 50177065..7de39ba1 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/config/Config.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/config/Config.java @@ -61,7 +61,7 @@ */ public class Config { - public static final int VERSION = 9; + public static final int VERSION = 10; public final int version = VERSION; private static final Path CONFIG_DIR = Services.PLATFORM.getConfigDir(); public static final String FILE_NAME = ChatNotify.MOD_ID + ".json"; @@ -199,6 +199,18 @@ public enum SenderDetectionMode { public static final Supplier> prefixesDefault = () -> new ArrayList<>(List.of("/shout", "/me", "!")); + /** + * Whether Discord webhook responses are enabled. + */ + public boolean discordWebhookEnabled; + public static final boolean discordWebhookEnabledDefault = false; + + /** + * The global Discord webhook URL for all notifications. + */ + public String discordWebhookUrl; + public static final String discordWebhookUrlDefault = ""; + // Notifications /** @@ -228,6 +240,8 @@ public Config() { SenderDetectionMode.values()[0], checkOwnMessagesDefault, prefixesDefault.get(), + discordWebhookEnabledDefault, + discordWebhookUrlDefault, notificationsDefault.get() ); } @@ -250,6 +264,8 @@ public Config() { SenderDetectionMode senderDetectionMode, boolean checkOwnMessages, List prefixes, + boolean discordWebhookEnabled, + String discordWebhookUrl, List notifications ) { this.debugMode = debugMode; @@ -266,6 +282,8 @@ public Config() { this.senderDetectionMode = senderDetectionMode; this.checkOwnMessages = checkOwnMessages; this.prefixes = prefixes; + this.discordWebhookEnabled = discordWebhookEnabled; + this.discordWebhookUrl = discordWebhookUrl; this.notifications = notifications; } @@ -634,6 +652,20 @@ public Config deserialize( silent ); + boolean discordWebhookEnabled = JsonUtil.getOrDefault( + obj, + "discordWebhookEnabled", + discordWebhookEnabledDefault, + silent + ); + + String discordWebhookUrl = JsonUtil.getOrDefault( + obj, + "discordWebhookUrl", + discordWebhookUrlDefault, + silent + ); + List notifications = JsonUtil.getOrDefault( ctx, obj, @@ -658,6 +690,8 @@ public Config deserialize( senderDetectionMode, checkOwnMessages, prefixes, + discordWebhookEnabled, + discordWebhookUrl, notifications ).validate(); } diff --git a/common/src/main/java/dev/terminalmc/chatnotify/config/Response.java b/common/src/main/java/dev/terminalmc/chatnotify/config/Response.java index 12750bf9..1f3dc56e 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/config/Response.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/config/Response.java @@ -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,12 +71,6 @@ 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. */ @@ -93,7 +92,7 @@ public enum Type { /** * Send as a Discord webhook message. */ - DISCORD("🪝"); + DISCORD("D"); public final String icon; @@ -110,19 +109,17 @@ 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, String webhookUrl) { + Response(boolean enabled, String string, Type type, int delayTicks, int cooldownTicks) { this.enabled = enabled; this.string = string; this.type = type; this.delayTicks = delayTicks; this.cooldownTicks = cooldownTicks; - this.webhookUrl = webhookUrl; } @Override @@ -191,14 +188,7 @@ public Response deserialize( silent ); - String webhookUrl = JsonUtil.getOrDefault( - obj, - "webhookUrl", - webhookUrlDefault, - silent - ); - - return new Response(enabled, string, type, delayTicks, cooldownTicks, webhookUrl).validate(); + return new Response(enabled, string, type, delayTicks, cooldownTicks).validate(); } } } diff --git a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java index 4a08bb52..e8a515ca 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/FilterList.java @@ -671,28 +671,8 @@ public ResponseOptions( : ""); elements.add(keyField2); } else if (message.type.equals(Response.Type.DISCORD)) { - // Discord webhook - show message field and webhook URL field - int discordMsgFieldWidth = msgFieldWidth / 2 - fieldSpacing / 2; - int webhookFieldWidth = msgFieldWidth / 2 - fieldSpacing / 2; - - // Message field - MultiLineTextField msgField = - new MultiLineTextField(movingX, 0, discordMsgFieldWidth, height * 2); - msgField.setCharacterLimit(256); - msgField.setValue(message.string); - msgField.setValueListener((val) -> message.string = val.strip()); - msgField.setHint(localized("option", "notif.response.discord.message").copy()); - elements.add(msgField); - movingX += discordMsgFieldWidth + fieldSpacing; - - // Webhook URL field - MultiLineTextField webhookField = - new MultiLineTextField(movingX, 0, webhookFieldWidth, height * 2); - webhookField.setCharacterLimit(256); - webhookField.setValue(message.webhookUrl != null ? message.webhookUrl : ""); - webhookField.setValueListener((val) -> message.webhookUrl = val.strip()); - webhookField.setHint(localized("option", "notif.response.discord.webhook_url").copy()); - elements.add(webhookField); + // Discord webhook - no fields needed, just enabled/disabled + // The message that triggered the notification is sent automatically } else { // Response field MultiLineTextField msgField = diff --git a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/root/ControlList.java b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/root/ControlList.java index 8e4189f0..f80dd552 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/root/ControlList.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/root/ControlList.java @@ -143,5 +143,6 @@ private static class Controls2 extends Entry { )); } } + } } diff --git a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/root/DetectionList.java b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/root/DetectionList.java index 0fa8540e..e1eea00a 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/root/DetectionList.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/gui/widget/list/root/DetectionList.java @@ -31,6 +31,8 @@ import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; +import java.time.Duration; + import static dev.terminalmc.chatnotify.util.Localization.localized; public class DetectionList extends OptionList { @@ -75,6 +77,7 @@ protected void addEntries() { addEntry(new Entry.SelfNotify(dynEntryX, dynEntryWidth, entryHeight)); addEntry(new Entry.SenderDetection(dynEntryX, dynEntryWidth, entryHeight)); + addEntry(new Entry.DiscordWebhook(dynEntryX, dynEntryWidth, entryHeight)); addEntry(new OptionList.Entry.Text( entryX, @@ -265,6 +268,53 @@ private static class SenderDetection extends Entry { } } + private static class DiscordWebhook extends Entry { + + DiscordWebhook(int x, int width, int height) { + super(); + + int toggleWidth = Math.min(120, (width - SPACE) / 3); + TextField webhookField = new TextField( + x + toggleWidth + SPACE, + 0, + width - toggleWidth - SPACE, + height + ); + webhookField.setMaxLength(256); + webhookField.setValue(Config.get().discordWebhookUrl); + webhookField.setTooltip(Tooltip.create(localized( + "option", + "detection.discord_webhook.url.tooltip" + ))); + webhookField.setTooltipDelay(Duration.ofMillis(500)); + webhookField.setResponder((val) -> Config.get().discordWebhookUrl = val.strip()); + webhookField.setHint(localized("option", "detection.discord_webhook.url").copy()); + webhookField.active = Config.get().discordWebhookEnabled; + elements.add(webhookField); + + elements.add(CycleButton.booleanBuilder( + CommonComponents.OPTION_ON.copy().withStyle(ChatFormatting.GREEN), + CommonComponents.OPTION_OFF.copy().withStyle(ChatFormatting.RED) + ) + .withInitialValue(Config.get().discordWebhookEnabled) + .withTooltip((status) -> Tooltip.create(localized( + "option", + "detection.discord_webhook.tooltip" + ))) + .create( + x, + 0, + toggleWidth, + height, + localized("option", "detection.discord_webhook"), + (button, status) -> { + Config.get().discordWebhookEnabled = status; + webhookField.active = status; + } + )); + } + } + private static class PrefixFieldEntry extends Entry { PrefixFieldEntry(int x, int width, int height, DetectionList list, int index) { diff --git a/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java b/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java index 499657e5..aae4dc60 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/util/DiscordWebhookHandler.java @@ -18,8 +18,10 @@ 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.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -44,8 +46,9 @@ private DiscordWebhookHandler() { * * @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) { + 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; @@ -62,9 +65,37 @@ public static void sendAsync(String webhookUrl, String content) { return; } - // Build JSON payload + // 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(); - payload.addProperty("content", content); + com.google.gson.JsonArray embeds = new com.google.gson.JsonArray(); + embeds.add(embed); + payload.add("embeds", embeds); String jsonPayload = payload.toString(); diff --git a/common/src/main/java/dev/terminalmc/chatnotify/util/ResponseUtil.java b/common/src/main/java/dev/terminalmc/chatnotify/util/ResponseUtil.java index d800576c..b6323653 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/util/ResponseUtil.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/util/ResponseUtil.java @@ -61,7 +61,15 @@ public static void tickResponses(Minecraft mc) { if (res.type.equals(Response.Type.COMMANDKEYS)) { CommandKeysWrapper.trySend(res.sendingString); } else if (res.type.equals(Response.Type.DISCORD)) { - DiscordWebhookHandler.sendAsync(res.webhookUrl, res.sendingString); + Config config = Config.get(); + String webhookUrl = config.discordWebhookUrl; + if (config.discordWebhookEnabled && !webhookUrl.isBlank()) { + DiscordWebhookHandler.sendAsync( + webhookUrl, + res.sendingString, + res.triggerMessage + ); + } } else { sending.add(res.sendingString); } diff --git a/common/src/main/java/dev/terminalmc/chatnotify/util/text/MessageUtil.java b/common/src/main/java/dev/terminalmc/chatnotify/util/text/MessageUtil.java index 2de8df97..467dac52 100644 --- a/common/src/main/java/dev/terminalmc/chatnotify/util/text/MessageUtil.java +++ b/common/src/main/java/dev/terminalmc/chatnotify/util/text/MessageUtil.java @@ -314,7 +314,7 @@ private static String checkOwner(String cleanStr) { // Send response messages Matcher subsMatcher = trig.type == Trigger.Type.REGEX ? matcher : null; - sendResponses(notif, subsMatcher); + sendResponses(notif, subsMatcher, msg); // Restyle msg = StyleUtil.restyle(msg, cleanStr, trig, matcher, notif.textStyle, restyleAll); @@ -633,10 +633,22 @@ private static void copyClipboardMsg(Notification notif, Component msg, Matcher * @param notif the Notification. */ private static void sendResponses(Notification notif, @Nullable Matcher matcher) { + sendResponses(notif, matcher, null); + } + + /** + * Sends all response messages of the specified notification, if the relevant control is + * enabled. + * + * @param notif the Notification. + * @param triggerMessage the original message that triggered the notification + */ + private static void sendResponses(Notification notif, @Nullable Matcher matcher, @Nullable Component triggerMessage) { if (notif.responseEnabled) { int totalDelay = 0; for (Response msg : notif.responses) { msg.sendingString = msg.string; + msg.triggerMessage = triggerMessage; if (msg.type.equals(Response.Type.REGEX) && matcher != null && matcher.find(0)) { // Capturing group substitution for (int i = 0; i <= matcher.groupCount(); i++) { diff --git a/common/src/main/resources/assets/chatnotify/lang/en_us.json b/common/src/main/resources/assets/chatnotify/lang/en_us.json index 2d840097..48faf915 100644 --- a/common/src/main/resources/assets/chatnotify/lang/en_us.json +++ b/common/src/main/resources/assets/chatnotify/lang/en_us.json @@ -85,6 +85,10 @@ "option.chatnotify.detection.sender.mode.status.SENT_MATCH": "Sent message match", "option.chatnotify.detection.sender.mode.status.SENT_MATCH.tooltip": "Incoming messages will be identified as sent by you if they match a recently-sent message and match a trigger of the first notification.", "option.chatnotify.detection.sender.tooltip": "If your own messages are triggering notifications unexpectedly, try adjusting the options below.", + "option.chatnotify.detection.discord_webhook": "Discord Webhook", + "option.chatnotify.detection.discord_webhook.tooltip": "Enable or disable Discord webhook responses for all notifications.", + "option.chatnotify.detection.discord_webhook.url": "Webhook URL", + "option.chatnotify.detection.discord_webhook.url.tooltip": "Set the global Discord webhook URL for all notifications.", "option.chatnotify.notif": "Notifications", "option.chatnotify.notif.click_edit": "Click to edit.\nRight-click to toggle.", "option.chatnotify.notif.color.field.tooltip": "Text Restyle Color", @@ -172,11 +176,9 @@ "option.chatnotify.notif.response.list.tooltip.warning": "Warning: Can crash the game, use with caution.", "option.chatnotify.notif.response.time.tooltip": "Time in ticks to wait (after the previous message, if any) before sending.", "option.chatnotify.notif.response.type.COMMANDKEYS.tooltip": "CommandKeys response.\nIf the CommandKeys mod is loaded (version 2.3.1 or later), this response will trigger all macros with keybinds matching the key IDs you set.", - "option.chatnotify.notif.response.type.DISCORD.tooltip": "Discord webhook response.\nSends a message to the specified Discord webhook URL when this notification is activated.", + "option.chatnotify.notif.response.type.DISCORD.tooltip": "Discord webhook response.\nUses the global Discord webhook settings in Detection.", "option.chatnotify.notif.response.type.NORMAL.tooltip": "Normal response.", "option.chatnotify.notif.response.type.REGEX.tooltip": "Regex response.\nUse (1), (2) etc in the message to access regex capturing groups from the trigger.", - "option.chatnotify.notif.response.discord.message": "Message", - "option.chatnotify.notif.response.discord.webhook_url": "Webhook URL", "option.chatnotify.notif.sound": "Sound", "option.chatnotify.notif.sound.field.tooltip": "Notification Sound", "option.chatnotify.notif.sound.open.minecraft_volume.tooltip": "Open Minecraft's volume settings", diff --git a/common/src/main/resources/assets/chatnotify/lang/ru_ru.json b/common/src/main/resources/assets/chatnotify/lang/ru_ru.json index fb1101f3..a72f4dde 100644 --- a/common/src/main/resources/assets/chatnotify/lang/ru_ru.json +++ b/common/src/main/resources/assets/chatnotify/lang/ru_ru.json @@ -85,6 +85,10 @@ "option.chatnotify.detection.sender.mode.status.SENT_MATCH": "Совпадение отправленного сообщения", "option.chatnotify.detection.sender.mode.status.SENT_MATCH.tooltip": "Входящие сообщения будут определены как отправленные вами, если они совпадают с недавно отправленным сообщением и соответствуют триггеру первого уведомления.", "option.chatnotify.detection.sender.tooltip": "Если ваши собственные сообщения вызывают уведомления, попробуйте настроить следующие параметры.", + "option.chatnotify.detection.discord_webhook": "Discord Webhook", + "option.chatnotify.detection.discord_webhook.tooltip": "Включить или отключить ответы вебхука Discord для всех уведомлений.", + "option.chatnotify.detection.discord_webhook.url": "URL вебхука", + "option.chatnotify.detection.discord_webhook.url.tooltip": "Установите глобальный URL вебхука Discord для всех уведомлений.", "option.chatnotify.notif": "Параметры уведомлений", "option.chatnotify.notif.click_edit": "Нажмите, чтобы редактировать.\nПКМ для переключения.", "option.chatnotify.notif.color.field.tooltip": "Цвет изменения стиля текста", diff --git a/common/src/main/resources/assets/chatnotify/lang/zh_cn.json b/common/src/main/resources/assets/chatnotify/lang/zh_cn.json index f394d81e..3307346c 100644 --- a/common/src/main/resources/assets/chatnotify/lang/zh_cn.json +++ b/common/src/main/resources/assets/chatnotify/lang/zh_cn.json @@ -85,6 +85,10 @@ "option.chatnotify.detection.sender.mode.status.SENT_MATCH": "已发送消息匹配", "option.chatnotify.detection.sender.mode.status.SENT_MATCH.tooltip": "如果传入消息与最近发送的消息匹配且与第一个通知的触发器匹配,则将其识别为你发送的消息", "option.chatnotify.detection.sender.tooltip": "If your own messages are triggering notifications unexpectedly, try adjusting the options below.", + "option.chatnotify.detection.discord_webhook": "Discord Webhook", + "option.chatnotify.detection.discord_webhook.tooltip": "Enable or disable Discord webhook responses for all notifications.", + "option.chatnotify.detection.discord_webhook.url": "Webhook URL", + "option.chatnotify.detection.discord_webhook.url.tooltip": "Set the global Discord webhook URL for all notifications.", "option.chatnotify.notif": "通知选项", "option.chatnotify.notif.click_edit": "点击编辑\n右键点击切换", "option.chatnotify.notif.color.field.tooltip": "文本重样式颜色", diff --git a/common/src/main/resources/assets/chatnotify/lang/zh_tw.json b/common/src/main/resources/assets/chatnotify/lang/zh_tw.json index ed9c9303..c8f53990 100644 --- a/common/src/main/resources/assets/chatnotify/lang/zh_tw.json +++ b/common/src/main/resources/assets/chatnotify/lang/zh_tw.json @@ -85,6 +85,10 @@ "option.chatnotify.detection.sender.mode.status.SENT_MATCH": "Sent message match", "option.chatnotify.detection.sender.mode.status.SENT_MATCH.tooltip": "Incoming messages will be identified as sent by you if they match a recently-sent message and match a trigger of the first notification.", "option.chatnotify.detection.sender.tooltip": "If your own messages are triggering notifications unexpectedly, try adjusting the options below.", + "option.chatnotify.detection.discord_webhook": "Discord Webhook", + "option.chatnotify.detection.discord_webhook.tooltip": "Enable or disable Discord webhook responses for all notifications.", + "option.chatnotify.detection.discord_webhook.url": "Webhook URL", + "option.chatnotify.detection.discord_webhook.url.tooltip": "Set the global Discord webhook URL for all notifications.", "option.chatnotify.notif": "通知選項", "option.chatnotify.notif.click_edit": "Click to edit.\nRight-click to toggle.", "option.chatnotify.notif.color.field.tooltip": "Text Restyle Color", diff --git a/gradle.properties b/gradle.properties index 27b5b09b..238ff3d1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ template_version=10 # Mod Version -mod_version=2.6.6 +mod_version=2.6.7 # 'STABLE', 'BETA' or 'ALPHA' mod_version_type=STABLE