Skip to content

Make Discord the flagship notification method for player joins#5

Merged
dmccoystephenson merged 16 commits intodevfrom
copilot/fix-27b487e8-e0f3-4525-a831-d24c30878dc3
Mar 7, 2026
Merged

Make Discord the flagship notification method for player joins#5
dmccoystephenson merged 16 commits intodevfrom
copilot/fix-27b487e8-e0f3-4525-a831-d24c30878dc3

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Oct 2, 2025

This PR adds Discord webhook notification support to Herald as the primary, flagship feature, enabling server administrators to receive player join notifications in Discord channels. Email notifications remain supported as a secondary, optional method, now extracted into its own dedicated class.

Features

  • Discord webhook integration (flagship): Send notifications to Discord channels when players join — configured and dispatched before email throughout the codebase
  • Flexible configuration: Enable/disable Discord notifications and configure webhook URL in config.yml; Discord section appears first in config
  • Async notifications: Discord and email messages are sent in a single async task, iterating notifiers sequentially to guarantee Discord-before-email ordering
  • Markdown formatting: Messages use Discord Markdown formatting for enhanced appearance: **PlayerName** joined the **ServerName** server
  • No external dependencies: Uses Java's built-in HttpURLConnection for HTTP requests
  • Robust HTTP handling: Connection/read timeouts (5s/10s) prevent thread starvation; connections are always closed via finally block
  • Complete JSON escaping: All control characters (including \b, \f, and chars below 0x20 as \uXXXX) are properly escaped to guarantee valid JSON payloads
  • Misconfiguration warnings: Server log warnings are emitted for missing/incomplete Discord or email configuration
  • Email notifications (optional, secondary): Extracted into a dedicated EmailNotifier class with conditional SMTP auth (only enabled when username is non-empty, supporting unauthenticated relay) and explicit emailSender validation

Discord-First Design

The entire codebase reflects Discord as the flagship feature:

  • config.yml: Discord section is listed first with a "flagship feature" comment; email defaults are empty to indicate it's optional
  • Herald.java: Discord notifier registered first, single async task iterates List<Notifier> sequentially so Discord always runs before email
  • plugin.yml: Description updated to "Sends Discord and email notifications when players join the server"
  • README.md: Project description, Features list, and Configuration sections all lead with Discord; email setup is under a separate "Email Server Setup (Optional)" section

Architecture

Each notification channel has its own dedicated class behind a common Notifier interface:

  • Notifier (interface): void notifyPlayerJoin(String playerName, String serverName) throws Exception
  • DiscordNotifier: Handles Discord webhook HTTP calls with timeouts, complete JSON escaping, and error handling
  • EmailNotifier: Handles SMTP email sending; SMTP auth is conditional on smtpUsername being set (supports unauthenticated relay); validates emailSender before use; accepts all config at construction time with a defensive copy of recipients
  • Herald.java: Stores a List<Notifier>, creates each conditionally based on config, and dispatches to all in a single async task on player join

Configuration

# Discord Configuration (flagship feature)
discord:
  enabled: false       # Set to true to enable Discord notifications
  webhook-url: ""      # Discord webhook URL for the channel

# Email Configuration (optional)
email-recipients: []
smtp:
  server: ""
  port: 587
  username: ""
  password: ""
  use-tls: true
email:
  sender: ""

Testing

  • 102 unit tests across 3 test files with a 1:1 correspondence between source and test classes:
    • DiscordNotifierTest.java: JSON escaping (all control characters, JsonPayloadTests), URL validation, message content, edge cases, security (JSON injection prevention)
    • EmailNotifierTest.java: Constructor behavior, validation (IllegalStateException for missing recipients/SMTP/sender), conditional auth, defensive copy, email format assertions
    • HeraldIntegrationTest.java: Cross-cutting Discord vs email format comparison, message formatting, player/server name handling, concurrency
  • GitHub Actions workflow: Automated testing via ./gradlew test --no-daemon on pushes to main/master/dev and all pull requests — scoped to prevent tests running twice on PR branch pushes
  • Gradle wrapper (gradlew, gradlew.bat, gradle/wrapper/gradle-wrapper.jar): Pinned to Gradle 8.8; wrapper JAR committed so contributors and CI use the same version without requiring a system Gradle install
  • Standalone test scripts: Bash (test-discord-webhook.sh) and Python (test-discord-webhook.py) scripts with explicit connect/read timeouts to test Discord webhooks without running a Minecraft server

Documentation

  • Updated README to lead with Discord as the flagship feature; email setup moved to an optional section
  • Updated README "Building from Source" section to document Gradle wrapper usage for Linux/Mac (./gradlew build, ./gradlew test) and Windows (gradlew.bat build)
  • Updated plugin.yml description to list Discord first
  • Added DISCORD_TESTING.md with comprehensive testing guide

Backward Compatibility

All changes are backward compatible. Discord is disabled by default and existing email functionality remains unchanged. Existing configurations will continue to work without modification.

Original prompt

This section details on the original issue you should resolve

<issue_title>Enable sending discord messages</issue_title>
<issue_description>It should be possible to enable sending discord messages to a particular channel when a player joins the server. This would be instead of or in addition to sending an email.</issue_description>

Comments on the Issue (you are @copilot in this section)

Fixes #3

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits October 2, 2025 23:45
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copilot AI changed the title [WIP] Enable sending discord messages Add Discord webhook notification support for player joins Oct 2, 2025
Copilot AI requested a review from dmccoystephenson October 2, 2025 23:51
Copy link
Copy Markdown
Member

@dmccoystephenson dmccoystephenson left a comment

Choose a reason for hiding this comment

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

@copilot Add unit tests and a simple GH action to run them

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 3, 2025

@copilot Add unit tests and a simple GH action to run them

Added comprehensive unit tests for DiscordNotifier with 18 test cases covering JSON escaping, validation, and edge cases. Also added GitHub Actions workflow to run tests automatically on push and PRs. (commit 8eb38e5)

Copilot AI requested a review from dmccoystephenson October 3, 2025 00:16
@dmccoystephenson dmccoystephenson marked this pull request as ready for review October 3, 2025 00:50
Copy link
Copy Markdown
Member

@dmccoystephenson dmccoystephenson left a comment

Choose a reason for hiding this comment

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

@copilot Expand unit tests

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 3, 2025

@copilot Expand unit tests

Expanded unit tests from 18 to 61 test methods (+239% coverage). Added:

  • Parameterized tests for JSON escaping with multiple input variations
  • 7 nested test class categories (Constructor, URL Validation, JSON Escaping, Message Content, Edge Cases, Security, Integration)
  • New HeraldIntegrationTest.java with 18 tests for message formatting, configuration, player/server names, concurrency, and performance
  • Security tests for JSON injection prevention
  • Performance tests ensuring operations complete in reasonable time
    (commit 8db0ae1)

Copy link
Copy Markdown
Member

@dmccoystephenson dmccoystephenson left a comment

Choose a reason for hiding this comment

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

@copilot CI checks are not running

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 3, 2025

@copilot CI checks are not running

Fixed! The workflow was configured to only run on pushes to main/master branches. Updated it to trigger on all pushes and pull requests. The CI checks should now run automatically. (commit 6772770)

Copy link
Copy Markdown
Member

@dmccoystephenson dmccoystephenson left a comment

Choose a reason for hiding this comment

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

@copilot CI checks are failing, no need to publish test results

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 6, 2026

@copilot expand unit tests and prevent CI check from running the unit tests twice

Done in commit 1ef2a36.

CI double-run fix: The push trigger is now scoped to branches: ['main', 'master', 'dev']. Previously it fired on every push to any branch — so a commit pushed to a PR branch triggered both a push run and a pull_request run. Now PR branch pushes only trigger via pull_request.

Expanded tests (58 → 78 total):

  • DiscordNotifierTest.java (42 → 54): Added JsonPayloadTests nested class with 9 new tests covering valid JSON structure, escaped quotes/backslashes/newlines not breaking the payload, all 6 named JSON control escapes, 7 parameterized low-control-char cases (\u0001\u001F), and printable ASCII passthrough
  • HeraldIntegrationTest.java (16 → 24): Added DiscordMessageFormatTests (exact format, safe content preservation, JSON injection prevention, default server name, both-names present) and NotificationOrderingTests (Discord vs email message differences, bold markdown presence)

@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot Extract email functionality into separate class (tests should also live in corresponding class)

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 6, 2026

@copilot Extract email functionality into separate class (tests should also live in corresponding class)

Done in commit cc7aa76.

EmailNotifier.java (new class, mirrors DiscordNotifier):

  • Constructor accepts all SMTP config + recipients list (defensive copy)
  • sendNotification(subject, body) throws IllegalStateException when recipients are empty or SMTP server is unset, and MessagingException for transport errors

Herald.java changes:

  • Removed all individual email fields (smtpServer, smtpPort, etc.) and the private sendEmail() method
  • loadConfiguration() now creates an EmailNotifier only when both recipients and SMTP server are configured (like Discord's pattern); logs a warning if config is partial
  • onPlayerJoin() calls emailNotifier.sendNotification() inside an async task, only if emailNotifier != null

EmailNotifierTest.java (new, 21 tests across 3 nested classes):

  • ConstructorTests — valid config, null/empty inputs, defensive copy, multiple recipients, TLS toggle
  • ValidationTestssendNotification throws IllegalStateException for empty/null recipients, null/empty SMTP server; recipients checked before SMTP; parameterized null+empty SMTP test
  • EmailFormatTests — plain-text subject (no markdown), Discord vs email format difference, body content, parameterized player names, Unicode names

HeraldIntegrationTest.java: removed testEmailSubjectIsPlainText (now in EmailNotifierTest); cross-cutting Discord vs email comparison tests remain.

There is now a clean 1:1 correspondence between source files and test files: DiscordNotifierDiscordNotifierTest, EmailNotifierEmailNotifierTest, with HeraldIntegrationTest covering cross-cutting concerns.

@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot Define Notifier interface to standardize notifier API, have existing notifiers implement the interface

…fier

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 6, 2026

@copilot Define Notifier interface to standardize notifier API, have existing notifiers implement the interface

Done in commit d89a7c5.

New Notifier interface (Notifier.java):

public interface Notifier {
    void notifyPlayerJoin(String playerName, String serverName) throws Exception;
}

Implementations:

  • DiscordNotifier implements NotifiernotifyPlayerJoin() formats the Discord Markdown message (**player** joined the **server** server) and delegates to sendMessage()
  • EmailNotifier implements NotifiernotifyPlayerJoin() formats the plain-text subject/body and delegates to sendNotification()

Herald.java now stores a List<Notifier> instead of individual fields. loadConfiguration() adds active notifiers to the list; onPlayerJoin() iterates it with a single polymorphic dispatch loop — eliminating the duplicated async blocks.

New tests (NotifyPlayerJoinTests nested class in each notifier test file, 6 tests each):

  • Interface contract check (assertInstanceOf(Notifier.class, ...))
  • Throws for missing configuration (null/empty URL; missing recipients/SMTP)
  • Correct message formatting verified via anonymous subclass that captures the formatted string without making network calls
  • Parameterized name combinations

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • hub.spigotmc.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED -XX:MaxMetaspaceSize=384m -XX:&#43;HeapDumpOnOutOfMemoryError -Xms256m -Xmx512m -Dfile.encoding=UTF-8 (dns block)
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=384m -XX:&#43;HeapDumpOnOutOfMemoryError -Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant -cp /home/REDACTED/.gradle/wrapper/dists/gradle-8.8-bin/dl7vupf4psengwqhwktix4v1/gradle-8.8/lib/gradle-launcher-8.8.jar -javaagent:/home/REDACTED/.gradle/wrapper/dists/gradle-8.8-bin/dl7vupf4psengwqhwktix4v1/gradle-8.8/lib/agents/gradle-instrumentation-agent-8.8.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 17 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +211 to +248
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'

exec "$JAVACMD" "$@"
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.

gradlew invokes -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar", but the repository doesn't include gradle/wrapper/gradle-wrapper.jar (only gradle-wrapper.properties). As committed, ./gradlew will fail for contributors/CI that try to use the wrapper. Either add the missing wrapper JAR (and typically gradlew.bat for Windows) or remove gradlew if the project intends to require a system Gradle install.

Suggested change
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
# At this point, instead of invoking the (missing) Gradle wrapper JAR, fall back to a
# system-installed Gradle, if available. This avoids relying on gradle/wrapper/gradle-wrapper.jar
# being present in the repository.
if command -v gradle >/dev/null 2>&1
then
exec gradle "$@"
fi
die "Gradle wrapper JAR is not available and 'gradle' is not installed on PATH. Please install Gradle and run 'gradle $*' manually."

Copilot uses AI. Check for mistakes.
"C:\\Path | C:\\\\Path",
"\\ | \\\\",
"\\\\ | \\\\\\\\",
"\\\\\\n | \\\\\\\\\\\\n"
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 last @CsvSource case appears to embed a real newline in the expected value ("\\\\\\\\\\n | \\\\\\\\\\\\n" ends with \n, which becomes an actual newline character in the Java string). That makes this assertion very likely to fail (the input string here ends with the literal character n, not a newline). Consider rewriting this case to avoid \n as a Java escape in the annotation value (or move it to a regular test method where you can construct the strings unambiguously).

Suggested change
"\\\\\\n | \\\\\\\\\\\\n"
"\\n | \\\\n"

Copilot uses AI. Check for mistakes.

# Send request
try:
with urllib.request.urlopen(req) as response:
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.

urllib.request.urlopen(req) is called without a timeout, so this test script can hang indefinitely on network stalls/DNS issues. Consider passing an explicit timeout (and optionally making it configurable) to match the plugin’s use of connect/read timeouts.

Suggested change
with urllib.request.urlopen(req) as response:
timeout_seconds = 10
with urllib.request.urlopen(req, timeout=timeout_seconds) as response:

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +112

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

System.out.println("Sending payload: " + jsonPayload);

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

int responseCode = connection.getResponseCode();
System.out.println("Response code: " + responseCode);

if (responseCode < 200 || responseCode >= 300) {
throw new IOException("Discord webhook returned error code: " + responseCode);
}
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 embedded Java snippet opens an HttpURLConnection but doesn't set connect/read timeouts or call disconnect() in a finally block. If Discord/DNS stalls, the script can hang and leave connections open. Consider mirroring the plugin behavior here (timeouts + disconnect) so the test script fails fast like the real notifier.

Suggested change
// Create JSON payload with the message content
String jsonPayload = String.format("{\"content\": \"%s\"}", escapeJson(content));
System.out.println("Sending payload: " + jsonPayload);
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
int responseCode = connection.getResponseCode();
System.out.println("Response code: " + responseCode);
if (responseCode < 200 || responseCode >= 300) {
throw new IOException("Discord webhook returned error code: " + responseCode);
}
// Set timeouts to avoid hanging indefinitely if Discord/DNS stalls
connection.setConnectTimeout(10_000); // 10 seconds
connection.setReadTimeout(15_000); // 15 seconds
// Create JSON payload with the message content
String jsonPayload = String.format("{\"content\": \"%s\"}", escapeJson(content));
System.out.println("Sending payload: " + jsonPayload);
try {
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
int responseCode = connection.getResponseCode();
System.out.println("Response code: " + responseCode);
if (responseCode < 200 || responseCode >= 300) {
throw new IOException("Discord webhook returned error code: " + responseCode);
}
} finally {
connection.disconnect();
}

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +76
for (Notifier notifier : notifiers) {
getServer().getScheduler().runTaskAsynchronously(this, () -> {
try {
notifier.notifyPlayerJoin(playerName, serverName);
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.

onPlayerJoin schedules one async task per notifier. Because these tasks can run concurrently, this does not guarantee Discord runs before email (even if Discord is first in notifiers), and it adds extra scheduler overhead per join. If ordering matters (per PR description), consider scheduling a single async task and iterating notifiers inside it sequentially, or otherwise explicitly ordering execution.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +83
props.put("mail.smtp.auth", "true");

if (useTLS) {
props.put("mail.smtp.starttls.enable", "true");
}

Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(smtpUsername, smtpPassword);
}
};

Session session = Session.getInstance(props, authenticator);

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.

mail.smtp.auth is hard-coded to true and an Authenticator is always provided, even though the config comments say username/password can be left empty for no authentication. This likely forces an auth attempt with empty credentials and can break SMTP setups that allow unauthenticated relay (e.g., the provided Docker mail server). Consider setting mail.smtp.auth based on whether smtpUsername is non-empty, and only supplying an Authenticator when auth is actually enabled.

Suggested change
props.put("mail.smtp.auth", "true");
if (useTLS) {
props.put("mail.smtp.starttls.enable", "true");
}
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(smtpUsername, smtpPassword);
}
};
Session session = Session.getInstance(props, authenticator);
boolean useAuth = smtpUsername != null && !smtpUsername.isEmpty();
props.put("mail.smtp.auth", useAuth ? "true" : "false");
if (useTLS) {
props.put("mail.smtp.starttls.enable", "true");
}
Session session;
if (useAuth) {
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(smtpUsername, smtpPassword);
}
};
session = Session.getInstance(props, authenticator);
} else {
session = Session.getInstance(props);
}

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +86
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(emailSender));

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.

message.setFrom(new InternetAddress(emailSender)) will throw (e.g., AddressException / NPE) if emailSender is null or empty, but sendNotification doesn't validate it. Since the default config sets email.sender: "", consider validating emailSender and throwing an IllegalStateException with a clear message (or skipping email setup earlier) before attempting to construct the message.

Copilot uses AI. Check for mistakes.
@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot address comments

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 6, 2026

@copilot address comments

Addressed all review comments in commit 0843eed:

  • Herald.java: Replaced one-task-per-notifier loop with a single async task that iterates notifiers sequentially — guarantees Discord-before-email ordering and reduces scheduler overhead
  • EmailNotifier.java: mail.smtp.auth is now conditional on smtpUsername being non-empty; Session is created without an Authenticator when auth is disabled (supports unauthenticated relay); added emailSender validation before use (throws IllegalStateException with a clear message)
  • gradlew: Added fallback to system gradle when gradle-wrapper.jar is absent; fixed $*$@ in error message
  • DiscordNotifierTest.java: Replaced the ambiguous CsvSource backslash+newline case (embedded Java \n making expected value unpredictable) with a dedicated @Test method using explicit string construction
  • test-discord-webhook.py: Added timeout=10 to urlopen
  • test-discord-webhook.sh: Added setConnectTimeout(10000) / setReadTimeout(15000) and wrapped the HTTP call in try/finally with connection.disconnect()
  • EmailNotifierTest.java: Added 4 new tests covering emailSender null/empty validation, SMTP-before-sender ordering, and notifyPlayerJoin throwing when sender is missing

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • hub.spigotmc.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED -XX:MaxMetaspaceSize=384m -XX:&#43;HeapDumpOnOutOfMemoryError -Xms256m -Xmx512m -Dfile.encoding=UTF-8 (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@dmccoystephenson
Copy link
Copy Markdown
Member

@copilot add gradle wrapper and document usage

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 6, 2026

@copilot add gradle wrapper and document usage

Done in commit 3de83b1.

  • gradle wrapper --gradle-version 8.8 was run to generate gradlew, gradlew.bat, and gradle/wrapper/gradle-wrapper.jar
  • .gitignore: Added !gradle/wrapper/gradle-wrapper.jar negation so the wrapper JAR is tracked despite the *.jar exclusion rule
  • CI workflow: Updated to use ./gradlew test --no-daemon instead of system gradle
  • README "Building from Source": Updated with wrapper usage for Linux/Mac and Windows:
    ./gradlew build       # build the plugin JAR
    ./gradlew test        # run unit tests
    gradlew.bat build     # Windows
    The wrapper auto-downloads Gradle 8.8 on first use.

@dmccoystephenson dmccoystephenson merged commit cc0da76 into dev Mar 7, 2026
3 of 4 checks passed
@dmccoystephenson dmccoystephenson deleted the copilot/fix-27b487e8-e0f3-4525-a831-d24c30878dc3 branch March 7, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants