Files
netneutrality/docs/architecture.md
2026-10-19 08:21:42 -04:00

161 lines
11 KiB
Markdown

# Architecture Overview
Net Neutrality is a Java 25 Discord bot built on top of [JDA](https://github.com/discord-jda/JDA).
It is packaged as a single fat JAR (via the Maven Shade plugin) whose entry point is
`me.bobert.netneutrality.App`.
## Big picture
Discord sends **events** over the gateway. A single JDA `EventListener`
(`me.bobert.netneutrality.hooks.EventsHandler`) receives every event and turns it into a
**Context** — a high-level abstraction over that event. Each context is then routed to one of three
execution paths:
```
┌──────────────────────────────┐
Discord Gateway ───────▶│ EventsHandler │
(JDA events) │ event class → Context factory│
└───────────────┬──────────────┘
│ builds a Context
┌─────────────────────────┼───────────────────────────┐
▼ ▼ ▼
CommandContext TaskContext (no Context)
│ │ lambda /
▼ ▼ consumer runs
CommandProcessor TaskManager.handle() directly
.dispatch() (lookup task by id) (e.g. GuildJoin)
│ │
▼ ▼
Command.execute() Task.execute()
```
1. **Command flow** — the context is a `CommandContext`; it is handed to `CommandProcessor` which
resolves and runs a `Command`. See [commands.md](commands.md).
2. **Task flow** — the context is a `TaskContext`; it is handed to `TaskManager` which looks up a
`Task` by its **id** and runs it. See [tasks.md](tasks.md).
3. **Lambda flow** — for simple events (e.g. `GuildJoinEvent`, `GuildLeaveEvent`) a consumer runs
directly and no context is routed onward. See [events.md](events.md).
Around this core are the supporting subsystems: [persistence](persistence.md) (Hibernate + Flyway),
[music](music.md) (a remote Lavalink node), the [web API](web-api.md), and the [threading/scheduling](threading.md)
infrastructure.
## Technology stack
| Concern | Library | Version |
|---|---|---|
| Discord API | `net.dv8tion:JDA` | 6.4.1 |
| Audio engine | Lavalink v4 node (`ghcr.io/lavalink-devs/lavalink`) | 4.2 |
| Audio client | `dev.arbjerg:lavalink-client` | 3.4.0 |
| ORM | `org.hibernate.orm:hibernate-core` | 7.1.9.Final |
| Connection pool | `hibernate-hikaricp` (HikariCP) | 7.1.9.Final |
| JDBC driver | `com.mysql:mysql-connector-j` | 9.7.0 |
| DB migrations | `org.flywaydb:flyway-core` / `flyway-mysql` | 11.20.3 |
| Error tracking | `io.sentry:sentry` | 8.41.0 |
| HTTP client | `com.squareup.okhttp3:okhttp-jvm` | 5.3.2 |
| JSON | `org.json:json` + `com.google.code.gson:gson` | — |
| Classpath scanning | `org.reflections:reflections` | 0.10.2 |
| Emoji parsing | `net.fellbaum:jemoji` | 1.7.6 |
| Logging | Log4j2 + `log4j-slf4j2-impl` | 2.26.0 |
| Testing | JUnit Jupiter 6 + Mockito 5 | — |
The HTTP **server** for the internal API uses embedded Javalin (with OpenAPI/Swagger plugins); see
[web-api.md](web-api.md).
## Startup lifecycle
The entry point is `App.main()` followed by the `App()` constructor which calls
`startup()`. Bootstrap is driven by a hand-rolled
[`ApplicationContext`](utilities-and-bootstrap.md#applicationcontext--lifecycle) that owns wired
instances and starts every [`Lifecycle`](utilities-and-bootstrap.md#applicationcontext--lifecycle)
component in ascending priority order.
1. **Print banner / read env** — reads `NETNEUTRALITY_BOT_ENV` (default `dev`) via the early-bootstrap
`Settings.getInstance()` singleton (the only remaining process-wide settings access point; it is
also registered into the context as `SettingsProvider` for constructor injection).
2. **Sentry** — if `NETNEUTRALITY_SENTRY_DSN` is set, initialize the Sentry SDK
(environment, release = `VERSION`, traces sample rate 1.0).
3. **Log levels** — in `dev`, raise JDA and Lavalink client logging to `DEBUG`.
4. `startup()`:
1. Validate `NETNEUTRALITY_BOT_TOKEN` is present (throws if missing).
2. `buildContext()` — register domain services and lifecycle adapters (see priorities below).
3. `context.start()` — start every `Lifecycle` component in ascending priority order.
4. Register a JVM shutdown hook calling `shutdown()` → `context.stop()`.
### Lifecycle priorities (startup order)
| Priority | Component | What `start()` does |
|---|---|---|
| 10 | `SessionManager` (`SessionProvider`) | Build Hibernate `SessionFactory` + run Flyway migrations. Binds itself into `JpaUtils`. |
| 20 | `ThreadManager` (`ExecutorProvider`) | No-op start (executors are lazy). Bound into `ChannelMessagePurger`. |
| 30 | `CommandManager` (`CommandRegistry`) | Reflectively discover/register `Command` implementations with context-aware constructor injection. |
| 30 | `TaskManagerImpl` (`TaskManager`) | Reflectively load built-in & scheduled tasks (DI) and reload persistable tasks from the DB. |
| 40 | `MusicServiceImpl` (`MusicService`) | Create the shared `LavalinkClient`, register the node and subscribe to its events. |
| 55 | `ScheduledTaskManager` | No-op start (tasks are scheduled by `TaskManager` during its own start). |
| 60 | JDA adapter | Build `JDA` with gateway intents and await ready. |
| 65 | Post-JDA adapter | Register slash commands, attach `EventsHandler` (injected providers), set presence. |
| 70 | `WebApi` | Start the internal HTTP server (`NETNEUTRALITY_API_PORT`, default 9000). |
Commands and tasks that need shared services declare them as constructor parameters
(`MusicService`, `TaskManager`, `GuildServiceProvider`, `SettingsProvider`, `CommandRegistry`, …).
`ReflectionHelper` picks the richest public constructor whose parameter types all resolve from the
context (falling back to a no-arg constructor when none match).
### Gateway intents
`initJDA()` enables: `GUILD_MEMBERS`, `GUILD_MESSAGE_REACTIONS`, `GUILD_VOICE_STATES`,
`MESSAGE_CONTENT`. It also installs `new JDAVoiceUpdateListener(music.getClient())` as the voice
dispatch interceptor, so voice server/state updates are forwarded to Lavalink instead of JDA's own
audio stack. DAVE (end-to-end encrypted voice) is handled by the node, which is why the bot no
longer ships native audio encoders.
## Shutdown lifecycle
`App.shutdown()` (invoked by the JVM shutdown hook, `ShutdownCommand`, or the HTTP
`/actions/shutdown` endpoint) calls `context.stop()`, which stops every registered `Lifecycle`
component in the **exact reverse** of start order. Failures in one component are logged and do not
block the others. Effectively this means:
1. `WebApi.stop()` — stop the HTTP server and its executor.
2. Post-JDA adapter — no-op.
3. JDA adapter — `jda.shutdown()`.
4. `ScheduledTaskManager.stop()` — cancel all scheduled tasks.
5. `MusicServiceImpl.stop()` — dispose the event subscriptions, destroy the players and close the Lavalink client.
6. `CommandManager` / `TaskManagerImpl` — no-op stop today.
7. `ThreadManager.stop()` — shut down the shared executors.
8. `SessionManager.stop()` — release the Hibernate registry / connection pool.
## Configuration & build
- **Config sources** — `Settings` loads `bot.properties` and overlays environment variables (env
vars win). See [utilities-and-bootstrap.md](utilities-and-bootstrap.md#settings). An example file
lives at `bot.properties.example`.
- **Build** — `./mvnw package` produces `target/netneutrality.jar` (Shade fat JAR, main class
`me.bobert.netneutrality.App`). Checkstyle (`checkstyle.xml`) runs in the `test` phase
and fails the build on violations.
- **Run** — the preferred method is Docker Compose (`docker compose build && docker compose up -d`),
which also provisions the MariaDB database. See `README.md` and `docker-compose.yml`.
## Package map
| Package | Contents |
|---------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| `me.bobert.netneutrality` | Entry point (`App`) |
| `…bootstrap` | `ApplicationContext`, `Lifecycle`, `BotState`, `ReflectionHelper` |
| `…core.command` | Command engine (registry, processor, manager, options, impl bases) |
| `…core.context` | Context interfaces, abstract bases (`CommandContext`, `TaskContext`, etc.) |
| `…core.tasks` | Task engine (manager, subtypes, traits, persistence, generic buttons) |
| `…core.thread` | Executors & scheduling (`ThreadManager`, `ScheduledTaskManager`, `BaseThreadFactory`) |
| `…core` | `LogHandler` |
| `…command.*` | Concrete command implementations (`admin`, `basic`, `bot`, `crypto`, `custom`, `git`, `music`, `react`) |
| `…context.command` / `…context.tasks` | Concrete context implementations (slash, message, button, modal, emoji, etc.) |
| `…context` | `MusicChannelContext` |
| `…tasks.*` | Concrete task implementations (`git`, `help`, `music`, `reaction`) |
| `…db` | Session/migration, `Repository`, `JpaUtils`, `GSettings` |
| `…db.entities` / `…db.repository` / `…db.service` | Persistence model, repositories, services |
| `…hooks` | `EventsHandler` |
| `…http` | Internal web API and HTTP client module |
| `…music` | Music domain (service, manager, queue, scheduler, track loading) |
| `…utils` | `Settings` / `SettingsProvider`, `ChannelMessagePurger`, `MusicUtils` |
| `…exceptions` | Custom exception hierarchy |