Lazybot is a self-hosted osu! score bot built on Java 21 and Spring Boot. It exposes two chat frontends — Discord (via JDA) and QQ (via the Shiro/OneBot adapter) — that both funnel incoming slash commands through a single, shared chain-of-responsibility pipeline before dispatching them to a matched Spring bean. Image results are rendered server-side: raw osu! API data is transformed into value objects, mapped onto SVG templates, and then rasterised to JPEG or PNG via a Resvg JNI binding.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Apeuriox/lazybot-renewal/llms.txt
Use this file to discover all available pages before exploring further.
Bot Frontends
| Frontend | Library | Protocol |
|---|---|---|
| Discord | JDA (Java Discord API) | Discord Gateway / REST |
| Shiro (mikuac/shiro-core) | OneBot v11 WebSocket |
LazybotSlashCommandEvent and pass it — together with the resolved LazybotSlashCommand bean — into CommandChainProcessor. This means every feature, permission check, and rate-limit rule is enforced identically on both platforms.
Command Registry
At startup,LazybotSlashCommandRegistry iterates over every Spring bean that implements LazybotSlashCommand and reads its @LazybotCommandMapping annotation to extract the command’s aliases.
HashMap<String, RegisteredCommand>. Each RegisteredCommand record also captures whether the class is annotated with @SkipLazybotCommandPreprocessing, which is checked before the event enters the chain (see below).
The legacy LazybotCommandScanner (classpath-reflection-based approach) is retained in the repository as commented-out code and is no longer active.
Command Pipeline
CommandChainProcessor.process() creates a fresh CommandHandlerChain backed by all CommandHandlerInterface beans, ordered by Spring’s @Order annotation, and calls chain.doHandle(). Each handler either short-circuits (throws or returns) or calls chain.doHandle() again to pass control to the next stage.
PermissionChainHandler (@Order 0)
Checks two permission scopes in sequence:
- CHANNEL — queries
PermissionService.checkPermission("CHANNEL", groupId, command, version)to decide whether the command has been disabled in the current QQ group. - GLOBAL — queries the same service with scope
"GLOBAL"and id0to check for a bot-wide developer disable.
USER-scoped check is present in the source but is currently commented out. If the calling user’s ID is in the hardcoded adminBypass list, all permission checks are skipped entirely and the chain proceeds directly to the next handler.Failing either check throws a LazybotRuntimeException with a human-readable message that is surfaced back to the user.HelpChainHandler (@Order 1)
Inspects the first parsed command parameter. If it equals
*help or *h (case-insensitive), the handler calls command.getHelp() on the matched bean and sends the result back to the channel — without advancing the chain. This means the targeted command’s business logic is never executed for help requests.RateLimitHandler (@Order 2)
Reads the
@LazybotRateLimit annotation from the command class. If no annotation is present the handler is a no-op and passes control through immediately.When an annotation exists, a bucket key is constructed from the configured scope (USER, CHANNEL, or GLOBAL) combined with the command type string. LazybotCommandRateLimitManager.tryConsume() attempts to remove one token from the Bucket4j bucket. On QQ, a failed consume sends a cooldown notice (“请等待50秒”) and stops the chain. On the Discord test path, a notice is written to the test output file but the chain continues (this reflects the current test-mode behaviour in the source).CommandExecutorHandler (@Order 3)
The terminal handler. Calls
command.execute(bot, event) (QQ path) or command.execute(event) (Discord path), delegating to the matched LazybotSlashCommand bean. No further chain forwarding occurs here — this is where the actual osu! API call, rendering, and reply happen.Skipping the Chain
Commands annotated with@SkipLazybotCommandPreprocessing bypass the common OneBot preprocessing step (mode normalisation, panel-version extraction, punctuation and whitespace canonicalisation, lower-casing). The registry records this flag per command so the event router can check LazybotSlashCommandRegistry.shouldSkipPreprocessing(commandName) before constructing the event.
Image Rendering Pipeline
Score cards, profile panels, beatmap statistics panels, and PP+ cards are all rendered server-side as rasterised images.Service layer fetches data
PlayerService (implemented by PlayerServiceImpl) calls the osu! API v2 and, where needed, the PP+ service (“lazybot plus”). The raw JSON responses are deserialised into DTO objects and then assembled into rich value objects (VOs) such as ScoreVO, PlayerInfoVO, PPPlusScore, or PerformancePlusProfile.RendererDistributor selects a template
RendererDistributor is the single entry point for all render operations. Each static method takes a specific VO type and delegates to the appropriate SVG mapper class (e.g. ScoreSVGMapper, PlayerInfoSVGMapper, PlusCardSVGMapper). The mapper uses SVGTemplateLoader, which calls Apache Batik’s SAXSVGDocumentFactory to parse the SVG template from src/main/resources/static/. Dynamic text, colours, and images are then written into the resulting org.w3c.dom.Document using standard DOM API calls.SVGRenderer rasterises the document
SVGRenderer receives the populated org.w3c.dom.Document. It serialises it to an SVG string via SvgUtil.documentToString() and hands it to ResvgJNI.Renderer, a JNI wrapper around the Resvg Rust library. The result is a byte[] of JPEG (.RenderJpg) or PNG (.RenderPng) data, which is sent back to the chat platform as an image message.TemplateRenderer component provides an alternative rendering path that posts a RenderRequest payload to a local sidecar process (http://localhost:9090/api/render) instead of invoking Resvg directly. This path is used for specific score panels that delegate rendering to an external Takumi service.
External Integrations
osu! API v2
Primary source of player stats, scores, and beatmap metadata. OAuth2 client credentials are configured under
lazybot.client_id / lazybot.client_secret. Per-user OAuth tokens obtained via the /link oauth flow are stored and refreshed by OsuUserTokenService.PP+ Service (lazybot plus)
An auxiliary performance-plus service accessed with credentials at
lazybot.plus.client_id / lazybot.plus.client_password. Provides extended performance data used by commands such as /ppplus and /plusbp.osuTrack
Used to fetch historical score snapshots and user-difference data. DTOs are located under
dto/osuTrack/. Powers the /update command’s rank-change tracking.Sayobot
A Chinese mirror of the osu! beatmap catalogue.
AssetDownloader queries Sayobot first when downloading beatmap cover images, falling back to the official osu! CDN if Sayobot does not have the asset.Star Moon Private Server
A Chinese osu! private server. Linked via
/linksm <username> using OsuServer.STAR_MOON. Score and profile commands in the command/starmoon/ package target this server’s API instead of Bancho.