Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Run Tests

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'gradle'

- name: Run tests with Gradle
run: gradle test --no-daemon
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow runs gradle test, but this repo doesn't include the Gradle wrapper (gradlew) and GitHub-hosted runners don't guarantee a gradle executable. This will likely make CI fail. Prefer committing the Gradle wrapper and running ./gradlew test --no-daemon, or add a step to install/setup Gradle before this command.

Suggested change
run: gradle test --no-daemon
uses: gradle/gradle-build-action@v3
with:
arguments: test --no-daemon

Copilot uses AI. Check for mistakes.

- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: |
build/test-results/**/*.xml
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,8 @@ buildNumber.properties
# Common working directory
run/

.env
.env

# Gradle
.gradle/
build/
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Herald
Herald is a Minecraft server plugin that sends email notifications when players join the server.
Herald is a Minecraft server plugin that sends notifications when players join the server. It supports both email and Discord notifications.

![screenshot of emails](./screenshots/mailhog-7-1-2025.PNG)

## Features
- Email notification system for player logins
- Discord webhook notifications for player logins
- Configurable notification methods (email, Discord, or both)
- Easy to configure and use
- Built-in mail server setup with Docker

Expand Down Expand Up @@ -42,10 +44,10 @@ This setup creates two mail-related services:
All emails sent to the mail server are relayed to MailHog, where you can view them in a convenient web interface.

## Configuration
After first run, a configuration file will be created that you can modify to set up your email notification preferences.
After first run, a configuration file will be created that you can modify to set up your notification preferences.

### Herald Plugin Configuration
Update your Herald `config.yml` to use the mail server:
Update your Herald `config.yml` to configure email and/or Discord notifications:

```yaml
# Herald Configuration
Expand All @@ -66,10 +68,31 @@ smtp:

email:
sender: "minecraft@minecraft-mail.local"

# Discord Configuration
discord:
enabled: false # Set to true to enable Discord notifications
webhook-url: "" # Your Discord webhook URL
```

Make sure to replace "mailserver" with your server's IP address if your Minecraft server is not running in the same Docker network.

### Setting up Discord Notifications
To enable Discord notifications:

1. In your Discord server, go to Server Settings → Integrations → Webhooks
2. Click "New Webhook"
3. Configure the webhook:
- Set a name (e.g., "Herald Bot")
- Choose the channel where notifications should be sent
- Copy the webhook URL
4. In your Herald `config.yml`, set:
- `discord.enabled: true`
- `discord.webhook-url: "<your-webhook-url>"`
5. Restart your Minecraft server or reload the plugin

You can use Discord notifications alone or in combination with email notifications.

## Testing
The Docker Compose setup includes a test Minecraft server that can be used to test the Herald plugin:

Expand Down
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,20 @@ repositories {
dependencies {
compileOnly("org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT")
implementation 'com.sun.mail:jakarta.mail:2.0.1'

// Test dependencies
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks {
runServer {
minecraftVersion("1.21")
}

test {
useJUnitPlatform()
}
}

def targetJavaVersion = 21
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/com/dansplugins/herald/DiscordNotifier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.dansplugins.herald;

import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class DiscordNotifier {

private final String webhookUrl;

public DiscordNotifier(String webhookUrl) {
this.webhookUrl = webhookUrl;
}

/**
* Send a message to Discord via webhook
* @param content The message content to send
* @throws IOException if there's an error sending the message
*/
public void sendMessage(String content) throws IOException {
if (webhookUrl == null || webhookUrl.isEmpty()) {
throw new IllegalArgumentException("Discord webhook URL is not configured");
}

URL url = new URL(webhookUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true);

// Create JSON payload with the message content
String jsonPayload = String.format("{\"content\": \"%s\"}", escapeJson(content));

try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}

int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
throw new IOException("Discord webhook returned error code: " + responseCode);
}
Comment on lines +47 to +64
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpURLConnection is used without connect/read timeouts, so a stalled network/DNS issue can block an async scheduler thread indefinitely. Set reasonable setConnectTimeout/setReadTimeout values and ensure the connection is disconnected/closed in all cases to avoid thread starvation under repeated joins.

Suggested change
connection.setDoOutput(true);
// Create JSON payload with the message content
String jsonPayload = String.format("{\"content\": \"%s\"}", escapeJson(content));
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
throw new IOException("Discord webhook returned error code: " + responseCode);
}
connection.setConnectTimeout(5000); // 5 seconds connect timeout
connection.setReadTimeout(10000); // 10 seconds read timeout
connection.setDoOutput(true);
// Create JSON payload with the message content
String jsonPayload = String.format("{\"content\": \"%s\"}", escapeJson(content));
try {
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
throw new IOException("Discord webhook returned error code: " + responseCode);
}
} finally {
if (connection != null) {
connection.disconnect();
}
}

Copilot uses AI. Check for mistakes.
}

/**
* Escape special characters in JSON strings
* @param text The text to escape
* @return The escaped text
*/
String escapeJson(String text) {
if (text == null) {
return "";
}
return text.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current JSON escaping only handles backslash, quote, and a few whitespace controls. JSON strings must also escape other control characters (e.g., backspace/formfeed and any chars < 0x20 via \uXXXX), otherwise the payload can become invalid JSON if such characters appear in content. Consider implementing complete JSON string escaping here.

Suggested change
return text.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
StringBuilder sb = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
switch (c) {
case '"':
sb.append("\\\"");
break;
case '\\':
sb.append("\\\\");
break;
case '\b':
sb.append("\\b");
break;
case '\f':
sb.append("\\f");
break;
case '\n':
sb.append("\\n");
break;
case '\r':
sb.append("\\r");
break;
case '\t':
sb.append("\\t");
break;
default:
if (c < 0x20) {
sb.append("\\u00");
String hex = Integer.toHexString(c);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
} else {
sb.append(c);
}
break;
}
}
return sb.toString();

Copilot uses AI. Check for mistakes.
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/dansplugins/herald/Herald.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public final class Herald extends JavaPlugin implements Listener {
private String smtpPassword;
private String emailSender;
private boolean useTLS;
private boolean discordEnabled;
private String discordWebhookUrl;
private DiscordNotifier discordNotifier;

@Override
public void onEnable() {
Expand Down Expand Up @@ -52,6 +55,16 @@ private void loadConfiguration() {
smtpPassword = getConfig().getString("smtp.password");
emailSender = getConfig().getString("email.sender");
useTLS = getConfig().getBoolean("smtp.use-tls", true);

// Load Discord settings
discordEnabled = getConfig().getBoolean("discord.enabled", false);
discordWebhookUrl = getConfig().getString("discord.webhook-url");

// Initialize Discord notifier if enabled
if (discordEnabled && discordWebhookUrl != null && !discordWebhookUrl.isEmpty()) {
discordNotifier = new DiscordNotifier(discordWebhookUrl);
getLogger().info("Discord notifications enabled");
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If discord.enabled is true but discord.webhook-url is missing/empty, discordNotifier remains null and Discord notifications are silently skipped. Logging a warning during config load in this case would make misconfiguration obvious for server admins.

Suggested change
getLogger().info("Discord notifications enabled");
getLogger().info("Discord notifications enabled");
} else if (discordEnabled) {
getLogger().warning("Discord notifications are enabled in config, but 'discord.webhook-url' is missing or empty. Discord notifications will be skipped.");

Copilot uses AI. Check for mistakes.
}
}

@EventHandler
Expand All @@ -73,6 +86,21 @@ public void onPlayerJoin(PlayerJoinEvent event) {
e.printStackTrace();
}
});

// Send Discord notification if enabled
if (discordEnabled && discordNotifier != null) {
String discordMessage = "**" + playerName + "** joined the **" + serverName + "** server";

getServer().getScheduler().runTaskAsynchronously(this, () -> {
try {
discordNotifier.sendMessage(discordMessage);
getLogger().info("Discord notification sent successfully for player: " + playerName);
} catch (Exception e) {
getLogger().severe("Failed to send Discord notification: " + e.getMessage());
e.printStackTrace();
}
});
}
}

private void sendEmail(String subject, String body) throws MessagingException {
Expand Down
7 changes: 6 additions & 1 deletion src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ smtp:
use-tls: false # Set to true if using port 587

email:
sender: "minecraft@minecraft-mail.local"
sender: "minecraft@minecraft-mail.local"

# Discord Configuration
discord:
enabled: false # Set to true to enable Discord notifications
webhook-url: "" # Discord webhook URL for the channel you want to send notifications to
2 changes: 1 addition & 1 deletion src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ version: '${version}'
main: com.dansplugins.herald.Herald
api-version: '1.16'
authors: ["Daniel McCoy Stephenson"]
description: Sends email notifications when players join the server
description: Sends email and Discord notifications when players join the server
Loading