This commit is contained in:
anthorob committed 2026-09-10 08:32:16 -04:00
1 parent 868dbea8df
commit a507b471b4
13 files changed
+33 -95

No files matched your search

+3 -3
View File
@@ -90,7 +90,7 @@ and exposes type-safe option access, e.g. `getOption(name, resolver)`. Concrete
specific JDA event to this API. specific JDA event to this API.
| Class | Wraps | Notes | | Class | Wraps | Notes |
|---|---|---| |-------------------------|----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| `SlashCommandContext` | `SlashCommandInteractionEvent` | Options already parsed by Discord; maps `OptionMapping` → `SlashCommandOption`. Interaction-based replies. | | `SlashCommandContext` | `SlashCommandInteractionEvent` | Options already parsed by Discord; maps `OptionMapping` → `SlashCommandOption`. Interaction-based replies. |
| `MessageCommandContext` | `MessageReceivedEvent` (prefix) | Contains the **manual parser** that turns the raw message string into `CommandOption`s (mentions, integers, booleans, …). Message-based replies. | | `MessageCommandContext` | `MessageReceivedEvent` (prefix) | Contains the **manual parser** that turns the raw message string into `CommandOption`s (mentions, integers, booleans, …). Message-based replies. |
| `MessageMenuContext` | `MessageContextInteractionEvent` | "Apps → …" action on a message; exposes the target `Message`. | | `MessageMenuContext` | `MessageContextInteractionEvent` | "Apps → …" action on a message; exposes the target `Message`. |
@@ -107,7 +107,7 @@ Abstract base for the [task flow](tasks.md). Two things distinguish it:
- **`contextData` map** — a small state bag used to carry values between steps of a multi-step flow. - **`contextData` map** — a small state bag used to carry values between steps of a multi-step flow.
| Class | Wraps | `getId()` derived from | | Class | Wraps | `getId()` derived from |
|---|---|---| |----------------------|------------------------------------|------------------------------------------------------------|
| `ButtonTaskContext` | `ButtonInteractionEvent` | `getComponentId()` of the clicked button | | `ButtonTaskContext` | `ButtonInteractionEvent` | `getComponentId()` of the clicked button |
| `ModalTaskContext` | `ModalInteractionEvent` | modal `customId` | | `ModalTaskContext` | `ModalInteractionEvent` | modal `customId` |
| `EmojiTaskContext` | `MessageReactionAddEvent` | composite `guildId:messageId` | | `EmojiTaskContext` | `MessageReactionAddEvent` | composite `guildId:messageId` |
@@ -146,7 +146,7 @@ correct channel even when playback state changes without a fresh user interactio
## End-to-end summary ## End-to-end summary
| Event | Context | Flow | Async result wraps | | Event | Context | Flow | Async result wraps |
|---|---|---|---| |---------------------------------|-------------------------|---------|--------------------|
| `SlashCommandInteractionEvent` | `SlashCommandContext` | Command | `InteractionHook` | | `SlashCommandInteractionEvent` | `SlashCommandContext` | Command | `InteractionHook` |
| `MessageReceivedEvent` (prefix) | `MessageCommandContext` | Command | `Message` | | `MessageReceivedEvent` (prefix) | `MessageCommandContext` | Command | `Message` |
| `ButtonInteractionEvent` | `ButtonTaskContext` | Task | `InteractionHook` | | `ButtonInteractionEvent` | `ButtonTaskContext` | Task | `InteractionHook` |
+3 -3
View File
@@ -7,7 +7,7 @@
## Stack ## Stack
| Concern | Technology | | Concern | Technology |
|---|---| |------------|-------------------------------------------------------------------------------|
| ORM | Hibernate 7.1.9 (Jakarta JPA) | | ORM | Hibernate 7.1.9 (Jakarta JPA) |
| Engine | MariaDB 11 (per `docker-compose.yml`), MySQL Connector/J 9.7.0 driver, InnoDB | | Engine | MariaDB 11 (per `docker-compose.yml`), MySQL Connector/J 9.7.0 driver, InnoDB |
| Pooling | HikariCP (via `hibernate-hikaricp`) | | Pooling | HikariCP (via `hibernate-hikaricp`) |
@@ -48,7 +48,7 @@ Entities live in `me.bobert.netneutrality.data.entity` and are prefixed with `O`
of a star-shaped model; most other tables reference `guild_id` with `ON DELETE CASCADE`. of a star-shaped model; most other tables reference `guild_id` with `ON DELETE CASCADE`.
| Entity | Table | Key | Relationships / notes | | Entity | Table | Key | Relationships / notes |
|---|---|---|---| |-----------------|------------------|--------------------------|--------------------------------------------------------------------------------------------------------|
| `OGuild` | `guilds` | `guild_id` (String) | Central entity; one-to-many with settings, blacklist, log messages, logging. | | `OGuild` | `guilds` | `guild_id` (String) | Central entity; one-to-many with settings, blacklist, log messages, logging. |
| `OGuildSetting` | `guild_settings` | `id` (auto int) | Many-to-one `OGuild`; key/value guild settings. | | `OGuildSetting` | `guild_settings` | `id` (auto int) | Many-to-one `OGuild`; key/value guild settings. |
| `OBlacklist` | `blacklist` | `id` (auto int) | Many-to-one `OGuild`; blacklisted words. | | `OBlacklist` | `blacklist` | `id` (auto int) | Many-to-one `OGuild`; blacklisted words. |
@@ -110,7 +110,7 @@ Services wrap repositories with business logic and **caching**.
`GSettings` is an **enum** of the available per-guild configuration keys and their defaults: `GSettings` is an **enum** of the available per-guild configuration keys and their defaults:
| Setting | Default | | Setting | Default |
|---|---| |-------------------------|---------|
| `MUSIC_ROLE` | (none) | | `MUSIC_ROLE` | (none) |
| `MUSIC_CHANNEL` | (none) | | `MUSIC_CHANNEL` | (none) |
| `MUSIC_VOLUME` | `30` | | `MUSIC_VOLUME` | `30` |
+5 -5
View File
@@ -13,7 +13,7 @@ inserted earlier (typically by the command that created the button), so the `Tas
the task and execute it. The system has since grown to cover several task subtypes: the task and execute it. The system has since grown to cover several task subtypes:
| Subtype | Trigger | Id source | | Subtype | Trigger | Id source |
|---|---|---| |----------------------------------------------|------------------------------------|----------------------------------------|
| [Button task](#button-emoji-and-modal-tasks) | `ButtonInteractionEvent` | button `componentId` | | [Button task](#button-emoji-and-modal-tasks) | `ButtonInteractionEvent` | button `componentId` |
| [Modal task](#button-emoji-and-modal-tasks) | `ModalInteractionEvent` | modal `customId` | | [Modal task](#button-emoji-and-modal-tasks) | `ModalInteractionEvent` | modal `customId` |
| [Emoji task](#button-emoji-and-modal-tasks) | `MessageReactionAddEvent` | composite `guildId:messageId` | | [Emoji task](#button-emoji-and-modal-tasks) | `MessageReactionAddEvent` | composite `guildId:messageId` |
@@ -38,7 +38,7 @@ The id is the **primary lookup key** used by `TaskManager`. How it is derived de
`TaskManager` what to do with the task afterwards: `TaskManager` what to do with the task afterwards:
| Lifecycle | Meaning | | Lifecycle | Meaning |
|---|---| |----------------------------|-------------------------------------------------------------------|
| `BUILTIN` | Task is static and stays registered indefinitely. | | `BUILTIN` | Task is static and stays registered indefinitely. |
| `KEEP` | Keep the task in memory after execution (can run again). | | `KEEP` | Keep the task in memory after execution (can run again). |
| `REMOVE_SELF` | Remove this task after execution (one-shot). | | `REMOVE_SELF` | Remove this task after execution (one-shot). |
@@ -138,7 +138,7 @@ The file declares, for each persistable task class, the constructor parameters a
Schema: Schema:
| Key | Meaning | | Key | Meaning |
|---|---| |----------|------------------------------------------------------------------|
| `tasks` | array of task mappings | | `tasks` | array of task mappings |
| `class` | fully-qualified class name; **must** implement `PersistableTask` | | `class` | fully-qualified class name; **must** implement `PersistableTask` |
| `params` | ordered list of payload field names | | `params` | ordered list of payload field names |
@@ -259,7 +259,7 @@ Tasks can opt into extra behavior by implementing marker/behavior interfaces in
`core.tasks.trait`: `core.tasks.trait`:
| Trait | Effect | | Trait | Effect |
|---|---| |----------------------|------------------------------------------------------------------------------------------|
| `Deletable` | After successful execution, `TaskManager` deletes the original message/interaction. | | `Deletable` | After successful execution, `TaskManager` deletes the original message/interaction. |
| `NotAcknowledgeable` | Suppresses the default acknowledgement (the task sends its own response). | | `NotAcknowledgeable` | Suppresses the default acknowledgement (the task sends its own response). |
| `WithButton` | The task can render itself as a JDA `Button` (`createButton()`). | | `WithButton` | The task can render itself as a JDA `Button` (`createButton()`). |
@@ -269,7 +269,7 @@ Tasks can opt into extra behavior by implementing marker/behavior interfaces in
## Concrete task catalog ## Concrete task catalog
| Task | Package | Base type | Description | | Task | Package | Base type | Description |
|---|---|---|---| |-----------------------------|-------------|------------------------------|--------------------------------------------------------------|
| `EmojiReactionRoleTask` | `emoji` | `EmojiTask` (persistable) | Assign/remove a role when a user reacts with a mapped emoji. | | `EmojiReactionRoleTask` | `emoji` | `EmojiTask` (persistable) | Assign/remove a role when a user reacts with a mapped emoji. |
| `AcceptButton` | `generic` | `ButtonTask` | Generic "accept" button running a supplied function. | | `AcceptButton` | `generic` | `ButtonTask` | Generic "accept" button running a supplied function. |
| `CancelButton` | `generic` | `ButtonTask` | Generic "cancel" button running a supplied function. | | `CancelButton` | `generic` | `ButtonTask` | Generic "cancel" button running a supplied function. |
@@ -1,4 +1,4 @@
package me.bobert.netneutrality.command.basic; package me.bobert.netneutrality.command.bot;
/* /*
* Copyright (c) anthorob * Copyright (c) anthorob
* *
@@ -14,8 +14,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import static java.util.function.Predicate.not;
public class UsageValidator { public class UsageValidator {
public boolean validate(CommandContext context, CommandContainer container) { public boolean validate(CommandContext context, CommandContainer container) {
@@ -100,7 +100,7 @@ public abstract class CommandContext extends AbstractContext {
public CommandOption getOption(String name) { public CommandOption getOption(String name) {
List<CommandOption> options = getOptionsByName(name); List<CommandOption> options = getOptionsByName(name);
return options.isEmpty() ? null : options.get(0); return options.isEmpty() ? null : options.getFirst();
} }
public List<CommandOption> getOptionsByName(String name) { public List<CommandOption> getOptionsByName(String name) {
@@ -89,7 +89,7 @@ public class ScheduledTaskManager implements Lifecycle {
public void cancelAll() { public void cancelAll() {
logger.info("Cancelling all {} scheduled tasks", scheduledTasks.size()); logger.info("Cancelling all {} scheduled tasks", scheduledTasks.size());
scheduledTasks.forEach((id, future) -> future.cancel(false)); scheduledTasks.forEach((_, future) -> future.cancel(false));
scheduledTasks.clear(); scheduledTasks.clear();
} }
@@ -182,7 +182,7 @@ public class TaskManagerImpl implements TaskManager {
String lockId = task.getLockId(); String lockId = task.getLockId();
ReentrantLock lock = (task instanceof PersistableTask || task instanceof BuiltInTask) ReentrantLock lock = (task instanceof PersistableTask || task instanceof BuiltInTask)
? null ? null
: tasksLock.computeIfAbsent(lockId, k -> new ReentrantLock()); : tasksLock.computeIfAbsent(lockId, _ -> new ReentrantLock());
try { try {
if (lock != null) { if (lock != null) {
@@ -105,7 +105,7 @@ public class WebModule {
try (Response response = client.newCall(request).execute()) { try (Response response = client.newCall(request).execute()) {
long elapsedTime = System.currentTimeMillis() - startTime; long elapsedTime = System.currentTimeMillis() - startTime;
Sentry.addBreadcrumb(Breadcrumb.http(url, method.name(), response.code())); Sentry.addBreadcrumb(Breadcrumb.http(url, method.name(), response.code()));
String body = response.body() != null ? response.body().string() : null; String body = response.body().string();
this.cachedResponse = new CachedResponse( this.cachedResponse = new CachedResponse(
response.code(), response.code(),
@@ -168,13 +168,13 @@ public class WebModule {
} }
public boolean isSuccess() { public boolean isSuccess() {
return cachedResponse != null && cachedResponse.getStatusCode() >= 200 && cachedResponse.getStatusCode() < 400; return cachedResponse != null && cachedResponse.statusCode() >= 200 && cachedResponse.statusCode() < 400;
} }
public static class Builder { public static class Builder {
private Request request; private Request request;
private final String url; private final String url;
private String version; private final String version;
private Method method = Method.GET; private Method method = Method.GET;
private long timeout = 5; private long timeout = 5;
private TimeUnit timeUnit = TimeUnit.MINUTES; private TimeUnit timeUnit = TimeUnit.MINUTES;
@@ -328,39 +328,13 @@ public class WebModule {
} }
} }
private static class CachedResponse { private record CachedResponse(int statusCode, Headers headers, long elapsedTime, String body, long expirationTime,
private final int statusCode; String errorMessage) {
private final Headers headers;
private final long elapsedTime;
private final String body;
private final long expirationTime;
private final String errorMessage;
public CachedResponse(int statusCode, Headers headers, long elapsedTime, String body, long expirationTime, String errorMessage) {
this.statusCode = statusCode;
this.headers = headers;
this.elapsedTime = elapsedTime;
this.body = body;
this.expirationTime = expirationTime;
this.errorMessage = errorMessage;
}
public boolean isExpired() { public boolean isExpired() {
return System.currentTimeMillis() > expirationTime; return System.currentTimeMillis() > expirationTime;
} }
public int getStatusCode() {
return statusCode;
}
public Headers getHeaders() {
return headers;
}
public long getElapsedTime() {
return elapsedTime;
}
public Optional<String> getBody() { public Optional<String> getBody() {
return Optional.ofNullable(body); return Optional.ofNullable(body);
} }
@@ -16,18 +16,8 @@ import static org.junit.jupiter.api.Assertions.assertNull;
public class ApplicationContextTest { public class ApplicationContextTest {
private static final class RecordingComponent implements Lifecycle { private record RecordingComponent(String name, int priority, List<String> startLog,
private final String name; List<String> stopLog) implements Lifecycle {
private final int priority;
private final List<String> startLog;
private final List<String> stopLog;
private RecordingComponent(String name, int priority, List<String> startLog, List<String> stopLog) {
this.name = name;
this.priority = priority;
this.startLog = startLog;
this.stopLog = stopLog;
}
@Override @Override
public void start() { public void start() {
@@ -38,11 +28,6 @@ public class ApplicationContextTest {
public void stop() { public void stop() {
stopLog.add(name); stopLog.add(name);
} }
@Override
public int priority() {
return priority;
}
} }
@Test @Test
@@ -25,30 +25,16 @@ public class ReflectionInjectionTest {
public interface Fixture { public interface Fixture {
} }
/** Dependency registered in the test context. */ /**
public static final class SampleDependency { * Dependency registered in the test context.
private final String label; */
public record SampleDependency(String label) {
public SampleDependency(String label) {
this.label = label;
} }
public String label() { /**
return label; * Fixture that requires a registered dependency via constructor injection.
} */
} public record InjectedFixture(SampleDependency dependency) implements Fixture {
/** Fixture that requires a registered dependency via constructor injection. */
public static final class InjectedFixture implements Fixture {
private final SampleDependency dependency;
public InjectedFixture(SampleDependency dependency) {
this.dependency = dependency;
}
public SampleDependency dependency() {
return dependency;
}
} }
/** Fixture that only exposes a no-arg constructor (fallback path). */ /** Fixture that only exposes a no-arg constructor (fallback path). */
@@ -22,19 +22,17 @@ import static org.mockito.Mockito.*;
public class QueueManagerTest { public class QueueManagerTest {
private AudioPlayer player; private AudioPlayer player;
private TrackScheduler scheduler;
private MusicManager musicManager; private MusicManager musicManager;
private MusicManagerCallback callback; private MusicManagerCallback callback;
private Guild guild;
private QueueManager queueManager; private QueueManager queueManager;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
player = mock(AudioPlayer.class); player = mock(AudioPlayer.class);
guild = mock(Guild.class); Guild guild = mock(Guild.class);
when(guild.getId()).thenReturn("guild1"); when(guild.getId()).thenReturn("guild1");
scheduler = mock(TrackScheduler.class); TrackScheduler scheduler = mock(TrackScheduler.class);
musicManager = mock(MusicManager.class); musicManager = mock(MusicManager.class);
callback = mock(MusicManagerCallback.class); callback = mock(MusicManagerCallback.class);
@@ -17,8 +17,6 @@ import static org.mockito.Mockito.*;
public class TrackSchedulerTest { public class TrackSchedulerTest {
private AudioPlayer player; private AudioPlayer player;
private MusicManager musicManager;
private QueueManager queueManager;
private TrackScheduler scheduler; private TrackScheduler scheduler;
@BeforeEach @BeforeEach
@@ -27,12 +25,11 @@ public class TrackSchedulerTest {
Guild guild = mock(Guild.class); Guild guild = mock(Guild.class);
when(guild.getId()).thenReturn("guild1"); when(guild.getId()).thenReturn("guild1");
musicManager = mock(MusicManager.class); MusicManager musicManager = mock(MusicManager.class);
when(musicManager.getPlayer()).thenReturn(player); when(musicManager.getPlayer()).thenReturn(player);
when(musicManager.getGuild()).thenReturn(guild); when(musicManager.getGuild()).thenReturn(guild);
scheduler = new TrackScheduler(musicManager); scheduler = new TrackScheduler(musicManager);
queueManager = scheduler.getQueueManager();
} }
@Test @Test