# Utilities, Bootstrap, Logging & Exceptions This document covers the cross-cutting glue of the bot: the entry points, configuration, logging / error tracking, the shared utility helpers, and the custom exception hierarchy. ## Bootstrap **Package:** `me.bobert.netneutrality` / `me.bobert.netneutrality.bootstrap` | Class | Role | |--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `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. | The full startup and shutdown sequences are documented in [architecture.md](architecture.md#startup-lifecycle). ### `ApplicationContext` + `Lifecycle` Domain services are constructed once in `App.buildContext()`, registered with the context, and resolved later by type: - `register(component)` — indexes the concrete class; if the component implements `Lifecycle` it participates in ordered start/stop. - `get(Class)` — returns the first registered instance assignable to `T` (interfaces work). - `start()` — sorts `Lifecycle` components by ascending `priority()` and starts them. - `stop()` — stops them in the exact reverse order; individual failures are logged and swallowed so remaining components still tear down. Constructor injection for reflectively loaded commands/tasks is handled by [`ReflectionHelper`](#reflectionhelper): the richest public constructor whose parameter types all resolve via `context.get(type)` is preferred; otherwise the no-arg constructor is used. **Interfaces used for injection** (implemented by the concrete managers registered above): | 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` | There are **no** `getInstance()` / `instance()` shims on the managers above. The only intentional static access points left are: - `Settings.getInstance()` — early bootstrap only (`main()`, Sentry, token validation) before the context exists; the same instance is registered as `SettingsProvider` for everything else. - `App.getInstance()` — application root used by a couple of shutdown entry points (`ShutdownCommand`). Prefer constructor injection of the needed collaborators where possible. - `JpaUtils.bind(SessionProvider)` / `LogHandler.bind(GuildServiceProvider)` / `ChannelMessagePurger.bind(ExecutorProvider)` — single well-defined static bind points for deep utility graphs that are not constructor-injected (repositories, static log helpers). ## Configuration ### `Settings` / `SettingsProvider` **Class:** `me.bobert.netneutrality.platform.config.Settings` implements `me.bobert.netneutrality.platform.config.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). - **Early bootstrap:** `Settings.getInstance()` (process-wide, created once before the context). - **Runtime injection:** depend on `SettingsProvider` and let `ApplicationContext` supply the same instance. Prefer this path in commands, tasks, handlers, and services. - `Settings.getVersion()` / `Settings.getDiscordInvite()` — thin static helpers used by the banner / invite strings; they delegate to the bootstrap instance. An example configuration file is `bot.properties.example`. Key configuration values: | Key | Purpose | |------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | `NETNEUTRALITY_BOT_ENV` | `dev` / `prod`; controls debug logging and the Sentry environment. | | `NETNEUTRALITY_BOT_TOKEN` | Discord bot token (**required**). | | `NETNEUTRALITY_BOT_PREFIX` | Message-command prefix (default `$`). | | `NETNEUTRALITY_BOT_OWNER` | Bot owner user id (used for owner-only commands / permissions). | | `NETNEUTRALITY_BOT_INVITE` | Invite link surfaced by help/info. | | `NETNEUTRALITY_API_PORT` | Port for the [Web API](web-api.md) (default `9000`). | | `MYSQL_*` | Database connection settings (see [persistence.md](persistence.md)). | | `NETNEUTRALITY_SENTRY_DSN` | Enables Sentry when present. | | `GITEA_HOST` / `GITEA_TOKEN` / `GITEA_OWNER` / `GITEA_REPO` | Gitea issue reporting ([`ReportIssueTask`](tasks.md#worked-example--gitea-bug-report-modal)). Token needs `write:issue`. | | `NETNEUTRALITY_LAVALINK_URI` / `_PASSWORD` / `_NAME` | Connection settings for the [Lavalink node](music.md#settings). | | `NETNEUTRALITY_PO_TOKEN` / `NETNEUTRALITY_VISITOR_DATA` / `NETNEUTRALITY_CIPHER_*` | YouTube auth/cipher settings, consumed by the node via `lavalink/application.yml`. | ## Utility helpers **Package:** `me.bobert.netneutrality.shared.util` (helpers) / `me.bobert.netneutrality.bootstrap` / `me.bobert.netneutrality.platform.*` | Class | Purpose | |---|---| | `Settings` | Configuration (see above). | | `ReflectionHelper` | Classpath scanning / reflective instantiation (see below). | | `ContextUtils` | Boilerplate helpers for building/handling [contexts](contexts.md). | | `GuildUtils` | Guild-related helpers (lookups, permission checks). | | `JpaUtils` | Session/transaction helpers for the [persistence](persistence.md) layer. Bound once to a `SessionProvider` during bootstrap (`JpaUtils.bind`). | | `MusicUtils` | Voice-channel connectivity and DJ/permission checks for [music](music.md). Callers pass the guild `MusicManager` (no static music lookup). | | `MiscUtils` | Assorted small helpers. | | `ChannelMessagePurger` | Bulk message deletion (used by the purge command). Bound once to an `ExecutorProvider` during bootstrap. | | `MessageFunction` | Standardized message/embed formatting. Help text builders take a `CommandRegistry` rather than a static command manager. | | `UserFunction` | Standardized user/string formatting. | ### `ReflectionHelper` Built on the `org.reflections` library, `ReflectionHelper` discovers and instantiates implementation classes in specific packages. It is the mechanism behind the bot's "drop a class in the right package and it's picked up" model, used to auto-load: - [`Command`](commands.md) implementations (`CommandManager` / `CommandRegistry.start()`), - [`BuiltInTask`](tasks.md#32-built-in-tasks) and [`ScheduledTask`](tasks.md#34-scheduled-tasks) implementations (`TaskManager.start()`). When given an `ApplicationContext`, `newInstance(Class, ApplicationContext)` selects the public constructor with the most parameters that can all be resolved via `context.get(parameterType)`. If no such constructor exists it falls back to the no-arg constructor. That is how music/admin/react commands receive `MusicService`, `GuildServiceProvider`, `TaskManager`, etc. without static singletons. ## Logging & error tracking ### `LogHandler` **Class:** `me.bobert.netneutrality.platform.logging.LogHandler`. `LogHandler` is the unified logging/error entry point. It routes messages and exceptions to: 1. **Log4j2** (configured by `log4j2.properties`; SLF4J is bridged via `log4j-slf4j2-impl`), 2. **Sentry** (when `NETNEUTRALITY_SENTRY_DSN` is configured), and 3. optionally a per-guild **Discord debug channel** (e.g. `sendMessageToDebugChannel(guild, msg)`), gated by [`GuildService.isDebugEnabled()`](persistence.md#service-layer) / [`OLogging`](persistence.md#entity-catalog) configuration. The Sentry **user** context is attached automatically when a [`AbstractContext`](contexts.md#abstractcontext-abstract-class) is created, so captured errors are attributed to the Discord user that triggered them. ## Exception hierarchy **Package:** `me.bobert.netneutrality.exceptions` ``` RuntimeException └── ContextException ── carries a Context (guild/channel/user) ├── CommandExecutionException ── thrown during command processing └── TaskExecutionException ── thrown during task execution GracefulContextError ── user-facing, non-logged failure → ephemeral reply ExecutorException ── Thread.UncaughtExceptionHandler → routes crashes to LogHandler ``` - **`ContextException`** — base runtime exception that carries the [`Context`](contexts.md) in which it occurred, so downstream handling can respond in the right channel and attribute the error. - **`CommandExecutionException`** / **`TaskExecutionException`** — specializations thrown by the [command](commands.md) and [task](tasks.md) flows respectively. - **`GracefulContextError`** — signals an expected, user-facing problem (e.g. bad input); when caught it produces an ephemeral message to the user instead of a full error log/Sentry event. - **`ExecutorException`** — an `UncaughtExceptionHandler` installed by [`BaseThreadFactory`](threading.md#basethreadfactory) so exceptions escaping worker threads are still captured by [`LogHandler`](#loghandler).