71: Implement Discord webhook notification delivery
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-12
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
## Context
|
||||
|
||||
See `proposal.md` for motivation. `DiscordNotificationService` is the only affected production class and currently validates only null/blank messages before throwing `UnsupportedOperationException`. The existing Jackson dependency can safely serialize the payload; no notification API, caller, or composition-root change is required.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Construct a fully bound synchronous Discord webhook service from `ΩRestEndpointΩ` and a positive `Duration`.
|
||||
- Validate the endpoint locally, preserve message text exactly, and make successful response handling explicit.
|
||||
- Keep the webhook URI confidential in all validation and delivery paths.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Configuration loading or Nenjim composition-root wiring.
|
||||
- Retries, rate-limit waiting, asynchronous execution, or non-text Discord features.
|
||||
- Changes to the notification API, Pushover, SMTP, alarms, Evelyn, or tests.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Strict local URI validation before client construction
|
||||
|
||||
The constructor validates the URI shape with URI components and a fixed path grammar: `https://discord.com/api/webhooks/{id}/{token}` or `https://discord.com/api/v{version}/webhooks/{id}/{token}`. Versioned paths accept only Discord API versions 6 through 10, which are currently non-discontinued; the host is case-insensitive and all token-bearing endpoint values are absent from diagnostics. A network probe is rejected because construction must be side-effect free and valid webhooks can change remotely.
|
||||
|
||||
### One synchronous JSON request per valid push
|
||||
|
||||
The service uses a construction-time `HttpClient` configured with the supplied connect timeout and `Redirect.NEVER`. Every valid message is serialized with Jackson into `content`, then posted with `wait=true`, the same request timeout, JSON headers, and a `DiscordBot (url, version)` User-Agent identifying Nenjim. This preserves the BoundNotificationService contract and lets Discord confirm persistence. Manual JSON escaping and asynchronous delivery are rejected.
|
||||
|
||||
### Failure policy preserves secrets and interruption
|
||||
|
||||
Only 2xx is successful. Other statuses become `IOException` containing only status and useful body. I/O failures are wrapped with non-sensitive context; interruption restores the flag and is the cause of an `IOException`. The service neither retries nor waits after 429, so each valid `push()` owns exactly one send attempt.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Webhook endpoint is a credential]** → Never log or interpolate it into exceptions; redirect following is disabled.
|
||||
- **[Strict URI validation could reject a future Discord route]** → Accept canonical unversioned and conventional versioned execute-webhook paths only; future route support requires an explicit spec change.
|
||||
- **[No automated tests by issue instruction]** → Compile generated sources and unchanged tests, assemble, validate OpenSpec, and perform structural and diff checks without a live webhook.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Replace the URL-based stub constructor and delivery path in place.
|
||||
2. Update the Discord requirement in the active capability delta.
|
||||
3. Verify compilation, assembly, strict OpenSpec validation, and absence of old-stub patterns or credentials.
|
||||
|
||||
Rollback is a single-class and active-change revert; no consumer migration is needed.
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
## Why
|
||||
|
||||
The transport-independent notification API already exposes a bound Discord service, but its valid-message path is still a stub. Completing synchronous Discord incoming-webhook delivery makes that service usable without exposing Discord details to consumers.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Replace the Discord stub with synchronous JSON webhook delivery through the existing bound notification-service API.
|
||||
- Require a structurally valid canonical Discord webhook URI and a positive caller-supplied timeout at construction.
|
||||
- Validate messages, preserve their content exactly, use `wait=true`, and report delivery failures through `IOException` without retrying.
|
||||
- Protect webhook credentials from diagnostics and disable redirect following.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
None.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `notification-services`: Replaces the unimplemented Discord-delivery requirement with fully bound synchronous webhook-delivery, validation, response, interruption, and secret-protection behavior.
|
||||
|
||||
## Impact
|
||||
|
||||
Only `DiscordNotificationService` and the active OpenSpec change are affected. The existing Jackson dependency is reused. No composition-root wiring, other notification transport, or test code changes.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Discord webhook delivery
|
||||
The Discord notification service SHALL remain a `BoundNotificationService` that is fully configured at construction from a non-null structurally valid canonical Discord incoming-webhook `ΩRestEndpointΩ` and a non-null positive `Duration`. It SHALL make no construction-time network request, follow no HTTP redirects, and SHALL NOT expose a raw `URL` or raw `String` constructor.
|
||||
|
||||
The endpoint SHALL be an absolute HTTPS URI with a case-insensitive `discord.com` host, no user-info, fragment, query, or non-HTTPS port, and either an unversioned `/api/webhooks/{id}/{token}` path or a versioned `/api/v{version}/webhooks/{id}/{token}` path using a non-discontinued Discord API version from 6 through 10, with non-blank ID and token segments. Constructor validation diagnostics SHALL NOT disclose the endpoint or token.
|
||||
|
||||
Every public `push(...)` entry point SHALL reject a null, empty, blank, or over-2,000-character `ΩNotificationMessageΩ` with `IllegalArgumentException` before JSON serialization, request construction, or transport work. For every valid message, the service SHALL synchronously send exactly one POST to the configured endpoint with `wait=true`, the configured request timeout, JSON UTF-8 content and accept headers, a `DiscordBot (url, version)` User-Agent identifying Nenjim, and a safely serialized JSON object whose `content` preserves the message exactly. The webhook token SHALL be the only authentication mechanism.
|
||||
|
||||
Every 2xx HTTP response SHALL return normally. Every non-2xx response, including redirects and 429 responses, and every communication or request-preparation failure SHALL cause `IOException` without retrying, waiting, or sending another request. Response failures SHALL include the HTTP status and useful response body when available, without disclosing the endpoint or token. When synchronous HTTP delivery is interrupted, the service SHALL restore the thread interrupt flag and throw an `IOException` retaining the `InterruptedException` as its cause.
|
||||
|
||||
#### Scenario: Fully configured construction
|
||||
- **WHEN** a caller constructs the service with a canonical Discord webhook endpoint and positive timeout
|
||||
- **THEN** it receives a bound notification service without a network request
|
||||
|
||||
#### Scenario: Invalid endpoint or timeout is rejected
|
||||
- **WHEN** a caller supplies a null or structurally invalid endpoint, or a null, zero, or negative timeout
|
||||
- **THEN** construction throws `IllegalArgumentException` without disclosing the endpoint token
|
||||
|
||||
#### Scenario: Invalid notification message is rejected before delivery
|
||||
- **WHEN** a caller pushes a null, empty, blank, or over-2,000-character message
|
||||
- **THEN** the service throws `IllegalArgumentException` before serialization or HTTP work
|
||||
|
||||
#### Scenario: Valid message is synchronously confirmed
|
||||
- **WHEN** a caller pushes a valid notification message and Discord returns a 2xx response
|
||||
- **THEN** the service sends one synchronous JSON POST with `wait=true` and returns normally
|
||||
|
||||
#### Scenario: Discord rejects delivery
|
||||
- **WHEN** Discord returns a non-2xx response, including 429 or a redirect
|
||||
- **THEN** the service throws `IOException` containing the status and does not retry or follow the redirect
|
||||
|
||||
#### Scenario: Delivery communication fails
|
||||
- **WHEN** preparing or sending the webhook request fails
|
||||
- **THEN** the service throws `IOException` without exposing the webhook endpoint or token
|
||||
|
||||
#### Scenario: Delivery is interrupted
|
||||
- **WHEN** synchronous webhook delivery is interrupted
|
||||
- **THEN** the service restores the thread interrupt status and throws `IOException` with the interruption as its cause
|
||||
|
||||
#### Scenario: Webhook secret remains protected
|
||||
- **WHEN** validation or delivery reports an error
|
||||
- **THEN** no log or exception message includes the complete webhook endpoint or token
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Discord delivery remains unimplemented
|
||||
**Reason**: Discord incoming-webhook delivery is now implemented synchronously.
|
||||
**Migration**: Construct `DiscordNotificationService` with a canonical Discord webhook `ΩRestEndpointΩ` and a positive `Duration`, then use it through `BoundNotificationService`.
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
## 1. Discord Webhook Delivery
|
||||
|
||||
- [x] 1.1 Replace the URL stub constructor with validated `ΩRestEndpointΩ` and positive `Duration` construction, including connect-timeout and no-redirect client configuration.
|
||||
- [x] 1.2 Implement direct notification-message validation and one synchronous Jackson JSON webhook POST with `wait=true`, headers, request timeout, and exact message content.
|
||||
- [x] 1.3 Implement 2xx success handling and non-sensitive `IOException` handling for non-2xx, communication, timeout, and interrupted delivery without retries.
|
||||
- [x] 1.4 Remove or update the private inline example so it cannot carry a credential or perform a network-executable example.
|
||||
|
||||
## 2. Verification
|
||||
|
||||
- [x] 2.1 Compile generated main sources and existing unchanged test sources, then run the normal assembly/build.
|
||||
- [x] 2.2 Run strict validation of the active OpenSpec change and structural checks for the old URL constructor, stub, credentials, and prohibited scope changes.
|
||||
- [x] 2.3 Review the complete diff, confirm no test or generated source changes, and commit the scoped implementation.
|
||||
Reference in New Issue
Block a user