From 389af563b91ccc710d8c576a094c6c037ceb4e41 Mon Sep 17 00:00:00 2001 From: anthorob Date: Mon, 19 Oct 2026 08:21:42 -0400 Subject: [PATCH] update docs --- README.md | 86 +++++++++++++++++++++++++-- docs/README.md | 2 +- docs/architecture.md | 5 +- docs/commands.md | 6 +- docs/contexts.md | 6 +- docs/events.md | 2 +- docs/persistence.md | 4 +- docs/tasks.md | 6 +- docs/threading.md | 8 +-- docs/utilities-and-bootstrap.md | 24 ++++---- docs/web-api.md | 101 ++++++++++++++++++++++---------- 11 files changed, 181 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 9aff4d0..c3e4959 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,89 @@ -# Net Neutrality Discord bot +# Net Neutrality Discord Bot -This is the source code for my custom discord bot. -This is a personal project, so I offer no support on how to build it. +A custom Discord bot written in Java. It uses [JDA](https://github.com/discord-jda/JDA) for Discord, a [Lavalink](https://github.com/lavalink-devs/Lavalink) node for music playback, MariaDB/MySQL for persistence, and exposes an HTTP API built with Javalin. +This is a personal project. No support is offered for building or running it. -The preferred method for running the bot is by using Docker Compose like this: +## Table of Contents + +- [Requirements](#requirements) +- [Configuration](#configuration) +- [Running with Docker Compose](#running-with-docker-compose) +- [Running Locally](#running-locally) +- [Testing](#testing) +- [License](#license) + +## Requirements + +- Docker and Docker Compose (recommended), or +- JDK 25 and Maven (the included `mvnw` wrapper works) for local runs +- A MariaDB/MySQL database +- A Lavalink node, version 4.2.0 or newer +- A Discord bot token + +## Configuration + +All settings are provided as environment variables. A template with every supported variable is available in [`bot.properties.example`](bot.properties.example). + +Key variables: + +| Variable | Description | +| --- | --- | +| `NETNEUTRALITY_BOT_TOKEN` | Discord bot token | +| `NETNEUTRALITY_BOT_PREFIX` | Command prefix | +| `NETNEUTRALITY_BOT_OWNER` | Discord user ID of the bot owner | +| `NETNEUTRALITY_API_PORT` | Port for the HTTP API (default `9000`) | +| `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` | Database connection settings | +| `NETNEUTRALITY_LAVALINK_URI`, `NETNEUTRALITY_LAVALINK_PASSWORD` | Lavalink connection settings | +| `NETNEUTRALITY_SENTRY_DSN` | Optional Sentry DSN for error reporting | +| `GITEA_HOST`, `GITEA_TOKEN`, `GITEA_OWNER`, `GITEA_REPO` | Optional Gitea settings for issue reporting | + +## Running with Docker Compose + +Docker Compose is the preferred way to run the bot. It starts the bot, the database, Lavalink, and the supporting YouTube cipher and PO token services. + +1. Copy `bot.properties.example` and fill in the values (see [Configuration](#configuration)). +2. Build and start the stack: ```bash docker compose build docker compose up -d ``` -The environment variables needed for the bot to run are available in the file **bot.properties.example** -If running locally without Docker, you need to copy the example file to **bot.properties** and fill the values. \ No newline at end of file +3. View logs: + +```bash +docker compose logs -f netneutrality-bot +``` + +Logs are also written to the `logs` directory. + +## Running Locally + +1. Copy `bot.properties.example` to `bot.properties` and fill in the values. +2. Make sure a database and a Lavalink node are reachable at the configured addresses. +3. Build the project: + +```bash +./mvnw package +``` + +4. Run the bot: + +```bash +java -jar target/netneutrality.jar +``` + +Dependencies are copied to `target/lib` during packaging and referenced by the jar manifest. + +## Testing + +```bash +./mvnw test +``` + +Database tests use Testcontainers, so Docker must be running. Checkstyle runs during the `test` phase. + +## License + +See the [LICENSE](LICENSE) file. diff --git a/docs/README.md b/docs/README.md index 8358252..dccc6e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ subsystem, how they fit together, and the end-to-end flows that tie them. | [tasks.md](tasks.md) | Tasks — button/emoji/modal/step/built-in/persistable/scheduled tasks | | [persistence.md](persistence.md) | Database — Hibernate/JPA, Flyway, entities, repositories, services | | [music.md](music.md) | Music — Lavalink node & client, queue, event dispatch | -| [web-api.md](web-api.md) | HTTP Web API — internal control & introspection server | +| [web-api.md](web-api.md) | HTTP Web API — Javalin server for control, introspection & music | | [threading.md](threading.md) | Threading & scheduling infrastructure | | [utilities-and-bootstrap.md](utilities-and-bootstrap.md) | Utilities, configuration, logging, exceptions | diff --git a/docs/architecture.md b/docs/architecture.md index 2fa99a6..835583b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -59,8 +59,8 @@ infrastructure. | Logging | Log4j2 + `log4j-slf4j2-impl` | 2.26.0 | | Testing | JUnit Jupiter 6 + Mockito 5 | — | -The HTTP **server** for the internal API uses the JDK built-in -`com.sun.net.httpserver.HttpServer` (no external framework); see [web-api.md](web-api.md). +The HTTP **server** for the internal API uses embedded Javalin (with OpenAPI/Swagger plugins); see +[web-api.md](web-api.md). ## Startup lifecycle @@ -157,5 +157,4 @@ block the others. Effectively this means: | `…http` | Internal web API and HTTP client module | | `…music` | Music domain (service, manager, queue, scheduler, track loading) | | `…utils` | `Settings` / `SettingsProvider`, `ChannelMessagePurger`, `MusicUtils` | -| `…shared.util` | Cross-cutting helpers | | `…exceptions` | Custom exception hierarchy | diff --git a/docs/commands.md b/docs/commands.md index b4b1cae..eee03f4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,7 +1,7 @@ # Commands -**Packages:** `me.bobert.netneutrality.framework.command` (framework), -`me.bobert.netneutrality.feature.*.command` (concrete commands) +**Packages:** `me.bobert.netneutrality.core.command` (framework), +`me.bobert.netneutrality.command.*` (concrete commands) A **Command** is what a user executes on Discord. It receives a [`CommandContext`](contexts.md#commandcontext) and runs logic based on it. There are two primary @@ -129,7 +129,7 @@ At runtime, an incoming command event becomes a `CommandContext` in the ## Command catalog -Concrete commands live under `me.bobert.netneutrality.feature.*.command`, grouped by feature package: +Concrete commands live under `me.bobert.netneutrality.command.*`, grouped by feature package: | Command | Package | Type | Description | |---|---|---|---| diff --git a/docs/contexts.md b/docs/contexts.md index 9c9fac5..bcf9aa1 100644 --- a/docs/contexts.md +++ b/docs/contexts.md @@ -1,7 +1,7 @@ # Contexts -**Packages:** `me.bobert.netneutrality.framework.context` (framework), -`me.bobert.netneutrality.framework.context` (concrete implementations) +**Packages:** `me.bobert.netneutrality.core.context` (framework), +`me.bobert.netneutrality.context.command` / `me.bobert.netneutrality.context.tasks` (concrete implementations) A **Context** is a high-level abstraction over a Discord event. It hides the differences between the many JDA event types (slash command, message, button, modal, reaction, …) behind a single, uniform @@ -117,7 +117,7 @@ Abstract base for the [task flow](tasks.md). Two things distinguish it: ### `AsyncContext` -`me.bobert.netneutrality.framework.context.AsyncContext` wraps the **result** of a previous reply — either an +`me.bobert.netneutrality.core.context.AsyncContext` wraps the **result** of a previous reply — either an `InteractionHook` (for interactions) or a `Message` (for message replies) — so follow-up actions are expressed the same way regardless of the original trigger: diff --git a/docs/events.md b/docs/events.md index 90a2853..21ef943 100644 --- a/docs/events.md +++ b/docs/events.md @@ -1,6 +1,6 @@ # Event Handling & Routing -**Package:** `me.bobert.netneutrality.platform.event` +**Package:** `me.bobert.netneutrality.hooks` **Primary class:** `EventsHandler` `EventsHandler` is the single JDA `EventListener` registered on the `JDA` instance diff --git a/docs/persistence.md b/docs/persistence.md index 0131842..a80c83e 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -1,6 +1,6 @@ # Persistence & Database -**Packages:** `me.bobert.netneutrality.platform.persistence` (session/migration), +**Packages:** `me.bobert.netneutrality.db` (session/migration), `me.bobert.netneutrality.db` (entities, repositories, services). **Resources:** `hibernate.cfg.xml`, `db/migration/V1__initial_schema.sql`, `db/migration/V2__task_type_id.sql`. @@ -45,7 +45,7 @@ Repositories obtain sessions through the factory; transactional access uses help ## Entity catalog -Entities live in `me.bobert.netneutrality.data.entity` and are prefixed with `O`. `OGuild` is the hub +Entities live in `me.bobert.netneutrality.db.entities` and are prefixed with `O`. `OGuild` is the hub of a star-shaped model; most other tables reference `guild_id` with `ON DELETE CASCADE`. | Entity | Table | Key | Relationships / notes | diff --git a/docs/tasks.md b/docs/tasks.md index b6eb70e..25ff67e 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -1,7 +1,7 @@ # Tasks -**Packages:** `me.bobert.netneutrality.framework.task` (framework), -`me.bobert.netneutrality.feature.*.task` (concrete tasks) +**Packages:** `me.bobert.netneutrality.core.tasks` (framework), +`me.bobert.netneutrality.tasks.*` (concrete tasks) Tasks are one of the more advanced concepts in the bot. A **Task** is a unit of logic that is looked up and executed by **id** when a matching Discord event arrives, as opposed to a @@ -60,7 +60,7 @@ AbstractTask (implements Task) ## `TaskManager` -`me.bobert.netneutrality.framework.task.TaskManager` is the registry, lookup, and execution +`me.bobert.netneutrality.core.tasks.TaskManager` is the registry, lookup, and execution coordinator. ### Registration — `insert` diff --git a/docs/threading.md b/docs/threading.md index f7532c5..c0725a8 100644 --- a/docs/threading.md +++ b/docs/threading.md @@ -1,6 +1,6 @@ # Threading & Scheduling -**Package:** `me.bobert.netneutrality.platform.concurrency` +**Packages:** `me.bobert.netneutrality.core.thread` and `me.bobert.netneutrality.core.scheduled` This subsystem centralizes asynchronous execution and recurring jobs. @@ -21,11 +21,11 @@ This subsystem centralizes asynchronous execution and recurring jobs. ## `BaseThreadFactory` -Names created threads (e.g. `web-api-1`) so they are identifiable in logs and stack dumps, and installs +Names created threads (e.g. `pool-1`) so they are identifiable in logs and stack dumps, and installs an [`ExecutorException`](utilities-and-bootstrap.md#exceptions) as the thread's `UncaughtExceptionHandler` so crashes on worker threads are still routed through -[`LogHandler`](utilities-and-bootstrap.md#loghandler). It is used both by `ThreadManager` and by the -[Web API](web-api.md) server pool. +[`LogHandler`](utilities-and-bootstrap.md#loghandler). It is used by `ThreadManager`; the +[Web API](web-api.md) runs on Javalin's own thread pool instead. ## `ScheduledTaskManager` diff --git a/docs/utilities-and-bootstrap.md b/docs/utilities-and-bootstrap.md index c5907d9..d7d8837 100644 --- a/docs/utilities-and-bootstrap.md +++ b/docs/utilities-and-bootstrap.md @@ -12,7 +12,7 @@ error tracking, the shared utility helpers, and the custom exception hierarchy. | `App` | The application: holds the process instance and the `JDA` handle, builds the `ApplicationContext`, runs `context.start()` / `context.stop()`, and is the JAR main class (`main()`). See [architecture.md](architecture.md). | | `bootstrap.ApplicationContext` | Hand-rolled container: `register(T)`, `get(Class)`, ordered `start()` / `stop()` for `Lifecycle` components. | | `bootstrap.Lifecycle` | Optional component contract: `start() throws Exception`, default `stop()`, default `priority()`. | -| `core.BotState` | Work-in-progress global state helper: exposes a `SAFE` flag and `dump()`, which serializes per-guild/music state to `state.json`. Bound to `GuildServiceProvider` / `MusicService` during bootstrap. | +| `bootstrap.BotState` | Work-in-progress global state helper: exposes a `SAFE` flag and `dump()`, which serializes per-guild/music state to `state.json`. Bound to `GuildServiceProvider` / `MusicService` during bootstrap. | The full startup and shutdown sequences are documented in [architecture.md](architecture.md#startup-lifecycle). @@ -37,13 +37,13 @@ resolve via `context.get(type)` is preferred; otherwise the no-arg constructor i | Interface | Typical implementor | |-----------------------------------------|---------------------------------------| -| `platform.config.SettingsProvider` | `utils.Settings` | -| `platform.persistence.SessionProvider` | `core.db.SessionManager` | -| `platform.concurrency.ExecutorProvider` | `core.thread.ThreadManager` | -| `framework.command.CommandRegistry` | `core.command.CommandManager` | -| `framework.task.TaskManager` | `core.tasks.TaskManagerImpl` | -| `feature.music.MusicService` | `music.MusicServiceImpl` | -| `data.service.GuildServiceProvider` | `db.service.GuildServiceProviderImpl` | +| `utils.SettingsProvider` | `utils.Settings` | +| `db.SessionProvider` | `db.SessionManager` | +| `core.thread.ExecutorProvider` | `core.thread.ThreadManager` | +| `core.command.CommandRegistry` | `core.command.CommandManager` | +| `core.tasks.TaskManager` | `core.tasks.TaskManagerImpl` | +| `music.MusicService` | `music.MusicServiceImpl` | +| `db.service.GuildServiceProvider` | `db.service.GuildServiceProviderImpl` | There are **no** `getInstance()` / `instance()` shims on the managers above. The only intentional static access points left are: @@ -60,8 +60,8 @@ static access points left are: ### `Settings` / `SettingsProvider` -**Class:** `me.bobert.netneutrality.platform.config.Settings` implements -`me.bobert.netneutrality.platform.config.SettingsProvider`. +**Class:** `me.bobert.netneutrality.utils.Settings` implements +`me.bobert.netneutrality.utils.SettingsProvider`. `Settings` is the single source of truth for configuration. It loads values from the `bot.properties` file and overlays **environment variables** on top (environment variables win). @@ -90,7 +90,7 @@ An example configuration file is `bot.properties.example`. Key configuration val ## Utility helpers -**Package:** `me.bobert.netneutrality.shared.util` (helpers) / `me.bobert.netneutrality.bootstrap` / `me.bobert.netneutrality.platform.*` +**Package:** `me.bobert.netneutrality.utils` (helpers) / `me.bobert.netneutrality.bootstrap` / `me.bobert.netneutrality.exceptions` | Class | Purpose | |---|---| @@ -125,7 +125,7 @@ singletons. ### `LogHandler` -**Class:** `me.bobert.netneutrality.platform.logging.LogHandler`. +**Class:** `me.bobert.netneutrality.core.LogHandler`. `LogHandler` is the unified logging/error entry point. It routes messages and exceptions to: diff --git a/docs/web-api.md b/docs/web-api.md index 3f15c8f..431e4ed 100644 --- a/docs/web-api.md +++ b/docs/web-api.md @@ -1,52 +1,91 @@ # HTTP Web API -**Package:** `me.bobert.netneutrality.platform.web` (client module) and -`me.bobert.netneutrality.platform.web.server` (server). -**Server:** JDK built-in `com.sun.net.httpserver.HttpServer` (no external web framework). +**Package:** `me.bobert.netneutrality.http` (outbound client) and +`me.bobert.netneutrality.http.server` (server). +**Server:** embedded [Javalin](https://javalin.io) 7 with the OpenAPI and Swagger UI plugins. -The bot exposes a small internal HTTP API for health monitoring, control, and guild introspection — -intended for an external dashboard/companion, not for end users in Discord. +The bot exposes an internal HTTP API for health monitoring, control, guild introspection, and music +control — intended for an external dashboard/companion, not for end users in Discord. ## Server startup `WebApi` is a [`Lifecycle`](utilities-and-bootstrap.md#applicationcontext--lifecycle) component (priority 70) registered in [`App.buildContext()`](architecture.md#startup-lifecycle). -It receives `SettingsProvider`, `GuildServiceProvider`, and suppliers for the live `JDA` / -shutdown callback via its constructor. On `start()` it: +It receives `SettingsProvider`, `GuildServiceProvider`, `MusicService`, and suppliers for the live +`JDA` / shutdown callback via its constructor. On `start()` it: - reads the port from `NETNEUTRALITY_API_PORT` (default `9000`), -- creates an `HttpServer` bound to that port, -- installs an `Executor` backed by a fixed thread pool of 10 threads named via - [`BaseThreadFactory`](threading.md), -- registers the handler contexts (below), and -- starts the server. +- creates a `Javalin` app (Javalin 7 requires routes and exception/error handlers to be registered + upfront inside `Javalin.create`), +- registers the `OpenApiPlugin` (title "Net Neutrality API", `X-API-Key` security scheme) and the + `SwaggerPlugin`, +- installs the `ApiKeyFilter` as a `before` handler, +- registers the route groups (below), the exception handlers and the 404 handler, and +- starts the server on the port. -On `stop()` the server and its executor are shut down gracefully. +On `stop()` the Javalin app is stopped. -## Handler model +## Authentication -`AbstractHandler` (implements `HttpHandler`) is the base for all handlers and provides helpers such as -`sendJsonResponse(...)` and `sendStringResponse(...)`. Concrete handlers extend it and are mounted at -a path prefix. +`ApiKeyFilter` compares the `X-API-Key` request header with the `NETNEUTRALITY_API_KEY` setting. +A missing/invalid key — or no configured key at all — yields `401` ("Missing or invalid API key"). +Public paths that skip the check: `/`, and anything starting with `/openapi`, `/swagger` or +`/webjars` (Swagger UI and its assets). -| Handler | Base path | Endpoints | -|---|---|---| -| `RootHandler` | `/` | Simple HTML "OK" landing response. | -| `GuildHandler` | `/guilds` | `/guilds/owner` — guild ids + owner ids (JSON); `/guilds/{guildId}/channels` — text channels (JSON); `/guilds/{guildId}/roles` — roles (JSON). | -| `ActionHandler` | `/actions` | `/actions/status` — JDA health + gateway ping; `/actions/shutdown` — graceful shutdown. | -| `SettingsHandler` | `/settings` | `/settings/catalog` — every known setting with its default value and description; `/settings/current/{guildId}` — current guild settings; `/settings/reload/{guildId}` — invalidate & reload the settings cache. | +## Error handling -The action/guild handlers read live state from the [`JDA`](architecture.md) instance and from the -[persistence services](persistence.md#service-layer) (e.g. `GuildSettingService` for the settings -endpoints). +- `HttpResponseException` (e.g. `BadRequestResponse`, `NotFoundResponse`) → + `{"status":"error","message":"..."}` with the exception's status. +- Any other exception → logged, `500` with `{"status":"error"}`. +- Unknown paths → `404` with `{"status":"error","message":"Not found"}`. + +## Route groups + +Each group is a `*Routes` class with a static `register(RoutesConfig, ...)` method; handlers are +annotated with `@OpenApi` so they appear in the generated OpenAPI document. Request/response shapes +live in the `http.server.dto` package. + +| Class | Endpoints | +|---|---| +| `WebApi` | `GET /` — HTML liveness message. | +| `GuildRoutes` | `GET /guilds` (optional `user_id` query restricts to mutual guilds); `GET /guilds/owner`; `GET /guilds/{guildId}/owner`; `/channels`; `/roles`; `/members/{userId}`, `/members/{userId}/access`, `/members/{userId}/roles`, `/members/{userId}/admin`; `/voice-channels`, `/voice-channels/{channelId}`; `GET /guilds/{guildId}/config`; `PUT /guilds/{guildId}/debug`, `/logging`, `/log-channel`. | +| `ActionRoutes` | `GET /actions/status` — JDA health + gateway ping; `POST /actions/shutdown` — graceful shutdown. | +| `SettingsRoutes` | `GET /settings/catalog` — every known setting with default and description; `GET /settings/current/{guildId}` — current guild settings; `PUT /settings/current/{guildId}/{name}` — update a setting; `PUT /settings/reload/{guildId}` — invalidate & reload the settings cache. | +| `StatsRoutes` | `GET /stats/bot`; `GET /stats/guilds/{guildId}`. | +| `MusicRoutes` | See below. | + +### Music endpoints + +Mirror the slash commands (see [music.md](music.md)); announcements are also posted to the guild's +music channel when a context is attached. + +| Method & path | Purpose | +|---|---| +| `GET /music/sources` | Supported music sources. | +| `GET /music/{guildId}/status` | Connection, playback, current track, position, queue size. | +| `GET /music/{guildId}/queue` | Queued tracks. | +| `POST /music/{guildId}/search` | Resolve a query (optional `searchEngine`) into candidates; returns a `search_id`. Nothing is queued. | +| `POST /music/{guildId}/queue` | Queue a candidate by `searchId` + `index` (or all with `queueAll=true`) for `userId`; the bot must already be in a voice channel. | +| `DELETE /music/{guildId}/queue` | Clear the queue, or remove one track with `?position=` (1-based). | +| `PATCH /music/{guildId}/queue/reorder` | Move a track (`fromPosition` → `toPosition`). | +| `POST /music/{guildId}/skip`, `/stop`, `/pause`, `/resume`, `/shuffle`, `/leave` | Playback controls. | +| `GET` / `PUT /music/{guildId}/volume` | Read / set volume (0–100). | +| `PUT /music/{guildId}/seek` | Seek to `positionMs`. | +| `GET /music/{guildId}/vote-skip` | Vote-skip status. | +| `POST /music/{guildId}/vote-skip/{userId}` | Cast a vote on behalf of a member. | +| `GET /music/{guildId}/dj/{userId}` | Whether the member is currently allowed to act as DJ. | + +Search results are held in `PendingSearchResults` and expire after a TTL (returned as +`search_id_ttl_seconds`). ## HTTP client — `WebModule` -`WebModule` wraps an `OkHttpClient` for the bot's own outbound HTTP calls (e.g. checking for updates -via GitLab). It keeps a small `LinkedHashMap`-based response cache limited to 50 entries. +`WebModule` (package `me.bobert.netneutrality.http`) wraps an `OkHttpClient` for the bot's own +outbound HTTP calls (e.g. checking for updates via GitLab). Responses are cached in +`ResponseCache`, a `LinkedHashMap`-based LRU limited to 50 entries. ## Notes -- There is no authentication layer documented on these endpoints; the API is expected to be bound to a - trusted/internal network (e.g. within the Docker network). `/actions/shutdown` is destructive, so the - port should not be exposed publicly. +- All routes except the public ones require the API key. Even so, the API is expected to be bound to + a trusted/internal network (e.g. within the Docker network); `/actions/shutdown` is destructive, + so the port should not be exposed publicly.