9.5 KiB
Architecture Overview
Net Neutrality is a Java 25 Discord bot built on top of 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()
- Command flow — the context is a
CommandContext; it is handed toCommandProcessorwhich resolves and runs aCommand. See commands.md. - Task flow — the context is a
TaskContext; it is handed toTaskManagerwhich looks up aTaskby its id and runs it. See tasks.md. - Lambda flow — for simple events (e.g.
GuildJoinEvent,GuildLeaveEvent) a consumer runs directly and no context is routed onward. See events.md.
Around this core are the supporting subsystems: persistence (Hibernate + Flyway), music (Lavaplayer), the web API, and the threading/scheduling infrastructure.
Technology stack
| Concern | Library | Version |
|---|---|---|
| Discord API | net.dv8tion:JDA |
6.4.1 |
| Voice / DAVE (E2EE) | club.minnced:jdave-* |
0.1.8 |
| Audio engine | dev.arbjerg:lavaplayer |
2.2.6 |
| YouTube source | dev.lavalink.youtube:v2 |
1.18.2 |
| 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 |
| GitLab integration | org.gitlab4j:gitlab4j-api |
6.2.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 the JDK built-in
com.sun.net.httpserver.HttpServer (no external framework); see 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 that owns wired
instances and starts every Lifecycle
component in ascending priority order.
- Print banner / read env — reads
NETNEUTRALITY_BOT_ENV(defaultdev) via the early-bootstrapSettings.getInstance()singleton (the only remaining process-wide settings access point; it is also registered into the context asSettingsProviderfor constructor injection). - Sentry — if
NETNEUTRALITY_SENTRY_DSNis set, initialize the Sentry SDK (environment, release =VERSION, traces sample rate 1.0). - Log levels — in
dev, raise JDA / Lavalink-YouTube / sedmelluq logging toDEBUG. startup():- Validate
NETNEUTRALITY_BOT_TOKENis present (throws if missing). buildContext()— register domain services and lifecycle adapters (see priorities below).context.start()— start everyLifecyclecomponent in ascending priority order.- Register a JVM shutdown hook calling
shutdown()→context.stop().
- Validate
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) |
Initialise the shared Lavaplayer AudioPlayerManager. |
| 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 the DAVE (end-to-end encrypted voice) session factory via
AudioModuleConfig().withDaveSessionFactory(new JDaveSessionFactory()).
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:
WebApi.stop()— stop the HTTP server and its executor.- Post-JDA adapter — no-op.
- JDA adapter —
jda.shutdown(). ScheduledTaskManager.stop()— cancel all scheduled tasks.MusicServiceImpl.stop()— disconnect players / leave voice channels.CommandManager/TaskManagerImpl— no-op stop today.ThreadManager.stop()— shut down the shared executors.SessionManager.stop()— release the Hibernate registry / connection pool.
Configuration & build
- Config sources —
Settingsloadsbot.propertiesand overlays environment variables (env vars win). See utilities-and-bootstrap.md. An example file lives atbot.properties.example. - Build —
./mvnw packageproducestarget/netneutrality.jar(Shade fat JAR, main classme.bobert.netneutrality.App). Checkstyle (checkstyle.xml) runs in thetestphase 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. SeeREADME.mdanddocker-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, NetNeutralityBot |
…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, sources, tracks) |
…utils |
Settings / SettingsProvider, ChannelMessagePurger, MusicUtils |
…shared.util |
Cross-cutting helpers |
…exceptions |
Custom exception hierarchy |