diff --git a/docs/external-plugin-messaging.md b/docs/external-plugin-messaging.md index 95249e30..28ac580f 100644 --- a/docs/external-plugin-messaging.md +++ b/docs/external-plugin-messaging.md @@ -7,9 +7,8 @@ Users can opt-out of this capability by disabling `External Plugin Requests > En Plugins can request that a screenshot is included with the notification, but users can also opt-out by disabling `External Plugin Requests > Send Image` (default: on) and `External Plugin Requests > Allow Overriding 'Send Image'` (default: off). -Plugins can include a Discord url for the webhook, otherwise Dink will utilize `External Webhook Override` +Plugins can include urls for the webhook, otherwise Dink will utilize `External Webhook Override` (or `Primary Webhook URLs` if an external url override is not specified). -If a plugin requests a non-Discord url, it will be ignored in favor of the Dink configuration. Below we describe the payload structure for how plugins can customize the webhook body and include a full code example to streamline implementation. @@ -23,7 +22,7 @@ The `Map` that is supplied to `PluginMessage` will be converted | ---------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `text` | Y | String | The body text of the notification. This field supports templating (see `replacements` below) and by default `%USERNAME%` is an available replacement. | | `sourcePlugin` | Y | String | The human-facing name of the plugin submitting the webhook notification request. | -| `urls` | N | String | The Discord URLs that the notification should be sent to (newline separated). | +| `urls` | N | List | A list of `okhttp3.HttpUrl`s that the notification should be sent to. | | `title` | N | String | The title for the Discord embed. | | `thumbnail` | N | String | A URL to an image for the thumbnail icon of the Discord embed. | | `imageRequested` | N | boolean | Whether dink should include a screenshot with the notification. | @@ -44,7 +43,7 @@ data.put("title", "An optional embed title for your notification"); data.put("imageRequested", true); data.put("fields", List.of(new Field("sample key", "sample value"))); data.put("metadata", Map.of("custom key", "custom value")); -data.put("urls", "https://discord.com/api/webhooks/a/b \n https://discord.com/api/webhooks/c/d"); +data.put("urls", Arrays.asList(HttpUrl.parse("https://discord.com/api/webhooks/a/b"), HttpUrl.parse("https://discord.com/api/webhooks/c/d"))); PluginMessage dinkRequest = new PluginMessage("dink", "notify", data); eventBus.post(dinkRequest); diff --git a/src/main/java/dinkplugin/domain/ExternalNotificationRequest.java b/src/main/java/dinkplugin/domain/ExternalNotificationRequest.java index a9d6aff2..2534b020 100644 --- a/src/main/java/dinkplugin/domain/ExternalNotificationRequest.java +++ b/src/main/java/dinkplugin/domain/ExternalNotificationRequest.java @@ -2,10 +2,7 @@ import dinkplugin.message.Field; import dinkplugin.message.templating.impl.SimpleReplacement; -import dinkplugin.util.ConfigUtil; -import lombok.AccessLevel; import lombok.Data; -import lombok.Getter; import okhttp3.HttpUrl; import org.jetbrains.annotations.Nullable; @@ -13,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Supplier; import java.util.stream.Collectors; @Data @@ -26,16 +24,12 @@ public class ExternalNotificationRequest { private @Nullable List fields; private @Nullable Map replacements; private @Nullable Map metadata; - @Getter(AccessLevel.PRIVATE) // to avoid accidentally using the requested url without sanitization - private @Nullable String urls; + private @Nullable List urls; - public String getSanitizedUrls() { - return ConfigUtil.readDelimited(this.urls) - .map(HttpUrl::parse) - .filter(Objects::nonNull) - .filter(url -> "discord.com".equalsIgnoreCase(url.host())) - .map(HttpUrl::toString) - .collect(Collectors.joining("\n")); + public String getUrls(Supplier defaultValue) { + return urls != null + ? urls.stream().filter(Objects::nonNull).map(HttpUrl::toString).collect(Collectors.joining("\n")) + : defaultValue.get(); } public List getFields() { diff --git a/src/main/java/dinkplugin/notifiers/ExternalPluginNotifier.java b/src/main/java/dinkplugin/notifiers/ExternalPluginNotifier.java index 6249f30d..1690fbfc 100644 --- a/src/main/java/dinkplugin/notifiers/ExternalPluginNotifier.java +++ b/src/main/java/dinkplugin/notifiers/ExternalPluginNotifier.java @@ -8,6 +8,7 @@ import dinkplugin.message.templating.Replacements; import dinkplugin.message.templating.Template; import dinkplugin.notifiers.data.ExternalNotificationData; +import dinkplugin.util.HttpUrlAdapter; import dinkplugin.util.Utils; import lombok.extern.slf4j.Slf4j; import net.runelite.api.GameState; @@ -21,7 +22,6 @@ @Singleton public class ExternalPluginNotifier extends BaseNotifier { - @Inject private Gson gson; @Override @@ -34,6 +34,13 @@ protected String getWebhookUrl() { return config.externalWebhook(); } + @Inject + void init(Gson gson) { + this.gson = gson.newBuilder() + .registerTypeAdapter(HttpUrl.class, new HttpUrlAdapter()) + .create(); + } + public void onNotify(Map data) { if (!isEnabled()) { log.debug("Skipping requested external dink since notifier is disabled: {}", data); @@ -91,11 +98,7 @@ private void handleNotify(ExternalNotificationRequest input) { .extra(new ExternalNotificationData(input.getSourcePlugin(), input.getFields(), input.getMetadata())) .build(); - var urls = input.getSanitizedUrls(); - if (!urls.isEmpty()) { - log.info("{} requested a dink notification to externally-specified url(s)", input.getSourcePlugin()); - } - + var urls = input.getUrls(this::getWebhookUrl); createMessage(urls, image, body); } diff --git a/src/main/java/dinkplugin/util/HttpUrlAdapter.java b/src/main/java/dinkplugin/util/HttpUrlAdapter.java new file mode 100644 index 00000000..245df6d5 --- /dev/null +++ b/src/main/java/dinkplugin/util/HttpUrlAdapter.java @@ -0,0 +1,20 @@ +package dinkplugin.util; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import okhttp3.HttpUrl; + +import java.io.IOException; + +public class HttpUrlAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, HttpUrl url) throws IOException { + out.value(url != null ? url.toString() : null); + } + + @Override + public HttpUrl read(JsonReader in) throws IOException { + return in.hasNext() ? HttpUrl.parse(in.nextString()) : null; + } +} diff --git a/src/test/java/dinkplugin/notifiers/ExternalPluginNotifierTest.java b/src/test/java/dinkplugin/notifiers/ExternalPluginNotifierTest.java index 75f5e87a..b4db63b1 100644 --- a/src/test/java/dinkplugin/notifiers/ExternalPluginNotifierTest.java +++ b/src/test/java/dinkplugin/notifiers/ExternalPluginNotifierTest.java @@ -9,6 +9,7 @@ import dinkplugin.message.templating.impl.SimpleReplacement; import dinkplugin.notifiers.data.ExternalNotificationData; import net.runelite.client.events.PluginMessage; +import okhttp3.HttpUrl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -43,11 +44,11 @@ protected void setUp() { @Test void testNotify() { // fire event - plugin.onPluginMessage(new PluginMessage("dink", "notify", samplePayload("https://example.com"))); + plugin.onPluginMessage(new PluginMessage("dink", "notify", samplePayload("https://example.com/"))); // verify notification verifyCreateMessage( - null, + "https://example.com/", false, NotificationBody.builder() .type(NotificationType.EXTERNAL_PLUGIN) @@ -67,10 +68,13 @@ void testNotify() { } @Test - void testUrl() { - // fire event + void testFallbackUrl() { + // update config mocks String url = "https://discord.com/example"; - plugin.onPluginMessage(new PluginMessage("dink", "notify", samplePayload(url))); + when(config.externalWebhook()).thenReturn(url); + + // fire event + plugin.onPluginMessage(new PluginMessage("dink", "notify", samplePayload(null))); // verify notification verifyCreateMessage( @@ -188,7 +192,7 @@ void testRequestImageDenied() { @Test void testIgnoreNamespace() { // fire event - plugin.onPluginMessage(new PluginMessage("DANK", "notify", samplePayload("https://example.com"))); + plugin.onPluginMessage(new PluginMessage("DANK", "notify", samplePayload("https://example.com/"))); // ensure no notification verify(messageHandler, never()).createMessage(any(), anyBoolean(), any()); @@ -197,7 +201,7 @@ void testIgnoreNamespace() { @Test void testIgnoreName() { // fire event - plugin.onPluginMessage(new PluginMessage("dink", "donk", samplePayload("https://example.com"))); + plugin.onPluginMessage(new PluginMessage("dink", "donk", samplePayload("https://example.com/"))); // ensure no notification verify(messageHandler, never()).createMessage(any(), anyBoolean(), any()); @@ -209,13 +213,13 @@ void testDisabled() { when(config.notifyExternal()).thenReturn(false); // fire event - plugin.onPluginMessage(new PluginMessage("dink", "notify", samplePayload("https://example.com"))); + plugin.onPluginMessage(new PluginMessage("dink", "notify", samplePayload("https://example.com/"))); // ensure no notification verify(messageHandler, never()).createMessage(any(), anyBoolean(), any()); } - private static Map samplePayload(String urls) { + private static Map samplePayload(String url) { Map data = new HashMap<>(); data.put("text", "Hello %TARGET% from %USERNAME%"); data.put("title", "My Title"); @@ -224,7 +228,9 @@ private static Map samplePayload(String urls) { data.put("replacements", Map.of("%TARGET%", new SimpleReplacement("world", null))); data.put("metadata", Map.of("hello", "world")); data.put("sourcePlugin", "MyExternalPlugin"); - data.put("urls", urls); + if (url != null) { + data.put("urls", Collections.singletonList(HttpUrl.parse(url))); + } return data; }