This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a submodule of the SkyBlock-Simplified multi-module Gradle project (Java 21, Gradle 9.4+). Run commands from the monorepo root (../).
# Build this module
./gradlew :discord-api:build
# Run tests
./gradlew :discord-api:test
# Clean build
./gradlew :discord-api:clean :discord-api:build
# Generate SVG hierarchy diagrams
./gradlew :discord-api:generateDiagramsRequired environment variables: DISCORD_TOKEN, DEVELOPER_ERROR_LOG_CHANNEL_ID
The debug bot (src/test/.../debug/DebugBot.java) can be run directly to test commands in isolation.
This module is a framework layer on top of Discord4J that provides a builder-driven, reactive API for building Discord bots. Entry point: DiscordBot (sole class in root discordapi package). Configuration via DiscordConfig (in handler/).
DiscordBot (abstract) → DiscordConfig (handler/) → initialize() → login() + connect()
├── CommandHandler — registers & routes commands
├── EmojiHandler — manages custom emoji upload/lookup
├── ExceptionHandler — abstract base in handler/exception/
│ ├── DiscordExceptionHandler — formats errors into Discord embeds
│ ├── SentryExceptionHandler — captures to Sentry with Discord context
│ └── CompositeExceptionHandler — chains multiple handlers in sequence
├── ResponseHandler — caches active Response messages (handler/response/)
└── ShardHandler — gateway shard management (handler/shard/)
Commands extend DiscordCommand<C extends CommandContext<?>> and are annotated with @Structure(...):
DiscordCommand<SlashCommandContext> → Slash commands (/command)
DiscordCommand<UserCommandContext> → Right-click user commands
DiscordCommand<MessageCommandContext> → Right-click message commands
@Structuredefines:name,description,parent(for subcommands),group(for subcommand groups),guildId(-1 for global),ephemeral,developerOnly,singleton,botPermissions,userPermissions,integrations,contextsgetParameters()returnsConcurrentUnmodifiableList<Parameter>for slash command optionsprocess(C context)is the abstract method to implement command logic, returnsMono<Void>- Commands are discovered via
Reflection.getResources().filterPackage(...).getTypesOf(DiscordCommand.class)and registered throughCommandHandler - The
apply()method inDiscordCommandhandles permission checks, parameter validation, and error handling before callingprocess() - Command-specific exceptions in
command/exception/:CommandException,PermissionException,BotPermissionException,DeveloperPermissionException,InputException,ExpectedInputException,ParameterException,DisabledCommandException,SingletonCommandException
Response is a single final class built via Response.builder(). It manages a HistoryHandler<Page, String> for page navigation and a PaginationHandler for building pagination components (buttons, select menus, modals).
Page hierarchy:
Page (interface)
├── TreePage — implements Subpages<TreePage>; supports nested subpages, embeds, content
└── FormPage — form/question pages for sequential input
Page.builder()→TreePage.TreePageBuilderPage.form()→FormPage.QuestionBuilderResponse.builder()builds the response;Response.from()creates a pre-filled builder from an existing response;response.mutate()is shorthand forResponse.from(this)
Response features:
- Multiple
Pageinstances (select menu navigation) ItemHandler<T>for paginated items with sort/filter/searchEmbedItemHandler— renders items as embed fieldsComponentItemHandler— renders items asSectioncomponents
- Interactive components (
Button,SelectMenu,TextInput,Modal,RadioGroup,Checkbox,CheckboxGroup) - Attachments, embeds, reactions
- Auto-expiration via
timeToLive(5-300 seconds) - Automatic Discord4J spec generation (
getD4jCreateSpec(),getD4jEditSpec(), etc.) - Persistence via
Response.builder().isPersistent(true)— writes the entry through to a JPA cold tier so the message survives bot restarts. Requires a matching@PersistentResponse-annotated builder method on the dispatchingDiscordCommand(or aPersistentComponentListener) that accepts anEventContext<?>and returns aResponse. Persistent components must use explicit user-suppliedcustomIdstrings so@Component(customId)handlers can route the click after hydration. See thehandler/response/andhandler/PersistentComponentHandlersections below for the full flow.
Components are a top-level package (component/), independent of response/. They are quality-of-life builders for their Discord4J counterparts and can be constructed independently.
component/ — Component (interface), TextDisplay
component/interaction/ — Button, SelectMenu, TextInput, Modal,
RadioGroup, Checkbox, CheckboxGroup
component/layout/ — ActionRow, Container, Section, Separator, Label
component/media/ — Attachment, FileUpload, MediaData, MediaGallery, Thumbnail
component/capability/ — application-level behavioral contracts:
EventInteractable, ModalUpdatable, Toggleable, UserInteractable
component/scope/ — Discord placement scoping interfaces:
ActionComponent, LayoutComponent, AccessoryComponent, ContainerComponent,
LabelComponent, SectionComponent, TopLevelMessageComponent, TopLevelModalComponent
Components support Discord's Components V2 flag (IS_COMPONENTS_V2) — detected automatically when v2 component types are present.
Every event gets a typed context wrapping the Discord4J event:
context/ — EventContext
context/scope/ — MessageContext, InteractionContext, DeferrableInteractionContext,
CommandContext, ComponentContext, ActionComponentContext
context/capability/ — ExceptionContext, TypingContext
context/command/ — SlashCommandContext, UserCommandContext,
MessageCommandContext, AutoCompleteContext
context/component/ — ButtonContext, SelectMenuContext, OptionContext, ModalContext,
CheckboxContext, CheckboxGroupContext, RadioGroupContext
context/message/ — ReactionContext
ComponentContext extends both MessageContext and DeferrableInteractionContext (diamond via interfaces).
Contexts provide: reply(), edit(), followup(), presentModal(), deleteFollowup(), and access to the cached Response/CachedResponse.
There are two parallel listener hierarchies, both auto-registered via classpath scanning of the dev.sbs.discordapi.listener package:
DiscordListener<T extends discord4j.core.event.domain.Event>— handles Discord4J gateway events. Subscribed to Discord4J'sEventDispatcher. Errors are routed through theExceptionHandlerchain.BotEventListener<T extends BotEvent>— handles bot-internal events emitted byDiscordBotitself (lifecycle hooks, future custom events). Subscribed to aSinks.Many<BotEvent>replay sink owned byDiscordBot(last 16 events replayed to late subscribers, so listeners registered insideconnect()still receive events emitted duringlogin()). Errors are logged locally.
Additional listeners of either type can be registered through DiscordConfig.Builder.withListeners() (Discord4J events) or withBotEventListeners() (bot events).
listener/ — DiscordListener, BotEventListener (base classes)
listener/command/ — SlashCommandListener, UserCommandListener,
MessageCommandListener, AutoCompleteListener
listener/component/ — ComponentListener, ButtonListener, SelectMenuListener,
ModalListener, CheckboxListener, CheckboxGroupListener,
RadioGroupListener
listener/message/ — MessageCreateListener, MessageDeleteListener,
ReactionListener, ReactionAddListener, ReactionRemoveListener
listener/lifecycle/ — DisconnectListener (BotEventListener), GuildCreateListener
listener/ — PersistentComponentListener (base for shared @Component
and @PersistentResponse hosts; classpath-scanned at startup)
— Component (annotation in dev.sbs.discordapi.listener)
dev.sbs.discordapi.event houses internal events that are emitted by DiscordBot and consumed by BotEventListener subclasses. Lifecycle hooks (onClientCreated, onGatewayConnected, onGatewayDisconnect) are NOT exposed as protected methods on DiscordBot — DiscordBot is the single bridge that translates Discord4J gateway events into bot events, and listeners are the only extension point.
event/ — BotEvent (marker interface)
event/lifecycle/ — ClientCreatedBotEvent, GatewayConnectBotEvent,
GatewayDisconnectBotEvent
handler/exception/ — pluggable error handling chain:
ExceptionHandler— abstract base class (extendsDiscordReference)DiscordExceptionHandler— formats errors into Discord embeds, sends to user and developer log channelSentryExceptionHandler— captures exceptions to Sentry with enriched Discord context tagsCompositeExceptionHandler— chains multiple handlers in sequence
handler/response/ — two-tier response cache (hot in-memory + optional cold JPA):
ResponseLocator— reactive interface exposingfindByMessage,findForInteraction,findByResponseId,findFollowupByIdentifier,store,storeFollowup,update,remove,findExpired. Persistence branching is internal to implementations.InMemoryResponseLocator— hot tier backed by auniqueId → CachedResponsemap plus amessageId → uniqueIdindex for O(1) lookups.JpaResponseLocator— cold tier writingPersistentResponseEntityrows via rawJpaSession.transaction(...). Reads run onSchedulers.boundedElastic().CompositeResponseLocator— wraps both tiers, performs cold-tier hydration on hot-tier miss via thePersistentComponentHandlerbuilder route registry.CachedResponse— single concrete entry type representing both top-level replies and followups (followups haveparentIdset). Lifecycle is aStateenum (IDLE,BUSY,DEFERRED); content dirty-tracking flows throughResponse.isCacheUpdateRequired(). Persistent entries carryownerClass+builderIdfor hydration.NavState—@GsonType-marked snapshot of the mutable navigation coordinates (current page, item page, page history) persisted to thenav_stateJSON column.jpa/PersistentResponseEntity—@Entitybacking thediscord_persistent_responsetable; followups are independent rows withparent_idset, cascade-deleted in app code.
handler/PersistentComponentHandler — routing registry for persistent component interactions:
- Scanned at bot startup over loaded
DiscordCommandinstances andPersistentComponentListenersubclasses. @Component(customId)-annotated methods (indev.sbs.discordapi.listener.Component) register a route from custom id →MethodHandle. Methods take aComponentContextsubtype and return aPublisher<Void>.@PersistentResponse([id])-annotated methods (indev.sbs.discordapi.response.PersistentResponse) register a route from(ownerClass, builderId)→MethodHandle. Methods take anEventContext<?>and return aResponse. Invoked at both creation time (with the dispatching command's context) and hydration time (with aHydrationContext).HydrationContext(context/HydrationContext) is a lightweightEventContext<ComponentInteractionEvent>that intentionally does NOT extendMessageContext, so builder methods cannot accidentally callgetResponse()against a not-yet-existing cache entry.
handler/DispatchingClassContextKey — Reactor Context key ("dev.sbs.discordapi.dispatching-class") carrying the dispatching Class<?> through the reactive pipeline. Written by DiscordCommand.apply (around process()) and by ComponentListener.dispatchPersistent (around the @Component invocation). Read by InMemoryResponseLocator.store to bind the owner class to persistent entries.
Persistent response flow:
DiscordConfig.Builder.withJpaConfig(JpaConfig)enables the cold tier;DiscordBotderives an internalJpaConfigwhoseRepositoryFactoryscansdev.sbs.discordapi.handler.response.jpaand connects its ownJpaSessionso the discord-api entity discovery is independent of any user-supplied factory.- A command's
process()callscontext.reply(Response.builder().isPersistent(true)...build()).EventContext.replydelegates toresponseLocator.store, which writes through to both the hot tier and the cold tier. - After a restart, when the user clicks the persistent component,
ComponentListenercallsresponseLocator.findForInteraction(event). The composite locator misses the hot tier, hits the JPA row, looks up the registered@PersistentResponsebuilder viaPersistentComponentHandler, invokes it with aHydrationContextto rebuild theResponse, restores the persistedNavState, seeds the hot tier, and returns the hydratedCachedResponse. ComponentListener.dispatchPersistentthen routes to the@Component-annotated handler viaMethodHandle, wrapping the publisher with the dispatching class context key so any nestedreply()from inside the handler is also persistence-aware.
response/handler/ — page navigation and pagination:
HistoryHandler<P, I>— generic stack-based page navigation (sibling and child navigation viaSubpages)PaginationHandler— builds pagination components (buttons, select menus, sort/filter/search modals) with emoji accessOutputHandler<T>— interface for cache-invalidation contractItemHandler<T>— interface for paginated item lists; implementations:EmbedItemHandler(embed fields),ComponentItemHandler(sections)FilterHandler/SortHandler/SearchHandler— item filtering, sorting, and search stateFilter/Sorter/Search— builder-pattern definitions for filter/sort/search criteria
DiscordReference— base class for anything needing bot access; providesgetDiscordBot(),getEmoji(),isDeveloper(), permission helpers.Component.Typeenum maps to Discord's integer component type IDs and tracks which types require the Components V2 flag.- Library dependencies — declared directly as JitPack coordinates (
com.github.simplified-dev:*:master-SNAPSHOT) inbuild.gradle.kts. The module is standalone and does not depend on a SkyBlock-Simplifiedapimodule.