Files
com_r35157_nenjim-hubd-impl…/openspec/specs/notification-services/spec.md
T

8.4 KiB

notification-services Specification

Purpose

Defines transport-independent notification services whose delivery configuration is bound before consumers submit semantically tagged text messages.

Requirements

Requirement: Transport-independent bound notification contract

The notification API SHALL provide a non-generic bound service under com.r35157.service.notification whose only delivery input is an ΩNotificationMessageΩ and whose synchronous failures use IOException. A consumer using an already bound service SHALL NOT need a transport-specific message type or an implementation-package dependency.

Scenario: Bound consumer sends a message

  • WHEN a consumer receives a configured BoundNotificationService
  • THEN it can call push(message) using only an ΩNotificationMessageΩ

Scenario: Delivery failure is reported synchronously

  • WHEN a bound transport encounters a delivery failure
  • THEN push(message) reports the failure through its IOException contract

Requirement: Addressed services bind destination configuration

The notification API SHALL provide an AddressedNotificationService generic only over a NotificationDestination type. Binding a destination SHALL produce a BoundNotificationService that captures the destination so later calls require only an ΩNotificationMessageΩ.

Scenario: Destination is bound once

  • WHEN a destination is passed to an addressed notification service's binding operation
  • THEN the returned bound service sends subsequent messages to that captured destination without requiring it again

Requirement: Notification message validation

Every public notification-service push(...) entry point SHALL reject a null, empty, or blank ΩNotificationMessageΩ with IllegalArgumentException before attempting any network or transport operation.

Scenario: Null message is rejected

  • WHEN a caller pushes a null notification message
  • THEN the service throws IllegalArgumentException before delivery is attempted

Scenario: Empty or blank message is rejected

  • WHEN a caller pushes an empty or whitespace-only notification message
  • THEN the service throws IllegalArgumentException before delivery is attempted

Requirement: Pushover delivery remains functional

The Pushover implementation SHALL accept ΩNotificationMessageΩ directly, preserve its existing synchronous HTTP delivery behavior, and expose it as PushoverNotificationService implementing BoundNotificationService.

Scenario: Valid Pushover message is submitted

  • WHEN a caller pushes a valid message through a configured Pushover notification service
  • THEN the service submits the same message through the existing Pushover API request flow

Scenario: Pushover rejects delivery

  • WHEN Pushover returns a non-successful HTTP response or communication fails
  • THEN the service reports the delivery failure as an IOException

Requirement: SMTP binding captures address and subject

An SMTP destination SHALL contain both an ΩEmailAddressΩ and a fixed ΩEmailSubjectΩ. SMTP SHALL support binding these two values into a BoundNotificationService, and SHALL reject a null, empty, or blank address or subject with IllegalArgumentException when the destination is created.

Scenario: SMTP service is fully bound

  • WHEN a caller binds a valid email address and subject
  • THEN the returned bound service requires only an ΩNotificationMessageΩ for each push

Scenario: Invalid SMTP binding is rejected

  • WHEN a caller creates or requests an SMTP binding with a null, empty, or blank email address or subject
  • THEN the operation throws IllegalArgumentException

Scenario: Valid SMTP delivery remains unimplemented

  • WHEN a valid message is pushed through a valid SMTP binding
  • THEN the service reports that SMTP delivery is not implemented

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

Requirement: Obsolete notification API is removed

The notification framework SHALL reside exclusively under com.r35157.service.notification. It SHALL NOT retain compatibility aliases under com.r35157.libs.notification, notifier-named public interfaces or implementations, the NotificationMessage marker, or transport-specific message wrapper types.

Scenario: Production code uses the new service API

  • WHEN the production source tree is inspected after migration
  • THEN notification framework code and consumers reference only the new package and service-oriented types

Scenario: Removed types are unavailable

  • WHEN downstream code attempts to import an old notifier or message-wrapper type
  • THEN that type is no longer part of the source or compiled API