medusae

A lightweight client for building Discord bots using both the Gateway and REST APIs.

Medusae (formerly jellycord)

Medusae is a Java library for building Discord bots with two complementary command models that share the same gateway and REST core.

  • Interaction Router API (DiscordClient + InteractionContext): lightweight, handler-first, minimal abstraction.
  • Annotation Command Framework (com.github.cybellereaper.commands): scalable typed commands with reusable checks, cooldowns, and schema sync.

Start small with direct handlers, then migrate to annotations as your bot surface grows.

Features

  • Gateway lifecycle management and event subscription
  • Slash commands, context menus, autocomplete, components, and modals
  • High-level interaction response helpers
  • REST convenience layer (DiscordApi)
  • Retry/backoff controls and rate-limit observability hooks
  • Optional in-memory state cache
  • Attachment upload helpers
  • Voice transport primitives for gateway/audio frame workflows

Installation

implementation 'com.github.cybellereaper:Medusae:1.0.0'

Choose a framework

Goal Recommended API
Small bot, direct handlers, low ceremony Interaction Router API
Typed modules, reusable policies, scalable command architecture Annotation Command Framework
Fine-grained endpoint control DiscordApi (works with either framework)

Quick start: Interaction Router API

String token = System.getenv("DISCORD_BOT_TOKEN");

DiscordClientConfig config = DiscordClientConfig.builder(token)
        .intents(GatewayIntent.combine(GatewayIntent.GUILDS, GatewayIntent.GUILD_MESSAGES))
        .build();

try (DiscordClient client = DiscordClient.create(config)) {
    client.onSlashCommandContext("ping", ctx -> ctx.respondWithMessage("pong"));

    client.onSlashCommandContext("echo", ctx -> {
        String text = ctx.requiredOptionString("text");
        ctx.respondEphemeral("You said: " + text);
    });

    client.registerGlobalSlashCommand("ping", "Reply with pong");
    client.registerGlobalSlashCommand("echo", "Echo text");

    client.login();
    Thread.currentThread().join();
}

Router highlights

  • Register handlers with on*Context(...)
  • Read typed options and send responses through InteractionContext
  • Support slash commands, components, autocomplete, and modal submissions

Quick start: Annotation Command Framework

CommandFramework framework = new CommandFramework();

framework.registerCheck("guildonly", ctx -> !ctx.interaction().dm());
framework.registerAutocomplete("membersearch", (ctx, value) -> List.of("alice", "bob"));
framework.registerCommands(new UserCommands());

DiscordCommandSyncService sync = new DiscordCommandSyncService(framework);
sync.syncGlobal(discordClient);

Annotation highlights

  • Slash commands, subcommands, and subcommand groups
  • User/message context commands
  • Typed parameter binding with custom resolvers
  • Declarative checks, permissions, cooldowns, and autocomplete
  • Discord schema export + sync service

See docs-command-framework.md for full details.

Annotated gateway events

You can register gateway listeners with annotations in the same module-centric style:

AnnotatedGatewayEventBinder binder = new AnnotatedGatewayEventBinder();
binder.bind(discordClient, new ModerationEvents());
@EventModule
public final class ModerationEvents {
    @OnGatewayEvent(value = "READY", payload = ReadyEvent.class)
    public void onReady(ReadyEvent event) {
        System.out.println("Session: " + event.sessionId());
    }

    @OnGatewayEvent(value = "MESSAGE_CREATE", payload = MessageCreateEvent.class)
    public void onMessage(MessageCreateEvent event, DiscordClient client) {
        if ("!ping".equals(event.content())) {
            client.api().sendMessage(event.channelId(), DiscordMessage.ofContent("pong"));
        }
    }
}

Handler signature rules:

  • Exactly one payload parameter compatible with payload()
  • Optional DiscordClient parameter for client access
  • Handler methods cannot be private

Core configuration examples

Sharding

DiscordClientConfig config = DiscordClientConfig.builder(token)
        .intents(GatewayIntent.combine(GatewayIntent.GUILDS, GatewayIntent.GUILD_MESSAGES))
        .shard(1, 4) // shardId=1 out of 4 total shards
        .build();

OAuth scopes

String scopes = DiscordOAuthScopes.join(
        DiscordOAuthScopes.BOT,
        DiscordOAuthScopes.APPLICATIONS_COMMANDS
);

Reliability hooks

RateLimitObserver observer = new RateLimitObserver() {
    @Override
    public void onRetryScheduled(String method, String path, int attempt, Duration backoff, String reason) {
        System.out.println("retry " + method + " " + path + " attempt=" + attempt + " cause=" + reason);
    }
};

DiscordClient client = DiscordClient.create(
        config,
        RetryPolicy.defaultPolicy(),
        observer,
        true // enable state cache
);

REST convenience calls

JsonNode currentUser = client.api().getCurrentUser();
JsonNode channel = client.api().getChannel("1234567890");
client.api().deleteMessage("1234567890", "9876543210");

Attachment uploads

client.sendMessageWithAttachments(
        "123",
        DiscordMessage.ofContent("upload"),
        List.of(DiscordAttachment.fromPath(Path.of("/tmp/demo.png")))
);

Additional documentation

Run tests

./gradlew test
Repository

medusae

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • 1 day ago
  • April 5, 2026
License

GNU General Public License v3.0

Links
Synced at

Mon, 27 Apr 2026 11:31:47 GMT

Languages