Skip to content

Commit 8edc694

Browse files
committed
Add and implement webhook addon; enables Discord webhook (#299)
This feature was originally intended to be used with Discord webhooks. The documentation is made for Discord since it is expected that most server owners will use it for that purpose. That said, using the webhook with a different HTTP endpoint, other than Discord, is perfectly possible and supported. As a result, the addon is not called "discord-webook" but just "webhook".
1 parent 79a6095 commit 8edc694

File tree

12 files changed

+446
-2
lines changed

12 files changed

+446
-2
lines changed

bans-core-addons/addon-integration/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@
135135
<artifactId>addon-warn-actions</artifactId>
136136
<version>${project.version}</version>
137137
</dependency>
138+
<dependency>
139+
<groupId>space.arim.libertybans.addon</groupId>
140+
<artifactId>addon-webhook</artifactId>
141+
<version>${project.version}</version>
142+
</dependency>
138143

139144
<dependency>
140145
<groupId>org.junit.jupiter</groupId>

bans-core-addons/addon-integration/src/test/java/space/arim/libertybans/core/addon/it/ServiceLoadingIT.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import space.arim.libertybans.core.addon.shortcutreasons.ShortcutReasonsModule;
3232
import space.arim.libertybans.core.addon.staffrollback.StaffRollbackModule;
3333
import space.arim.libertybans.core.addon.warnactions.WarnActionsModule;
34+
import space.arim.libertybans.core.addon.webhook.WebhookModule;
3435

3536
import java.util.Set;
3637

@@ -45,7 +46,7 @@ public void loadAddons() {
4546
Set.of(
4647
new CheckPunishModule(), new CheckUserModule(), new ExpungeModule(), new ExtendModule(),
4748
new StaffRollbackModule(), new ExemptionLuckPermsModule(), new ExemptionVaultModule(),
48-
new LayoutsModule(), new ShortcutReasonsModule(), new WarnActionsModule()
49+
new LayoutsModule(), new ShortcutReasonsModule(), new WarnActionsModule(), new WebhookModule()
4950
),
5051
assertDoesNotThrow(AddonLoader::loadAddonBindModules)
5152
);

bans-core-addons/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
<module>layouts</module>
6767
<module>shortcut-reasons</module>
6868
<module>warn-actions</module>
69+
<module>webhook</module>
6970
<module>addon-integration</module>
7071
</modules>
7172

bans-core-addons/webhook/pom.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!--
2+
~ LibertyBans
3+
~ Copyright © 2025 Anand Beh
4+
~
5+
~ LibertyBans is free software: you can redistribute it and/or modify
6+
~ it under the terms of the GNU Affero General Public License as
7+
~ published by the Free Software Foundation, either version 3 of the
8+
~ License, or (at your option) any later version.
9+
~
10+
~ LibertyBans is distributed in the hope that it will be useful,
11+
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
~ GNU Affero General Public License for more details.
14+
~
15+
~ You should have received a copy of the GNU Affero General Public License
16+
~ along with LibertyBans. If not, see <https://www.gnu.org/licenses/>
17+
~ and navigate to version 3 of the GNU Affero General Public License.
18+
-->
19+
20+
<project xmlns="http://maven.apache.org/POM/4.0.0"
21+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
22+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
23+
<modelVersion>4.0.0</modelVersion>
24+
25+
<parent>
26+
<groupId>space.arim.libertybans.addon</groupId>
27+
<artifactId>bans-core-addons</artifactId>
28+
<version>1.1.2-SNAPSHOT</version>
29+
</parent>
30+
31+
<name>webhook</name>
32+
<artifactId>addon-webhook</artifactId>
33+
</project>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* LibertyBans
3+
* Copyright © 2025 Anand Beh
4+
*
5+
* LibertyBans is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* LibertyBans is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with LibertyBans. If not, see <https://www.gnu.org/licenses/>
17+
* and navigate to version 3 of the GNU Affero General Public License.
18+
*/
19+
20+
import space.arim.libertybans.core.addon.AddonProvider;
21+
import space.arim.libertybans.core.addon.webhook.WebhookProvider;
22+
23+
module space.arim.libertybans.core.addon.webhook {
24+
requires jakarta.inject;
25+
requires net.kyori.adventure;
26+
requires net.kyori.examination.api;
27+
requires static org.checkerframework.checker.qual;
28+
requires org.slf4j;
29+
requires space.arim.api.jsonchat;
30+
requires space.arim.dazzleconf;
31+
requires space.arim.injector;
32+
requires space.arim.libertybans.core;
33+
requires java.net.http;
34+
exports space.arim.libertybans.core.addon.webhook;
35+
provides AddonProvider with WebhookProvider;
36+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* LibertyBans
3+
* Copyright © 2025 Anand Beh
4+
*
5+
* LibertyBans is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* LibertyBans is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with LibertyBans. If not, see <https://www.gnu.org/licenses/>
17+
* and navigate to version 3 of the GNU Affero General Public License.
18+
*/
19+
20+
package space.arim.libertybans.core.addon.webhook;
21+
22+
import jakarta.inject.Inject;
23+
import jakarta.inject.Singleton;
24+
import net.kyori.adventure.text.Component;
25+
import net.kyori.adventure.text.TextComponent;
26+
import org.checkerframework.checker.nullness.qual.Nullable;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
import space.arim.api.jsonchat.adventure.util.ComponentText;
30+
import space.arim.libertybans.api.Operator;
31+
import space.arim.libertybans.api.event.PostPardonEvent;
32+
import space.arim.libertybans.api.event.PostPunishEvent;
33+
import space.arim.libertybans.api.punish.Punishment;
34+
import space.arim.libertybans.core.config.InternalFormatter;
35+
import space.arim.libertybans.core.service.FuturePoster;
36+
import space.arim.omnibus.events.ListenerPriorities;
37+
import space.arim.omnibus.events.ListeningMethod;
38+
import space.arim.omnibus.util.ThisClass;
39+
40+
import java.net.URI;
41+
import java.net.http.HttpClient;
42+
import java.net.http.HttpRequest;
43+
import java.net.http.HttpResponse;
44+
import java.time.Duration;
45+
import java.util.concurrent.CompletableFuture;
46+
import java.util.function.Function;
47+
48+
@Singleton
49+
public final class PostWebhookListener {
50+
51+
private final WebhookAddon addon;
52+
private final InternalFormatter formatter;
53+
private final FuturePoster futurePoster;
54+
private final HttpClient client = HttpClient.newHttpClient();
55+
56+
private static final Logger logger = LoggerFactory.getLogger(ThisClass.get());
57+
58+
@Inject
59+
public PostWebhookListener(WebhookAddon addon, InternalFormatter formatter, FuturePoster futurePoster) {
60+
this.addon = addon;
61+
this.formatter = formatter;
62+
this.futurePoster = futurePoster;
63+
}
64+
65+
@ListeningMethod(priority = ListenerPriorities.LOW)
66+
public void onPunish(PostPunishEvent event) {
67+
onEvent(event.getPunishment(), event.getTarget().orElse(null), null, WebhookConfig::onPunish);
68+
}
69+
70+
@ListeningMethod(priority = ListenerPriorities.LOW)
71+
public void onPardon(PostPardonEvent event) {
72+
onEvent(event.getPunishment(), event.getTarget().orElse(null), event.getOperator(), WebhookConfig::onPardon);
73+
}
74+
75+
private void onEvent(Punishment punishment, @Nullable String target, @Nullable Operator unOperator,
76+
Function<WebhookConfig, WebhookConfig.EventPayload> getEventPayload) {
77+
WebhookConfig config = addon.config();
78+
if (!config.enable()) {
79+
return;
80+
}
81+
URI webhookUrl = config.webhookUrl();
82+
WebhookConfig.EventPayload eventPayload = getEventPayload.apply(config);
83+
if (!eventPayload.enable()) {
84+
return;
85+
}
86+
String jsonPayload = eventPayload.jsonPayload();
87+
jsonPayload = jsonPayload.replace("%TARGET%", target == null ? "<none>" : target);
88+
ComponentText formattable = ComponentText.create(Component.text(jsonPayload));
89+
CompletableFuture<Component> formatted;
90+
if (unOperator == null) {
91+
formatted = formatter.formatWithPunishment(formattable, punishment);
92+
} else {
93+
formatted = formatter.formatWithPunishmentAndUnoperator(formattable, punishment, unOperator);
94+
}
95+
var future = formatted.thenCompose(component -> {
96+
String json = ((TextComponent) component).content();
97+
return postWebhook(webhookUrl, json);
98+
});
99+
futurePoster.postFuture(future);
100+
}
101+
102+
private CompletableFuture<Void> postWebhook(URI webhookUrl, String json) {
103+
HttpRequest request = HttpRequest.newBuilder()
104+
.uri(webhookUrl)
105+
.header("Content-Type", "application/json")
106+
.timeout(Duration.ofSeconds(40L))
107+
.POST(HttpRequest.BodyPublishers.ofString(json))
108+
.build();
109+
return client.sendAsync(request, HttpResponse.BodyHandlers.discarding()).thenAccept(response -> {
110+
int statusCode = response.statusCode();
111+
if (statusCode == 200 || statusCode == 204) {
112+
logger.debug("Successfully posted webhook");
113+
} else {
114+
logger.warn("Received unexpected status code {} from webhook API", statusCode);
115+
}
116+
});
117+
}
118+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* LibertyBans
3+
* Copyright © 2025 Anand Beh
4+
*
5+
* LibertyBans is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* LibertyBans is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with LibertyBans. If not, see <https://www.gnu.org/licenses/>
17+
* and navigate to version 3 of the GNU Affero General Public License.
18+
*/
19+
20+
package space.arim.libertybans.core.addon.webhook;
21+
22+
import jakarta.inject.Inject;
23+
import jakarta.inject.Provider;
24+
import jakarta.inject.Singleton;
25+
import space.arim.libertybans.core.addon.AbstractAddon;
26+
import space.arim.libertybans.core.addon.AddonCenter;
27+
import space.arim.omnibus.Omnibus;
28+
29+
@Singleton
30+
public final class WebhookAddon extends AbstractAddon<WebhookConfig> {
31+
32+
private final Omnibus omnibus;
33+
private final Provider<PostWebhookListener> listener;
34+
35+
@Inject
36+
public WebhookAddon(AddonCenter addonCenter, Omnibus omnibus, Provider<PostWebhookListener> listener) {
37+
super(addonCenter);
38+
this.omnibus = omnibus;
39+
this.listener = listener;
40+
}
41+
42+
@Override
43+
public void startup() {
44+
omnibus.getEventBus().registerListeningMethods(listener.get());
45+
}
46+
47+
@Override
48+
public void shutdown() {
49+
omnibus.getEventBus().unregisterListeningMethods(listener.get());
50+
}
51+
52+
@Override
53+
public Class<WebhookConfig> configInterface() {
54+
return WebhookConfig.class;
55+
}
56+
57+
@Override
58+
public String identifier() {
59+
return "webhook";
60+
}
61+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* LibertyBans
3+
* Copyright © 2025 Anand Beh
4+
*
5+
* LibertyBans is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* LibertyBans is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with LibertyBans. If not, see <https://www.gnu.org/licenses/>
17+
* and navigate to version 3 of the GNU Affero General Public License.
18+
*/
19+
20+
package space.arim.libertybans.core.addon.webhook;
21+
22+
import space.arim.dazzleconf.annote.ConfComments;
23+
import space.arim.dazzleconf.annote.ConfDefault;
24+
import space.arim.dazzleconf.annote.ConfHeader;
25+
import space.arim.dazzleconf.annote.ConfKey;
26+
import space.arim.dazzleconf.annote.SubSection;
27+
import space.arim.libertybans.core.addon.AddonConfig;
28+
29+
import java.net.URI;
30+
31+
@ConfHeader({
32+
"Configuration for sending webhooks every time a player is punished.",
33+
"",
34+
"Most people will use this addon for Discord webhooks. As a result, the comments guide people through",
35+
"creating a Discord webhook and configuring it here. However, it is technically possible to send any kind of",
36+
"webhook to any kind of URL; the free-form json payload enables this possibility."
37+
})
38+
public interface WebhookConfig extends AddonConfig {
39+
40+
41+
@ConfKey("webhook-url")
42+
@ConfComments({
43+
"This is the URL of your webhook. Do not share this with anyone!",
44+
"visit your Discord server's admin panel, then click on Integrations > New Webhook.",
45+
"Click 'Copy Webhook URL' and paste it here."
46+
})
47+
@ConfDefault.DefaultString("https://mysite.com")
48+
URI webhookUrl();
49+
50+
@ConfKey("on-punish")
51+
@SubSection
52+
@ConfComments("When a player is punished, this webhook is sent")
53+
EventPayload onPunish();
54+
55+
@ConfKey("on-pardon")
56+
@SubSection
57+
@ConfComments("When a player is unpunished, this webhook is sent. Note that punishments expiring will NOT trigger this webhook.")
58+
EventPayload onPardon();
59+
60+
interface EventPayload {
61+
62+
@ConfComments("Whether to enable the webhook for this event")
63+
@ConfDefault.DefaultBoolean(false)
64+
boolean enable();
65+
66+
@ConfKey("json-payload")
67+
@ConfComments({
68+
"The raw JSON that will be sent to the Discord webhook.",
69+
"A Discord webhook has many potential fields, so this section lets you add whatever you want.",
70+
"",
71+
"Note that punishment-related variables are fully supported here, but color codes are not.",
72+
"",
73+
"We HIGHLY recommend using a tool, like Discohook, to create a webhook",
74+
"Steps:",
75+
"1. Visit https://discohook.org/",
76+
"2. Make the webhook look how you want it.",
77+
"3. Click 'JSON Editor' and copy the text",
78+
"4. Minify the JSON using https://www.minifyjson.org/ so that it fits on a single line",
79+
"5. Copy the minified version here.",
80+
"",
81+
"If editing the raw JSON, it's recommended to use a JSON minifier/prettifier to help visualize your changes.",
82+
"For example, https://www.minifyjson.org/ . Use the \"Beautify\" button to see the JSON, then edit it.",
83+
"Once you're done, use the \"Minify\" button and paste the result back here.",
84+
"",
85+
"You can see a full list of options by checking Discord's documentation, or by browsing some examples.",
86+
"Official reference:",
87+
"https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params",
88+
"Examples and guide:",
89+
"https://gist.github.com/Birdie0/78ee79402a4301b1faf412ab5f1cdcf9#example-for-a-webhook"
90+
})
91+
@ConfDefault.DefaultString("{\"content\":\"BAM! %VICTIM% has been punished with %TYPE%.\\n\\nThere's more info below, but you don't have to read it. This punishment will last for %DURATION%\\n_ _\",\"embeds\":[{\"title\":\"Punishment reason\",\"description\":\"%REASON%\",\"color\":5814783,\"fields\":[{\"name\":\"Operator\",\"value\":\"By %OPERATOR%\"},{\"name\":\"Scope\",\"value\":\"%SCOPE%\"}],\"author\":{\"name\":\"LibertyBans\"}}]}")
92+
String jsonPayload();
93+
94+
}
95+
96+
}

0 commit comments

Comments
 (0)