update docs

This commit is contained in:
anthorob committed 2026-10-19 08:21:42 -04:00
1 parent d3211c6702
commit 389af563b9
11 files changed
+181 -69

No files matched your search

+80 -6
View File
@@ -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.
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.
+1 -1
View File
@@ -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 |
+2 -3
View File
@@ -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 |
+3 -3
View File
@@ -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 |
|---|---|---|---|
+3 -3
View File
@@ -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:
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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 |
+3 -3
View File
@@ -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`
+4 -4
View File
@@ -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`
+12 -12
View File
@@ -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<T>)`, 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:
+70 -31
View File
@@ -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.