Implement synchronous Discord webhook delivery in DiscordNotificationService #71

Closed
opened 2026-08-12 08:39:33 +02:00 by minimons · 0 comments
Owner

Background

Issue #70 refactored the notification framework into the transport-independent API under:

com.r35157.service.notification

The resulting DiscordNotificationService currently exists as an intentionally unimplemented stub:

com.r35157.service.notification.impl.discord.DiscordNotificationService

It already implements:

BoundNotificationService

and accepts an ΩNotificationMessageΩ, but valid messages currently result in UnsupportedOperationException.

The notification architecture intentionally separates consumers from transports. A future EvelynBurnerService, for example, may receive a configured BoundNotificationService, but it must not know whether notifications are delivered through Discord, Pushover, SMTP, Telegram, or another transport.

Discord-specific endpoints, HTTP configuration, JSON payloads and response handling must therefore remain encapsulated inside DiscordNotificationService.

Objective

Implement synchronous, text-only Discord incoming-webhook delivery in DiscordNotificationService.

After construction, the service must be fully configured and usable solely through:

BoundNotificationService notificationService = discordNotificationService;

notificationService.push(message);

Each push() call must require only the ΩNotificationMessageΩ.

Constructor and configuration

Replace the current raw URL constructor with a constructor equivalent to:

public DiscordNotificationService(
        @NotNull ΩRestEndpointΩ discordWebhookEndpoint,
        @NotNull Duration timeout
)

The precise formatting may follow existing project conventions, but these type semantics must be preserved.

ΩRestEndpointΩ resolves to URI and is already present in the authoritative DeTag configuration. It is the appropriate existing ValueTag for this endpoint.

The implementation must:

  • Store the fully bound Discord webhook endpoint.
  • Use the supplied timeout as both the HTTP connection timeout and the individual request timeout.
  • Build its internal HttpClient during construction.
  • Disable HTTP redirect following.
  • Remove the existing constructor accepting URL.
  • Not add a compatibility constructor accepting URL or raw String.
  • Not add or modify any ValueTag definitions.

Because ΩRestEndpointΩ resolves to URI, the .tjava source must retain the required java.net.URI import even if the backing type name is only introduced by DeTag generation.

Constructor validation

All constructor arguments must be validated before the service instance is usable and before any network activity can occur.

Reject the webhook endpoint with IllegalArgumentException if it is null or is not a structurally valid canonical Discord incoming-webhook endpoint.

Validation must require:

  • An absolute URI.
  • The https scheme.
  • The canonical discord.com host, compared case-insensitively.
  • No user-info component.
  • No fragment.
  • No pre-existing query string, because the service owns the execution query parameters.
  • No non-HTTPS port.
  • A Discord execute-webhook path containing non-blank webhook ID and token segments.
  • Either the canonical unversioned webhook path or a valid versioned Discord API webhook path.

The validation must be structural only. Construction must not perform a network request to test whether the webhook currently exists.

Reject the timeout with IllegalArgumentException if it is:

  • null
  • zero
  • negative

Validation exceptions, logs and diagnostic output must never contain the complete webhook endpoint or its token.

Notification-message validation

push() must continue to reject an ΩNotificationMessageΩ that is:

  • null
  • empty
  • blank

These cases must throw IllegalArgumentException.

Additionally, reject messages that exceed Discord’s 2,000-character content limit with IllegalArgumentException.

All message validation must occur before:

  • JSON serialization
  • HttpRequest construction
  • any HTTP or transport operation

A valid message must not be trimmed or otherwise rewritten. Its content must be preserved exactly when placed in the Discord JSON payload.

Discord HTTP request

For every valid push() call, DiscordNotificationService must synchronously execute one Discord webhook request.

The request must:

  • Use HTTP POST.
  • Use the configured Discord webhook endpoint.
  • Add the query parameter:
wait=true
  • Set Content-Type to application/json with UTF-8 encoding.
  • Set Accept to application/json.
  • Provide a valid Discord HTTP API User-Agent identifying the Nenjim client.
  • Apply the configured request timeout.
  • Send a JSON object containing the message in Discord’s content field.

The payload must be semantically equivalent to:

{
  "content": "the notification message"
}

Use an existing JSON library already present in the project, preferably Jackson, to produce the payload. Do not construct JSON by manually concatenating or escaping strings.

The webhook token embedded in the endpoint is the authentication mechanism. Do not add a bot Authorization header.

The call must use synchronous HttpClient.send(...). Do not use sendAsync(...), background executors or fire-and-forget delivery.

Using wait=true is required so Discord confirms that the message was saved instead of merely acknowledging the request without reporting a later save failure.

HTTP response handling

Any HTTP status in the 2xx range must be treated as successful delivery.

Any other status must cause push() to throw IOException, including:

  • redirects
  • client errors
  • invalid or deleted webhooks
  • invalid payload responses
  • 429 Too Many Requests
  • server errors

The IOException should include the HTTP status code and, when present and useful, Discord’s response body. It must not include the webhook URI or webhook token.

This issue must not implement automatic retry or sleeping after a 429 response. Rate limiting is reported synchronously as an IOException.

After a successful response, push() returns normally.

Communication and interruption failures

Network, connection, timeout, request-body preparation and other delivery failures must be reported through the existing synchronous IOException contract.

If HttpClient.send(...) throws InterruptedException, the implementation must:

  1. Restore the current thread’s interrupt status:
Thread.currentThread().interrupt();
  1. Throw an IOException describing that Discord delivery was interrupted.
  2. Preserve the original InterruptedException as the cause.

A valid Discord notification must no longer throw UnsupportedOperationException.

Secret handling

A Discord webhook endpoint contains a secret token and must be treated as a credential.

The implementation must not:

  • Log the complete webhook endpoint.
  • Include it in exception messages.
  • Print it during debugging or manual verification.
  • Add a real webhook endpoint to source code, examples, OpenSpec artifacts or repository configuration.
  • Follow redirects that could disclose the webhook token to another endpoint.

If live manual verification is performed, the webhook must be supplied through a non-versioned local mechanism and removed afterwards.

Existing inline example

Update the current private inline usage example in DiscordNotificationService as required by the new constructor signature, or remove it if it no longer provides useful compile-time documentation.

It must not contain a real Discord webhook, a credential-like placeholder, or a network-executable example.

This is not permission to add a test.

OpenSpec expectations

Create an active OpenSpec change for this issue using the change ID:

71-implement-discord-webhook-notification-delivery

The OpenSpec artifacts must include:

  • proposal
  • design
  • delta specification
  • implementation tasks

The delta specification must modify the existing notification-services capability. In particular, it must replace the current requirement that Discord delivery remains unimplemented with requirements and scenarios covering:

  • fully configured construction
  • webhook endpoint and timeout validation
  • notification-message validation
  • JSON webhook delivery
  • synchronous confirmation with wait=true
  • successful HTTP responses
  • non-successful HTTP responses
  • communication failures
  • interrupted delivery with restored interrupt status
  • webhook-secret protection

The OpenSpec design must record the constructor types, URI validation rules, timeout semantics, HTTP status policy and the decision not to implement retries or composition-root wiring.

Run strict OpenSpec validation before considering the implementation complete.

Keep the OpenSpec change active and unarchived after implementation. Do not sync or archive it unless explicitly requested separately.

Scope boundaries

This issue may change:

  • DiscordNotificationService
  • its inline documentation or compile-time example
  • the active OpenSpec change artifacts
  • unavoidable directly affected production references, if any exist

This issue must not:

  • Load a webhook from a configuration file.
  • Change NenjimHubImpl.startAutoRunProcesses().
  • Wire Discord into Nenjim, Evelyn or a Burner service.
  • Implement or modify EvelynBurnerService.
  • Change the notification API interfaces.
  • Change PushoverNotificationService.
  • Change SMTPNotificationService or SMTPDestination.
  • Change alarm-specific notification code.
  • Add or modify ValueTags or the DeTag configuration.
  • Add asynchronous delivery.
  • Add retries, rate-limit waiting, queues, persistence or an outbox.
  • Add Discord embeds, attachments, polls, components, TTS, thread selection, username overrides or avatar overrides.
  • Add bot authentication.
  • Add or modify unit tests.

Configuration loading and composition-root wiring must be handled in a separate follow-up issue after this service implementation has been reviewed.

Unit-test restriction

Do not add, generate or modify unit tests as part of this issue.

Existing tests may be compiled or executed unchanged as part of normal verification, but no test source files may be changed.

Do not introduce a mock HTTP server, test-only constructor or production seam solely for unit testing.

Verification

Verification must include:

  • Compilation of the generated main sources from the .tjava source.
  • Compilation of the existing unchanged test sources.
  • The repository’s normal assembly or build.
  • Strict validation of the active OpenSpec change.
  • Structural confirmation that the old URL constructor and UnsupportedOperationException delivery stub are gone.
  • Structural confirmation that no real webhook endpoint or token was added.
  • Review of the complete diff for changes outside the permitted scope.
  • Confirmation that no unit-test files were added or modified.

If a real webhook is explicitly available, one manual live text delivery may additionally be performed. Absence of a live webhook must not be worked around by committing credentials or weakening endpoint validation.

Acceptance criteria

  • DiscordNotificationService remains a BoundNotificationService.
  • Consumers still supply only ΩNotificationMessageΩ to push().
  • The constructor accepts an ΩRestEndpointΩ and a positive Duration.
  • The old raw URL constructor has been removed without a compatibility overload.
  • No new ValueTag is introduced.
  • Invalid endpoints and invalid timeouts fail during construction with IllegalArgumentException.
  • Null, empty, blank and over-2,000-character messages fail before JSON or HTTP work begins.
  • A valid message is serialized safely into the Discord content JSON field.
  • The service sends exactly one synchronous HTTP POST request per valid push().
  • The request uses wait=true, the configured timeout, valid JSON headers and a valid Discord API User-Agent.
  • HTTP redirects are not followed.
  • Successful 2xx responses return normally.
  • Every non-2xx response is reported as IOException.
  • HTTP 429 is reported without automatic retry.
  • Network and timeout failures use the IOException contract.
  • Interrupted delivery restores the thread’s interrupt status and throws IOException with the original interruption as its cause.
  • Valid delivery no longer throws UnsupportedOperationException.
  • The webhook endpoint and token never appear in logs or exception messages.
  • No real webhook credential is committed.
  • Pushover, SMTP, alarm code, Burner code and Nenjim composition-root wiring remain unchanged.
  • No unit tests are added or modified.
  • The active OpenSpec change passes strict validation and remains unarchived.
  • The project compiles and assembles successfully.
## Background Issue #70 refactored the notification framework into the transport-independent API under: ```text com.r35157.service.notification ``` The resulting `DiscordNotificationService` currently exists as an intentionally unimplemented stub: ```text com.r35157.service.notification.impl.discord.DiscordNotificationService ``` It already implements: ```java BoundNotificationService ``` and accepts an `ΩNotificationMessageΩ`, but valid messages currently result in `UnsupportedOperationException`. The notification architecture intentionally separates consumers from transports. A future `EvelynBurnerService`, for example, may receive a configured `BoundNotificationService`, but it must not know whether notifications are delivered through Discord, Pushover, SMTP, Telegram, or another transport. Discord-specific endpoints, HTTP configuration, JSON payloads and response handling must therefore remain encapsulated inside `DiscordNotificationService`. ## Objective Implement synchronous, text-only Discord incoming-webhook delivery in `DiscordNotificationService`. After construction, the service must be fully configured and usable solely through: ```java BoundNotificationService notificationService = discordNotificationService; notificationService.push(message); ``` Each `push()` call must require only the `ΩNotificationMessageΩ`. ## Constructor and configuration Replace the current raw `URL` constructor with a constructor equivalent to: ```java public DiscordNotificationService( @NotNull ΩRestEndpointΩ discordWebhookEndpoint, @NotNull Duration timeout ) ``` The precise formatting may follow existing project conventions, but these type semantics must be preserved. `ΩRestEndpointΩ` resolves to `URI` and is already present in the authoritative DeTag configuration. It is the appropriate existing ValueTag for this endpoint. The implementation must: * Store the fully bound Discord webhook endpoint. * Use the supplied timeout as both the HTTP connection timeout and the individual request timeout. * Build its internal `HttpClient` during construction. * Disable HTTP redirect following. * Remove the existing constructor accepting `URL`. * Not add a compatibility constructor accepting `URL` or raw `String`. * Not add or modify any ValueTag definitions. Because `ΩRestEndpointΩ` resolves to `URI`, the `.tjava` source must retain the required `java.net.URI` import even if the backing type name is only introduced by DeTag generation. ## Constructor validation All constructor arguments must be validated before the service instance is usable and before any network activity can occur. Reject the webhook endpoint with `IllegalArgumentException` if it is null or is not a structurally valid canonical Discord incoming-webhook endpoint. Validation must require: * An absolute URI. * The `https` scheme. * The canonical `discord.com` host, compared case-insensitively. * No user-info component. * No fragment. * No pre-existing query string, because the service owns the execution query parameters. * No non-HTTPS port. * A Discord execute-webhook path containing non-blank webhook ID and token segments. * Either the canonical unversioned webhook path or a valid versioned Discord API webhook path. The validation must be structural only. Construction must not perform a network request to test whether the webhook currently exists. Reject the timeout with `IllegalArgumentException` if it is: * null * zero * negative Validation exceptions, logs and diagnostic output must never contain the complete webhook endpoint or its token. ## Notification-message validation `push()` must continue to reject an `ΩNotificationMessageΩ` that is: * null * empty * blank These cases must throw `IllegalArgumentException`. Additionally, reject messages that exceed Discord’s 2,000-character content limit with `IllegalArgumentException`. All message validation must occur before: * JSON serialization * `HttpRequest` construction * any HTTP or transport operation A valid message must not be trimmed or otherwise rewritten. Its content must be preserved exactly when placed in the Discord JSON payload. ## Discord HTTP request For every valid `push()` call, `DiscordNotificationService` must synchronously execute one Discord webhook request. The request must: * Use HTTP `POST`. * Use the configured Discord webhook endpoint. * Add the query parameter: ```text wait=true ``` * Set `Content-Type` to `application/json` with UTF-8 encoding. * Set `Accept` to `application/json`. * Provide a valid Discord HTTP API `User-Agent` identifying the Nenjim client. * Apply the configured request timeout. * Send a JSON object containing the message in Discord’s `content` field. The payload must be semantically equivalent to: ```json { "content": "the notification message" } ``` Use an existing JSON library already present in the project, preferably Jackson, to produce the payload. Do not construct JSON by manually concatenating or escaping strings. The webhook token embedded in the endpoint is the authentication mechanism. Do not add a bot `Authorization` header. The call must use synchronous `HttpClient.send(...)`. Do not use `sendAsync(...)`, background executors or fire-and-forget delivery. Using `wait=true` is required so Discord confirms that the message was saved instead of merely acknowledging the request without reporting a later save failure. ## HTTP response handling Any HTTP status in the `2xx` range must be treated as successful delivery. Any other status must cause `push()` to throw `IOException`, including: * redirects * client errors * invalid or deleted webhooks * invalid payload responses * `429 Too Many Requests` * server errors The `IOException` should include the HTTP status code and, when present and useful, Discord’s response body. It must not include the webhook URI or webhook token. This issue must not implement automatic retry or sleeping after a `429` response. Rate limiting is reported synchronously as an `IOException`. After a successful response, `push()` returns normally. ## Communication and interruption failures Network, connection, timeout, request-body preparation and other delivery failures must be reported through the existing synchronous `IOException` contract. If `HttpClient.send(...)` throws `InterruptedException`, the implementation must: 1. Restore the current thread’s interrupt status: ```java Thread.currentThread().interrupt(); ``` 2. Throw an `IOException` describing that Discord delivery was interrupted. 3. Preserve the original `InterruptedException` as the cause. A valid Discord notification must no longer throw `UnsupportedOperationException`. ## Secret handling A Discord webhook endpoint contains a secret token and must be treated as a credential. The implementation must not: * Log the complete webhook endpoint. * Include it in exception messages. * Print it during debugging or manual verification. * Add a real webhook endpoint to source code, examples, OpenSpec artifacts or repository configuration. * Follow redirects that could disclose the webhook token to another endpoint. If live manual verification is performed, the webhook must be supplied through a non-versioned local mechanism and removed afterwards. ## Existing inline example Update the current private inline usage example in `DiscordNotificationService` as required by the new constructor signature, or remove it if it no longer provides useful compile-time documentation. It must not contain a real Discord webhook, a credential-like placeholder, or a network-executable example. This is not permission to add a test. ## OpenSpec expectations Create an active OpenSpec change for this issue using the change ID: ```text 71-implement-discord-webhook-notification-delivery ``` The OpenSpec artifacts must include: * proposal * design * delta specification * implementation tasks The delta specification must modify the existing `notification-services` capability. In particular, it must replace the current requirement that Discord delivery remains unimplemented with requirements and scenarios covering: * fully configured construction * webhook endpoint and timeout validation * notification-message validation * JSON webhook delivery * synchronous confirmation with `wait=true` * successful HTTP responses * non-successful HTTP responses * communication failures * interrupted delivery with restored interrupt status * webhook-secret protection The OpenSpec design must record the constructor types, URI validation rules, timeout semantics, HTTP status policy and the decision not to implement retries or composition-root wiring. Run strict OpenSpec validation before considering the implementation complete. Keep the OpenSpec change active and unarchived after implementation. Do not sync or archive it unless explicitly requested separately. ## Scope boundaries This issue may change: * `DiscordNotificationService` * its inline documentation or compile-time example * the active OpenSpec change artifacts * unavoidable directly affected production references, if any exist This issue must not: * Load a webhook from a configuration file. * Change `NenjimHubImpl.startAutoRunProcesses()`. * Wire Discord into Nenjim, Evelyn or a Burner service. * Implement or modify `EvelynBurnerService`. * Change the notification API interfaces. * Change `PushoverNotificationService`. * Change `SMTPNotificationService` or `SMTPDestination`. * Change alarm-specific notification code. * Add or modify ValueTags or the DeTag configuration. * Add asynchronous delivery. * Add retries, rate-limit waiting, queues, persistence or an outbox. * Add Discord embeds, attachments, polls, components, TTS, thread selection, username overrides or avatar overrides. * Add bot authentication. * Add or modify unit tests. Configuration loading and composition-root wiring must be handled in a separate follow-up issue after this service implementation has been reviewed. ## Unit-test restriction Do not add, generate or modify unit tests as part of this issue. Existing tests may be compiled or executed unchanged as part of normal verification, but no test source files may be changed. Do not introduce a mock HTTP server, test-only constructor or production seam solely for unit testing. ## Verification Verification must include: * Compilation of the generated main sources from the `.tjava` source. * Compilation of the existing unchanged test sources. * The repository’s normal assembly or build. * Strict validation of the active OpenSpec change. * Structural confirmation that the old `URL` constructor and `UnsupportedOperationException` delivery stub are gone. * Structural confirmation that no real webhook endpoint or token was added. * Review of the complete diff for changes outside the permitted scope. * Confirmation that no unit-test files were added or modified. If a real webhook is explicitly available, one manual live text delivery may additionally be performed. Absence of a live webhook must not be worked around by committing credentials or weakening endpoint validation. ## Acceptance criteria * `DiscordNotificationService` remains a `BoundNotificationService`. * Consumers still supply only `ΩNotificationMessageΩ` to `push()`. * The constructor accepts an `ΩRestEndpointΩ` and a positive `Duration`. * The old raw `URL` constructor has been removed without a compatibility overload. * No new ValueTag is introduced. * Invalid endpoints and invalid timeouts fail during construction with `IllegalArgumentException`. * Null, empty, blank and over-2,000-character messages fail before JSON or HTTP work begins. * A valid message is serialized safely into the Discord `content` JSON field. * The service sends exactly one synchronous HTTP `POST` request per valid `push()`. * The request uses `wait=true`, the configured timeout, valid JSON headers and a valid Discord API `User-Agent`. * HTTP redirects are not followed. * Successful `2xx` responses return normally. * Every non-`2xx` response is reported as `IOException`. * HTTP `429` is reported without automatic retry. * Network and timeout failures use the `IOException` contract. * Interrupted delivery restores the thread’s interrupt status and throws `IOException` with the original interruption as its cause. * Valid delivery no longer throws `UnsupportedOperationException`. * The webhook endpoint and token never appear in logs or exception messages. * No real webhook credential is committed. * Pushover, SMTP, alarm code, Burner code and Nenjim composition-root wiring remain unchanged. * No unit tests are added or modified. * The active OpenSpec change passes strict validation and remains unarchived. * The project compiles and assembles successfully.
minimons added the enhancement label 2026-08-12 08:39:33 +02:00
minimons self-assigned this 2026-08-12 08:39:33 +02:00
minimons added this to the AssetAZ project 2026-08-12 08:39:33 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: r35157/com_r35157_nenjim-hubd-impl_ref#71