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.
|
||||
@@ -61,12 +61,46 @@ An SMTP destination SHALL contain both an `ΩEmailAddressΩ` and a fixed `ΩEmai
|
||||
- **WHEN** a valid message is pushed through a valid SMTP binding
|
||||
- **THEN** the service reports that SMTP delivery is not implemented
|
||||
|
||||
### Requirement: Discord delivery remains unimplemented
|
||||
The Discord stub SHALL implement `BoundNotificationService`, accept `ΩNotificationMessageΩ` directly, and SHALL NOT implement webhook delivery as part of this capability.
|
||||
### 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.
|
||||
|
||||
#### Scenario: Valid Discord delivery remains unimplemented
|
||||
- **WHEN** a valid message is pushed through the configured Discord service
|
||||
- **THEN** the service reports that Discord delivery is not implemented without performing webhook delivery
|
||||
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.
|
||||
|
||||
+227
-18
@@ -1,49 +1,258 @@
|
||||
package com.r35157.service.notification.impl.discord;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.r35157.service.notification.BoundNotificationService;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Placeholder for bound Discord webhook notification delivery.
|
||||
* Synchronously delivers bound notification messages through a Discord
|
||||
* incoming webhook.
|
||||
*/
|
||||
public class DiscordNotificationService implements BoundNotificationService {
|
||||
|
||||
/**
|
||||
* Creates a service bound to a Discord webhook URL.
|
||||
* Creates a service bound to a canonical Discord incoming-webhook endpoint.
|
||||
*
|
||||
* @param discordWebhookUrl Discord webhook URL
|
||||
* <p>The supplied user agent must use Discord's canonical format:
|
||||
* {@code DiscordBot (<client-url>, <version-number>)}. For example:
|
||||
* {@code DiscordBot (https://example.com/my-project, 1.0)}.
|
||||
*
|
||||
* <p>The client URL and version must identify the application or library
|
||||
* configuring this service, rather than this transport implementation.
|
||||
* Discord may reject or block HTTP requests that do not provide a valid
|
||||
* user agent.
|
||||
*
|
||||
* @param discordWebhookEndpoint Discord incoming-webhook endpoint
|
||||
* @param userAgent complete Discord HTTP user agent in the canonical
|
||||
* {@code DiscordBot (<client-url>, <version-number>)} format
|
||||
* @param timeout connection and individual request timeout
|
||||
* @throws IllegalArgumentException if the endpoint is not a structurally
|
||||
* valid canonical Discord incoming-webhook
|
||||
* endpoint, if the user agent does not use
|
||||
* the required Discord format, or if the
|
||||
* timeout is null, zero, or negative
|
||||
*/
|
||||
public DiscordNotificationService(URL discordWebhookUrl) {
|
||||
this.httpClient = HttpClient.newHttpClient();
|
||||
this.discordWebhookUrl = discordWebhookUrl;
|
||||
public DiscordNotificationService(
|
||||
@NotNull ΩRestEndpointΩ discordWebhookEndpoint,
|
||||
@NotNull ΩUserAgentΩ userAgent,
|
||||
@NotNull Duration timeout
|
||||
) {
|
||||
this.discordWebhookEndpoint = validateDiscordWebhookEndpoint(discordWebhookEndpoint);
|
||||
this.userAgent = validateDiscordUserAgent(userAgent);
|
||||
this.timeout = validateTimeout(timeout);
|
||||
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(this.timeout)
|
||||
.followRedirects(HttpClient.Redirect.NEVER)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push(@NotNull ΩNotificationMessageΩ message) throws IOException {
|
||||
validateDiscordMessage(message);
|
||||
throw new UnsupportedOperationException("Not implemented yet!");
|
||||
|
||||
HttpRequest request = createRequest(message);
|
||||
HttpResponse<String> response;
|
||||
|
||||
try {
|
||||
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
} catch(InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Discord delivery was interrupted", ie);
|
||||
} catch(IOException ioe) {
|
||||
throw new IOException("Discord delivery failed", ioe);
|
||||
}
|
||||
|
||||
private static void validateDiscordMessage(ΩNotificationMessageΩ message) {
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new IOException(createHttpFailureMessage(response));
|
||||
}
|
||||
}
|
||||
|
||||
private HttpRequest createRequest(ΩNotificationMessageΩ message) throws IOException {
|
||||
try {
|
||||
String payload = OBJECT_MAPPER.writeValueAsString(Map.of("content", message));
|
||||
|
||||
return HttpRequest.newBuilder()
|
||||
.uri(createExecutionEndpoint())
|
||||
.timeout(timeout)
|
||||
.header("Content-Type", "application/json; charset=UTF-8")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", userAgent)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(
|
||||
payload,
|
||||
StandardCharsets.UTF_8
|
||||
))
|
||||
.build();
|
||||
} catch(JsonProcessingException jpe) {
|
||||
throw new IOException("Failed to serialize Discord notification message", jpe);
|
||||
} catch(RuntimeException re) {
|
||||
throw new IOException("Failed to prepare Discord delivery request", re);
|
||||
}
|
||||
}
|
||||
|
||||
private ΩRestEndpointΩ createExecutionEndpoint() {
|
||||
return URI.create(discordWebhookEndpoint + "?wait=true");
|
||||
}
|
||||
|
||||
private String createHttpFailureMessage(HttpResponse<String> response) {
|
||||
String responseBody = redactWebhookSecret(response.body());
|
||||
if (responseBody == null || responseBody.isBlank()) {
|
||||
return "Discord delivery failed: HTTP " + response.statusCode();
|
||||
}
|
||||
return "Discord delivery failed: HTTP " + response.statusCode()
|
||||
+ ": " + responseBody;
|
||||
}
|
||||
|
||||
private String redactWebhookSecret(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value
|
||||
.replace(discordWebhookEndpoint.toString(), "<redacted>")
|
||||
.replace(webhookToken(), "<redacted>");
|
||||
}
|
||||
|
||||
private String webhookToken() {
|
||||
String path = discordWebhookEndpoint.getPath();
|
||||
return path.substring(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
private static ΩRestEndpointΩ validateDiscordWebhookEndpoint(final ΩRestEndpointΩ endpoint) {
|
||||
if (endpoint == null
|
||||
|| !endpoint.isAbsolute()
|
||||
|| !"https".equalsIgnoreCase(endpoint.getScheme())
|
||||
|| endpoint.getHost() == null
|
||||
|| !"discord.com".equalsIgnoreCase(endpoint.getHost())
|
||||
|| endpoint.getUserInfo() != null
|
||||
|| endpoint.getFragment() != null
|
||||
|| endpoint.getQuery() != null
|
||||
|| (endpoint.getPort() != -1 && endpoint.getPort() != 443)
|
||||
|| !isCanonicalWebhookPath(endpoint.getPath()))
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid Discord webhook endpoint");
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private static boolean isCanonicalWebhookPath(String path) {
|
||||
if (path == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] segments = path.split("/", -1);
|
||||
if (segments.length == 5) {
|
||||
return isUnversionedWebhookPath(segments);
|
||||
}
|
||||
if (segments.length == 6) {
|
||||
return isVersionedWebhookPath(segments);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isUnversionedWebhookPath(String[] segments) {
|
||||
return segments[0].isEmpty()
|
||||
&& "api".equals(segments[1])
|
||||
&& "webhooks".equals(segments[2])
|
||||
&& !segments[3].isBlank()
|
||||
&& !segments[4].isBlank();
|
||||
}
|
||||
|
||||
private static boolean isVersionedWebhookPath(String[] segments) {
|
||||
return segments[0].isEmpty()
|
||||
&& "api".equals(segments[1])
|
||||
&& isSupportedDiscordApiVersion(segments[2])
|
||||
&& "webhooks".equals(segments[3])
|
||||
&& !segments[4].isBlank()
|
||||
&& !segments[5].isBlank();
|
||||
}
|
||||
|
||||
private static boolean isSupportedDiscordApiVersion(String version) {
|
||||
return "v6".equals(version)
|
||||
|| "v7".equals(version)
|
||||
|| "v8".equals(version)
|
||||
|| "v9".equals(version)
|
||||
|| "v10".equals(version);
|
||||
}
|
||||
|
||||
private static Duration validateTimeout(final Duration timeout) {
|
||||
if (timeout == null || timeout.isZero() || timeout.isNegative()) {
|
||||
throw new IllegalArgumentException("Discord timeout must be positive");
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
private static void validateDiscordMessage(final ΩNotificationMessageΩ message) {
|
||||
if (message == null || message.isBlank()) {
|
||||
throw new IllegalArgumentException("Notification message cannot be blank");
|
||||
}
|
||||
if (message.length() > DISCORD_CONTENT_LIMIT) {
|
||||
throw new IllegalArgumentException(
|
||||
"Notification message exceeds Discord's 2,000-character limit"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void test() throws IOException {
|
||||
URL discordWebhookUrl = null;
|
||||
|
||||
BoundNotificationService notificationService =
|
||||
new DiscordNotificationService(discordWebhookUrl);
|
||||
|
||||
ΩNotificationMessageΩ message = "Hello World!";
|
||||
notificationService.push(message);
|
||||
private static ΩUserAgentΩ validateDiscordUserAgent(final ΩUserAgentΩ userAgent) {
|
||||
if (userAgent == null) {
|
||||
throw new IllegalArgumentException("Discord user agent cannot be null");
|
||||
}
|
||||
|
||||
Matcher matcher = DISCORD_USER_AGENT_PATTERN.matcher(userAgent);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Discord user agent must use the format "
|
||||
+ "'DiscordBot (<client-url>, <version-number>)'"
|
||||
);
|
||||
}
|
||||
|
||||
URI clientUri;
|
||||
try {
|
||||
clientUri = URI.create(matcher.group("clientUrl"));
|
||||
} catch(IllegalArgumentException iae) {
|
||||
throw new IllegalArgumentException(
|
||||
"Discord user agent contains an invalid client URL",
|
||||
iae
|
||||
);
|
||||
}
|
||||
|
||||
if (!clientUri.isAbsolute()
|
||||
|| clientUri.getHost() == null
|
||||
|| (!"https".equalsIgnoreCase(clientUri.getScheme())
|
||||
&& !"http".equalsIgnoreCase(clientUri.getScheme()))
|
||||
|| clientUri.getUserInfo() != null
|
||||
|| clientUri.getFragment() != null)
|
||||
{
|
||||
throw new IllegalArgumentException(
|
||||
"Discord user agent must contain an absolute HTTP(S) client URL"
|
||||
);
|
||||
}
|
||||
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
private static final int DISCORD_CONTENT_LIMIT = 2_000;
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private static final Pattern DISCORD_USER_AGENT_PATTERN = Pattern.compile(
|
||||
"\\ADiscordBot \\((?<clientUrl>[^,\\r\\n]+), "
|
||||
+ "[0-9]+(?:\\.[0-9]+)*\\)\\z"
|
||||
);
|
||||
|
||||
private final ΩUserAgentΩ userAgent; // Example: "DiscordBot (https://git.r35157.com/r35157/com_r35157_nenjim-hubd-impl_ref, 0.1)";
|
||||
private final ΩRestEndpointΩ discordWebhookEndpoint;
|
||||
private final Duration timeout;
|
||||
private final HttpClient httpClient;
|
||||
private final URL discordWebhookUrl;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user