discord

    An experimental Discord library for MoonBit: native/JS/Wasm REST, interactions, and gateway bots; native voice with DAVE E2EE.

    discord
    bot
    gateway
    api
    voice
    interactions
    Download zip
    Author
    Version
    0.5.0
    License
    Apache-2.0
    Last updated
    6 hours ago
    Downloads
    71

    #discord.mbt

    CI Voice shim Release mooncakes Ask DeepWiki License

    A Discord application library for MoonBit: typed interaction declarations, API models, a rate-limited REST client, native/JS/moonrun Wasm HTTP interactions and WebSocket gateway shards.

    The design follows twilight: loosely coupled packages that model the Discord API, plus an App layer for typed interaction declarations.

    Status: experimental. Minor releases may contain breaking changes; see the changelog for migration notes.

    Long-form guides live in src/guide/; their code blocks compile and run as part of the test suite, so the examples cannot drift from the library. Task-focused recipes are docstring examples on the relevant symbols — look anything up with moon ide doc (see Development).

    #Prerequisites

    On a clean machine, install git and run moon update before resolving the module dependencies. Moon uses git to clone the package registry.

    Node.js must be on PATH for every build, on any target and whether or not the bot uses voice. gaato/discord and its gaato/dave dependency declare prebuild hooks that Moon runs with node; without the voice opt-in variables they exit without downloading anything, but moon build still fails when node is missing. moon check does not run the hooks.

    Every native build compiles the Gateway package's zlib_stream.c, which always includes <zlib.h> even when compress=false. The zlib development headers are therefore required for all native builds: install zlib1g-dev on Debian/Ubuntu or zlib-devel on Fedora/openSUSE. The zlib shared library is additionally required at runtime when native Gateway zlib-stream compression is used.

    #Install

    moon update moon add gaato/discord

    The interaction App, REST client, data packages, and gateway Bot executor support native, JavaScript, and linear-memory Wasm on moonrun. Voice is native-only. These executors use moonbitlang/async.

    #Quickstart

    Build the interaction App, then pass it to the gateway Bot executor. This example registers and answers a /echo command. See src/examples/slash_echo for the runnable version.

    ///|
    struct QuickstartEchoArgs {
    text : String
    times : Int
    }

    ///|
    async fn run_echo_bot(token : String) -> Unit {
    let args : @discord.Args[QuickstartEchoArgs] = @discord.Args::map2(
    @discord.arg_string(name="text", description="What to echo"),
    @discord.arg_int32(
    name="times",
    description="How many times (1-5)",
    min=1,
    max=5,
    ).with_default(1),
    (text, times) => { text, times, },
    )
    let echo = @discord.slash(
    name="echo",
    description="Echo your text back",
    args~,
    handler=Immediate((_ctx, value) => {
    @discord.CommandReply::message(
    content=Array::make(value.times, value.text).join("\n"),
    )
    }),
    )
    let app = @discord.App()
    app.command(echo)
    let bot = @discord.Bot(app, token~, sync=Global)
    bot.on(@discord.Events::ready(), (_ctx, ready) => {
    println("ready as \{ready.user.username}")
    })
    bot.run()
    }

    #Packages

    PackageWhat it isNativeJSWasm¹
    gaato/discordFacade: golden-path names a typical application uses directlyYesYes²Yes²
    gaato/discord/modelPure data: ~24 entity domains, gateway payloads, zero IOYesYesYes
    gaato/discord/telemetryStructured REST, gateway, and dispatch observability valuesYesYesYes
    gaato/discord/httpREST Client, routes, rate limiting, multipart uploadsYesYesYes
    gaato/discord/gatewayShard: connection state machine, heartbeat, resumeYesYesYes
    gaato/discord/voiceExperimental native voice gateway v8, DAVE, RTP, and Opus send/receiveYesNoNo
    gaato/discord/interactionCommand/component builders, typed args and autocomplete dataYesYesYes
    gaato/discord/frameworkInteraction routing, response gates, low-level response contextsYesYesYes
    gaato/discord/appGateway-free typed commands/components/modals, HTTP endpoint, sync and policyYesYesYes
    gaato/discord/botGateway executor and typed gateway event descriptorsYesYesYes
    gaato/discord/endpoint_httpSigned-interactions HTTP server (serve_interactions)YesNoYes
    gaato/discord/cacheOpt-in, gateway-driven in-memory cacheYesYesYes
    gaato/discord/utilPure helpers: permissions, mentions, timestamps, CDN URLsYesYesYes
    gaato/discord/verifyPure MoonBit Ed25519 request verificationYesYesYes
    gaato/discord/ratelimitRate limiter trait + in-memory implementationYesYesYes
    gaato/discord/cooldownCommand cooldown store trait + in-memory fixed windowsYesYesYes
    gaato/discord/queueIdentify queue trait + in-memory implementationYesYesYes
    gaato/discord/coordinatorExperimental TCP coordinator for multi-process Identify, REST limits, and cooldownsYesNoYes
    gaato/discord/testkitDeterministic interaction and model fixturesYesYesYes

    ¹ Linear-memory Wasm run by moonrun. The host imports come from moonbitlang/async, so generic WASI runtimes, browsers, and wasm-gc are not targets. See Wasm runtime and permissions.

    ² Voice exports exist only on native. The HTTP-server exports exist on native and Wasm. Gateway zlib-stream compression is native-only; check zlib_stream_supported() before enabling it. As on JS, the root retains DAVE in its dependency graph, but gaato/dave.available() is false on Wasm and its safe constructors raise LibraryUnavailable. This does not enable DAVE or Voice. Packages that are empty on a backend do not provide that feature, even if Moon accepts them as dependencies.

    #Wasm runtime and permissions

    Use --target wasm with the pinned MoonBit toolchain and its matching moonrun. This backend uses MoonBit host APIs for sockets, TLS, timers, and environment variables; it is not a browser, generic WASI, or wasm-gc application target. Wasm builds do not link the voice/DAVE native libraries, and Gateway zlib-stream compression is unavailable (leave compress off). Node is still required for the build-time prebuild hooks described above.

    The interactions_http example also builds for Wasm:

    moon build --target wasm --release src/examples/interactions_http moonrun --policy discord-policy.json _build/wasm/release/build/examples/interactions_http/interactions_http.wasm

    An example discord-policy.json for that server:

    { "env": { "required_from_host": ["DISCORD_TOKEN", "PUBLIC_KEY"], "from_host": ["GUILD_ID"], "set": {"PORT": "8080"} }, "net": { "connect": ["discord.com:443"], "bind": ["0.0.0.0:8080"] } }

    Supply credentials through the environment, never in the policy file. This example synchronizes commands at startup; run it only when you intend to update your application's commands. A REST-only program needs the token and outbound connection permission but no bind permission. The coordinator needs bind permission on its server address and connect permission on each client. Add other destinations only for features that actually need them. In policy mode, omitted environment, filesystem, network, and process permissions are denied.

    Packages remain usable on their own. Import only what the program needs:

    The facade follows the typical bot's golden path. Import the focused http, framework, gateway, coordinator, telemetry, or interaction package for resource refs, pagination, low-level routing and raw contexts, shard transport, coordination, observability events, or raw option declarations.

    • REST-only tool: http (brings model).
    • Serverless HTTP interactions (Cloudflare Workers, Deno, Bun, and Node.js Functions, JS): app and verify.
    • Native HTTP interactions server: endpoint_http.
    • Gateway bot: bot, or the gaato/discord facade for everything.

    #Examples

    Runnable programs live under src/examples/:

    • slash_echo: minimal typed Gateway bot.
    • kitchen_sink: subcommands, autocomplete, context menus, components, and modals.
    • ping_gateway: low-level Gateway and REST use.
    • low_level: manual Framework and Shard wiring.
    • workers_echo: Cloudflare Workers adapter.
    • deno_echo: Deno adapter for the same MoonBit interaction App.
    • bun_echo: Bun HTTP server adapter for the same interaction App.
    • vercel_echo: Vercel Node.js Function adapter for the same interaction App.
    • fastly_echo: Fastly FetchEvent entry point experiment.
    • lambda_url_echo: Lambda Function URL payload v2 adapter experiment.
    • workers_gateway: Cloudflare Durable Object running a gateway bot (no voice).
    • interactions_http: native signed-interactions HTTP server.
    • experiments/spin_interactions: WASIp2 Spin component for signed PING and immediate echo; this experiment sits outside src/examples/.
    • plugin_demo: a stateful feedback feature installed as a separate package.
    • gate_probe: live probe for error-policy recovery and user-restricted component waits (needs a Discord client; guild commands only).
    • voice_player: join a voice channel and play an Ogg/Opus file.
    • voice_recorder: record a user's voice to an Ogg/Opus file.

    #App core and executors

    App owns the interaction declaration: commands, components, modals, autocomplete routes, and the error policy. It has no gateway dependency. After building an App, choose an executor:

    • Bot(app, token~) connects to the gateway and routes InteractionCreate events through the App.
    • app.serve(group, token~) creates an InteractionEndpoint for an HTTP adapter.

    Both executors use the same handlers, so an application can move between a persistent gateway process and an HTTP or serverless deployment without rewriting its interaction declarations.

    Command[A] pairs one handler with Args[A]; the argument value drives both Discord's registration payload and interaction decoding (build records with Args::map1 through Args::map8). Choose a handler mode against Discord's three-second initial-response deadline: Immediate computes and returns a CommandReply before it, Deferred acknowledges first and continues on a DeferredCtx, and Raw receives the underlying CommandCtx for imperative flows. Subcommands carry their own typed Args, autocomplete attaches directly to the focused argument, and context-menu commands, components, and typed modals route through the same App — see the commands and components and modals guides.

    Client, App, and Bot each accept onion-style middleware — around one logical REST call, around routed interaction dispatch, and around gateway event fan-out — plus structured telemetry callbacks. See the middleware guide.

    #Gateway executor

    Bot adds typed gateway event subscriptions and services:

    bot.on(@discord.Events::message_create(), (ctx, event) => {
    let message = event.message
    println("\{message.author.username}: \{message.content}")
    })

    If intents is omitted, Bot derives non-privileged intents from typed subscriptions; pass privileged intents (members, presences, message content) explicitly, and pass the full set when raw event handlers are used. ctx.wait_for(...) makes one-shot typed event observations inside a handler. The events and intents guide covers descriptors, intent derivation, decode-error observers, opt-in zlib-stream compression, and the gateway-driven in-memory cache.

    Several shards can run in one process behind the same dispatch path, so handlers, the cache, collectors, and telemetry observe every shard:

    ///|
    let bot = @discord.Bot(app, token~, shards=Auto)

    Identify calls honor the max_concurrency bucket rules from GET /gateway/bot, and multi-process deployments can share Identify and REST limits through the bundled TCP coordinator. See the scaling guide.

    #Voice (experimental)

    Native builds can join voice gateway v8 calls, play 20 ms Opus frames, and receive per-user Opus streams. Discord requires DAVE encryption, so voice applications use the native-only gaato/dave binding to Discord's official libdave for MLS and media encryption. The separate Rust library in voice-shim/ handles RTP transport AEAD only. gaato/dave currently pins upstream v1.2.0/cpp; gaato/discord pins transport component release voice-shim-v0.1.0. A native voice build can bootstrap and verify both prebuilt runtimes without installing Cargo. See the voice guide for installation and runtime limits, the dave_probe, voice_player, and voice_recorder examples, and the accepted design in src/voice/DESIGN.md.

    #Synchronization and failures

    CommandScope selects global, guild, or multi-guild registration. Pass sync=scope to Bot to synchronize once after the first READY; when omitted, the bot makes no command request. HTTP executors never synchronize commands. For those deployments, call app.sync_commands(client, application_id,scope~) from a one-shot registration program. It returns an @app.SyncReport whose scopes list each scope's created, updated, deleted, unchanged, and preserved command names, plus whether an overwrite was sent.

    Synchronization fetches the remote catalog and only sends a bulk-overwrite PUT when it differs. By default (unowned=Delete), it removes undeclared commands from the selected scope. The Entry Point command is always preserved, including its id and fields unknown to this library. Use unowned=Keep on App::sync_commands or Framework::sync_*, or sync_unowned=Keep on Bot, when another process owns commands in the same scope. Bot emits one warning per scope with deletions through app.on_warn. Use exactly one process for synchronization in a multi-process deployment.

    ///|
    async test "sync preserves entry points and skips an unchanged catalog" {
    let spec = @interaction.CommandSpec::slash("ping", "Ping")
    let (payload, report) = @framework.plan_command_sync(
    [spec],
    [spec.to_json(), { "id": "10", "type": 4, "name": "Launch", "handler": 2 }],
    unowned=Delete,
    )
    assert_true(payload is None)
    assert_false(report.overwritten)
    assert_eq(report.preserved, ["4:Launch"])
    }

    Install app.error_policy(...) to map failures to logs or interaction responses; handlers raise HandlerError variants such as UserMessage or MissingPermission for expected failures, and commands accept ordered pre-execution checks and fixed-window cooldowns. See the structuring bots guide.

    #HTTP interactions (experimental)

    App::serve(group, token~) starts the HTTP interaction executor and returns an InteractionEndpoint that uses the App's declarations and error policy without opening a gateway. handle accepts decoded interaction JSON and returns callback JSON for Reply; the caller owns the task group, so handlers that defer keep running on it after handle returns:

    ///|
    async fn handle_http_interaction(
    app : @discord.App,
    group : @async.TaskGroup[Unit],
    token : String,
    body : Json,
    ) -> Json? {
    app.serve(group, token~).handle(body)
    }

    The Cloudflare Worker, Deno, Bun, and Vercel Function HTTP adapters verify the raw request bytes with the reusable pure MoonBit @discord.InteractionVerifier before parsing. The dispatch bridge is App::start_signed_http in the library. The src/examples/interactions_js example is an exported wrapper plus a small handler.js that maps a Web Request to InteractionHttpRequest and back; the per-host adapters (Workers, Deno, Bun, Vercel, Fastly, Lambda) only wire their entry point and lifetime API. The shared handler is tested on Node.js and Bun, while the Vercel lifecycle adapter is tested with an injected waitUntil collector. These local tests do not establish hosted Vercel deployment behavior. workers_echo is tested inside Cloudflare's local workerd runtime and supports streamed multipart callbacks for in-memory FileUpload values. On native, @discord.serve_interactions(group, app, addr~, public_key~, token~) is a complete signed-interactions HTTP server. See the HTTP interactions guide and the interactions_http, workers_echo, deno_echo, bun_echo, and vercel_echo examples. The separate Spin experiment uses WASIp2 and Spin's variables import for an immediate handler. It does not yet run the async App on a general Wasm host.

    The JavaScript host checks cover different boundaries:

    HostChecked hereRemaining host integration
    Cloudflare WorkersSigned requests and deferred REST in local workerdProduction deployment
    DenoDeno.serve adapter and signed requestsHosted lifecycle
    BunShared MoonBit handler and Bun.serve loopback endpointHosted lifecycle
    Node.jsShared MoonBit handler and Web APIsHTTP hosting and process lifecycle
    Vercel FunctionsNode.js fetch adapter and waitUntil handoffVercel packaging and hosted lifecycle
    Fastly ComputeFetchEvent and synchronous lifetime registration with a fake eventFastly compilation and hosted execution
    Lambda Function URLPayload v2 body and response conversion with local eventsAWS invocation and deployment

    Other JavaScript function platforms can reuse the handler if they provide Web Request, Response, crypto.subtle, and fetch. Their entry point, background-work API, deployment bundle, and execution limits still need platform-specific checks. The Spin experiment instead tests the WASIp2 component boundary. The host entry point survey records the other host conventions and the remaining API decisions, including multipart responses and background-work lifetime.

    The JavaScript REST client supports null-body 204 responses through moonbitlang/async@0.22.1; the workers_echo workerd suite covers a deferred interaction-response deletion end to end. Voice remains native-only; the gateway also runs on JavaScript and moonrun Wasm.

    #Cloudflare Workers gateway

    The workers_gateway example runs one gateway shard in a Durable Object with an outbound WebSocket. A bot accepts saved BotSession values through resume~; bot.sessions() returns snapshots containing both the Gateway session and its original READY metadata. The example stores them on a 30-second alarm so a restarted bot can process replayed events, including interactions arriving before RESUMED. Call bot.sessions() while bot.run() is active to capture live sessions:

    ///|
    fn restored_bot(
    app : @discord.App,
    token : String,
    saved : Map[Int, @discord.BotSession],
    ) -> @discord.Bot {
    @discord.Bot(app, token~, resume=saved)
    }

    ///|
    fn current_bot_sessions(bot : @discord.Bot) -> Map[Int, @discord.BotSession] {
    bot.sessions()
    }

    An alarm restarts a failed bot, and a five-minute cron reconciles the object's stored enabled state after eviction or deployment. /stop disables those restarts and clears the saved session because a graceful close invalidates it; the public controls require a separate bearer secret. Snapshots are periodic: they do not guarantee exactly-once event handling or restore application state or optional cache contents. Cloudflare can evict an object after an outbound socket has protected it for up to 15 minutes. The JavaScript gateway does not support zlib-stream compression; received WebSocket messages are limited to 32 MiB, CPU to 30 seconds per event by default, and a connected object incurs duration charges. See the example README for deployment and local test details.

    #Typed models

    Every entity decodes from real API payloads, and unknown enum values and unknown JSON keys round-trip through Unknown(...) variants, so a new Discord feature does not break your application. IDs are phantom-typed snowflakes — a UserId cannot be passed where a ChannelId is expected — that keep full precision above 2^53:

    let id : @model.MessageId = @model.Id::parse("175928847299117063")
    println(id.timestamp_ms()) // 1462015105796

    Wire models use T? for optional fields and Nullable[T] for nullable fields; PATCH methods expose plain optional arguments with clear_<field>=true flags for clearing. See the models and utilities guide.

    #REST without the gateway

    http.Client stands alone: typed wrappers over the routes, per-bucket rate limiting with a global window, automatic 429 retry, multipart file uploads, and a raw client.request(route) escape hatch for anything not wrapped yet. Ordinary channel messages suppress @everyone and @here by default while still parsing user and role mentions.

    let client = @dhttp.Client(token)
    let message = client.create_message(channel_id, content="hello")
    client.create_message(channel_id, content="with reply", reply_to=message.id)
    |> ignore

    Every endpoint with a resource anchor is also available through client-bound refs, which chain from parent to child resource before applying a verb:

    client.guild_ref(guild_id).emoji_ref(emoji_id).edit(name="renamed") |> ignore

    Validation failures raise before any I/O, request failures use DiscordHttpError, and list endpoints expose stateful Paginator[T] values. The REST guide covers the full ref surface, allowed mentions, file uploads, pagination, and custom routes.

    Also in the box, each with its guide chapter:

    #Development

    For handler tests without a Discord connection, Client::offline answers REST calls from a function you write, and the native/JS/Wasm gaato/discord/testkit provides deterministic interaction and model fixtures. Import the testkit only in your package's for "test" block.

    moon check --target native --deny-warn moon check --target js --deny-warn moon check --target wasm --deny-warn moon test --target native --release # debug native builds need a working tcc setup moon test --target js --release moon test --target wasm --release moon fmt moon -C template fmt --check moon info --target native # regenerate pkg.generated.mbti (API review signal) scripts/gen_extends.py # regenerate extends.mbt after adding or removing a ToJson type

    Look up any package, type, or symbol — including its docstring examples — from the terminal:

    moon ide doc "@discord" moon ide doc "@http.Client::create_message" moon ide doc "@model.Message" moon ide doc "@util.channel_permissions"

    Each package's pkg.generated.mbti is a concise public-interface snapshot; review its diff when checking API drift.

    Every MoonBit code block in this README has a compiled twin in src/readme_native_test.mbt, and code blocks marked mbt check in the guides and in docstrings compile and run as part of the test suite.

    #License

    Apache-2.0

    AllowedMentionType

    A mention category Discord may derive directly from message content.

    AllowedMentions

    Controls which mentions Discord resolves and notifies for an outgoing message.

    Use AllowedMentions(...) so parse categories and explicit ID lists cannot conflict. The stored arrays are copied during construction to preserve that invariant if callers later mutate their input arrays.

    AllowedMentionsError

    An invalid allowed-mentions configuration rejected during construction.

    App

    using @gaato/discord/app { type App }

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    AppConfigError

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    AppCtx

    using @gaato/discord/app { type AppCtx }

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    Arg

    One typed command option, combining its registration definition and reader.

    Args

    A typed collection of command options used for registration and decoding.

    AudioSource

    Experimental native voice connection, playback, and receive APIs.

    Bot

    using @gaato/discord/bot { type Bot }

    Gateway executor and typed event descriptors (native, JavaScript, Wasm).

    BotError

    Gateway executor and typed event descriptors (native, JavaScript, Wasm).

    BotSession

    Gateway executor and typed event descriptors (native, JavaScript, Wasm).

    CheckCtx

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    Client

    Golden-path names that a typical bot uses directly: application declarations and handler contexts, typed arguments and components, common models, the REST client, executors, verification, and voice.

    Not re-exported — import the focused package instead:
    • gaato/discord/http for resource refs and pagination.
    • gaato/discord/framework for the low-level router and raw contexts used by Raw handlers.
    • gaato/discord/gateway for shard transport primitives.
    • gaato/discord/coordinator for multi-process coordination.
    • gaato/discord/telemetry for observability events.
    • gaato/discord/interaction for raw option builders, CommandSpec, and low-level option decoding.
    • gaato/discord/app for implementation-facing registration, spawning, waiting, and raw failure details.

    Command

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    CommandCheck

    type CommandCheck = async (
    CheckCtx
    ) -> Bool

    A command guard. Returning false produces HandlerError::CheckFailed; raise a more specific HandlerError for an expected denial. Any other error reaches the error policy unchanged.

    CommandReply

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    CommandScope

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ComponentDeferredCtx

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ComponentHandler

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ComponentImmediateCtx

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ComponentReply

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ComponentRoute

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    CooldownBucket

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    CustomIdCodec

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    CustomIdError

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    DeferredCtx

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ErrorPolicy

    type ErrorPolicy = async (
    FailureCtx
    , Error) -> Unit

    Policy invoked for interaction, event, and service failures.

    Event

    A gateway dispatch event decoded by its wire event name.

    Unknown event names retain both the name and payload for forward compatibility.

    EventType

    Gateway executor and typed event descriptors (native, JavaScript, Wasm).

    Events

    using @gaato/discord/bot { type Events }

    Gateway executor and typed event descriptors (native, JavaScript, Wasm).

    FailureCtx

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    FailureOrigin

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    FileUpload

    Golden-path names that a typical bot uses directly: application declarations and handler contexts, typed arguments and components, common models, the REST client, executors, verification, and voice.

    Not re-exported — import the focused package instead:
    • gaato/discord/http for resource refs and pagination.
    • gaato/discord/framework for the low-level router and raw contexts used by Raw handlers.
    • gaato/discord/gateway for shard transport primitives.
    • gaato/discord/coordinator for multi-process coordination.
    • gaato/discord/telemetry for observability events.
    • gaato/discord/interaction for raw option builders, CommandSpec, and low-level option decoding.
    • gaato/discord/app for implementation-facing registration, spawning, waiting, and raw failure details.

    GatewayCapabilities

    Gateway capabilities: the Identify bitfield that opts a client into gateway behaviors (as opposed to Intents, which select events). Unknown bits are preserved. Wire format: JSON number.

    test "opt into channel obfuscation ahead of general availability" {
    let capabilities = @model.GatewayCapabilities::channel_obfuscation()
    inspect(capabilities.bits(), content="32768")
    }

    GatewayCtx

    Gateway executor and typed event descriptors (native, JavaScript, Wasm).

    HandlerError

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ImmediateCtx

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    InitialResponse

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    Intents

    Gateway intents: the server-side event subscription bitfield sent in Identify. Unknown bits are preserved. Wire format: JSON number.

    test "compose the gateway event subscriptions a bot needs" {
    let intents = @model.Intents::guilds() |
    @model.Intents::guild_messages() |
    @model.Intents::message_content()
    inspect(
    intents.bits(),
    content=(
    #|33281
    ),
    )
    assert_true(intents.contains(@model.Intents::message_content()))
    }

    InteractionEndpoint

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    InteractionHandler

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    InteractionHttpBody

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    InteractionHttpRequest

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    InteractionHttpResponse

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    InteractionOutcome

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    InteractionVerifier

    Pure MoonBit verification for Discord HTTP interaction signatures.

    InteractionVerifierError

    Pure MoonBit verification for Discord HTTP interaction signatures.

    InteractionsServer

    Discord HTTP-interactions server for native and moonrun Wasm.
    using @gaato/discord/app { type Modal }

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ModalDeferredCtx

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ModalField

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ModalFields

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ModalHandle

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ModalImmediateCtx

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    ModalSubmitHandler

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    OggOpusError

    Experimental native voice connection, playback, and receive APIs.

    OggOpusSource

    Experimental native voice connection, playback, and receive APIs.

    OggOpusWriter

    Experimental native voice connection, playback, and receive APIs.

    ShardConfig

    Gateway executor and typed event descriptors (native, JavaScript, Wasm).

    SlashChild

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    SuggestCtx

    Data available while producing autocomplete choices. This context is transport-neutral and intentionally does not own an HTTP client.

    VoiceConnection

    Experimental native voice connection, playback, and receive APIs.

    VoiceConnectionState

    Experimental native voice connection, playback, and receive APIs.

    VoiceError

    Experimental native voice connection, playback, and receive APIs.

    VoiceEvent

    Experimental native voice connection, playback, and receive APIs.

    VoiceReceiveStream

    Experimental native voice connection, playback, and receive APIs.

    VoiceTelemetry

    Experimental native voice connection, playback, and receive APIs.

    WaitFrom

    Gateway-free application declarations, typed interaction handlers, command synchronization, and error policy contexts.

    API_VERSION

    let API_VERSION : Int

    Discord API version this library targets.

    SILENCE_FRAME

    let SILENCE_FRAME : Bytes

    Discord's canonical Opus silence frame.

    action_row

    Build a message component row.

    arg_attachment

    fn arg_attachment(name~ : String, description~ : String, file_types? : Array[
    FileTypeFilter
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [
    Attachment
    ]

    Define a required attachment option resolved to its attachment object. file_types restricts which files the client lets the user pick (by extension only; still validate the contents).

    arg_bool

    fn arg_bool(name~ : String, description~ : String, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [Bool]

    Define a required boolean option.

    arg_channel

    fn arg_channel(name~ : String, description~ : String, channel_types? : Array[
    ChannelType
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [
    Id
    [
    ChannelMarker
    ]]

    Define a required channel option.

    arg_int

    fn arg_int(name~ : String, description~ : String, min? : Int64, max? : Int64, choices? : Array[
    CommandOptionChoice
    ], suggest? : async (
    SuggestCtx
    , Int64) -> Array[
    CommandOptionChoice
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [Int64]

    Define a required integer option.

    arg_int32

    fn arg_int32(name~ : String, description~ : String, min? : Int, max? : Int, choices? : Array[
    CommandOptionChoice
    ], suggest? : async (
    SuggestCtx
    , Int) -> Array[
    CommandOptionChoice
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [Int]

    Define a required integer option that decodes to Int.

    Discord integers span the 53-bit safe range, so arg_int yields Int64. This variant keeps the narrowing safe instead of leaving a wrapping to_int() to the caller: a missing min or max is registered as the Int bound, so Discord rejects wider input before it is sent, and a received value outside the range fails decoding with ArgsDecodeError::Validation. With choices, only explicit bounds are registered, because the choices already close the value set.

    arg_mentionable

    fn arg_mentionable(name~ : String, description~ : String, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [
    Id
    [
    GenericMarker
    ]]

    Define a required mentionable option.

    arg_number

    fn arg_number(name~ : String, description~ : String, min? : Double, max? : Double, choices? : Array[
    CommandOptionChoice
    ], suggest? : async (
    SuggestCtx
    , Double) -> Array[
    CommandOptionChoice
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [Double]

    Define a required floating-point number option.

    arg_role

    fn arg_role(name~ : String, description~ : String, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [
    Id
    [
    RoleMarker
    ]]

    Define a required role option.

    arg_string

    fn arg_string(name~ : String, description~ : String, choices? : Array[
    CommandOptionChoice
    ], min_length? : Int, max_length? : Int, suggest? : async (
    SuggestCtx
    , String) -> Array[
    CommandOptionChoice
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [String]

    Define a required string option.

    arg_user

    fn arg_user(name~ : String, description~ : String, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Arg
    [
    Id
    [
    UserMarker
    ]]

    Define a required user option.

    button

    fn button(custom_id~ : String, label? : String, style? :
    ButtonStyle
    , emoji? :
    Emoji
    , disabled? : Bool) ->
    Component

    Build an interactive button.

    channel_select

    fn channel_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool, channel_types? : Array[
    ChannelType
    ]) ->
    Component

    Build a channel select menu, optionally restricted to channel_types; the chosen channels arrive via the component context's selected_channels.

    component_route

    Define a component route matching id or id:state. The id may itself contain :, so ids that are already in use elsewhere (bot:rolemenu) keep working. App::validate rejects empty ids, ids over 100 units, and an id that extends another typed route's id at a : (ticket beside ticket:close), because the shorter route's state could then produce a custom id the longer route receives.

    test "build a component from its route" {
    let route = @app.component_route(id="page", state=@app.CustomIdCodec::int())
    let app = @app.App()
    app.on_component(
    route,
    Immediate((_, page) => @app.ComponentReply::message(content="Page \{page}")),
    )
    let _ = @interaction.button(custom_id=route.custom_id(2), label="Next")
    assert_eq(route.custom_id(2), "page:2")
    app.validate()
    }

    container

    fn container(components~ : Array[
    Component
    ], accent_color? : Int, spoiler? : Bool) ->
    Component

    Build a container (components v2): groups child components in a rounded box with an optional accent_color (0xRRGGBB) stripe, similar to an embed. Reply and send layers set the required IS_COMPONENTS_V2 message flag automatically.

    dm_only

    fn dm_only() -> (async (
    CheckCtx
    ) -> Bool)

    Require a direct-message invocation.

    file_field

    fn file_field(custom_id~ : String, label~ : String, description? : String, min_values? : Int, max_values? : Int, file_types? : Array[
    FileTypeFilter
    ]) ->
    ModalField
    [Array[
    Attachment
    ]]

    Define a modern label-wrapped file upload. Decodes to the uploaded files' attachment objects (resolved by Discord alongside the submission). file_types filters selectable files by extension only — validate the contents yourself. min_values/max_values bound the file count.

    file_upload

    fn file_upload(custom_id~ : String, min_values? : Int, max_values? : Int, required? : Bool, file_types? : Array[
    FileTypeFilter
    ]) ->
    Component

    Build a file upload input for a modal (wrap it in label). file_types restricts selectable files by extension; min_values/max_values bound the file count (0-10 / 1-10).

    guild_only

    fn guild_only() -> (async (
    CheckCtx
    ) -> Bool)

    Require a guild invocation.

    int_choice

    fn int_choice(name : String, value : Int64, name_localizations? : Map[String, String]) ->
    CommandOptionChoice

    Describe an integer-valued option choice.

    label

    fn label(label~ : String, component~ :
    Component
    , description? : String) ->
    Component

    Wrap a modal component with a label and optional description. Modals require every input to be wrapped in a label; the typed ModalField builders do this automatically.
    fn link_button(url~ : String, label? : String, emoji? :
    Emoji
    , disabled? : Bool) ->
    Component

    Build a link button.

    Build a media gallery (components v2): a grid of 1–10 media items. Reply and send layers set the required IS_COMPONENTS_V2 message flag automatically.

    media_item

    fn media_item(url~ : String, description? : String, spoiler? : Bool) ->
    MediaGalleryItem

    Build one media gallery entry from an image or video url (https://... or attachment://<filename>), for use with media_gallery.

    mentionable_select

    fn mentionable_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool) ->
    Component

    Build a mentionable select menu accepting both users and roles.
    fn[A] modal(custom_id~ : String, title~ : String, fields~ :
    ModalFields
    [A]) ->
    Modal
    [A]

    Define a typed modal.

    test "construct and validate a typed modal" {
    let feedback = @app.modal(
    custom_id="feedback",
    title="Feedback",
    fields=@app.ModalFields::of(
    @app.text_field(custom_id="details", label="Details").optional(),
    ),
    )
    let app = @app.App()
    app.on_modal(feedback, Raw(_ => ()))
    app.validate()
    let _ : @app.ModalHandle = feedback.show(state="draft")
    }

    number_choice

    fn number_choice(name : String, value : Double, name_localizations? : Map[String, String]) ->
    CommandOptionChoice

    Describe a number-valued option choice.

    required_permissions

    fn required_permissions(required :
    Permissions
    ) -> (async (
    CheckCtx
    ) -> Bool)

    Require effective guild permissions supplied on the interaction member. Discord does not provide member permissions for DM invocations.

    role_select

    fn role_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool) ->
    Component

    Build a role select menu; the chosen roles arrive via the component context's selected_roles.

    section

    Build a section (components v2): up to three text displays laid out next to an accessory (a thumbnail or button). Reply and send layers set the required IS_COMPONENTS_V2 message flag automatically.

    select_field

    fn select_field(custom_id~ : String, label~ : String, options~ : Array[
    SelectOption
    ], description? : String, placeholder? : String, min_values? : Int, max_values? : Int) ->
    ModalField
    [Array[String]]

    Define a modern label-wrapped string select.

    select_option

    fn select_option(label~ : String, value~ : String, description? : String, emoji? :
    Emoji
    , default? : Bool) ->
    SelectOption

    Build an option for a string select menu.

    separator

    fn separator(divider? : Bool, spacing? : Int) ->
    Component

    Build a separator (components v2): vertical padding between components, with an optional visible divider line and spacing size (1 = small, 2 = large). Reply and send layers set the required IS_COMPONENTS_V2 message flag automatically.

    serve_interactions

    async fn[X] serve_interactions(group :
    TaskGroup
    [X], app :
    App
    , addr~ : String, public_key~ : String, client? :
    Client
    , token? : String, application_id? :
    Id
    [
    ApplicationMarker
    ], path? : String, deadline_ms? : Int) ->
    InteractionsServer

    Start a Discord HTTP-interactions server in group on native or moonrun Wasm.

    Use port zero in addr to let the OS choose a free port, then read the resolved address from InteractionsServer::addr.

    slash

    fn[A] slash(name~ : String, description~ : String, args~ :
    Args
    [A], handler~ :
    InteractionHandler
    [A], default_member_permissions? :
    Permissions
    , nsfw? : Bool, integration_types? : Array[
    ApplicationIntegrationType
    ], contexts? : Array[
    InteractionContextType
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Command
    [A]

    Define a typed slash command.

    test "inspect a slash-command registration spec" {
    let command = @app.slash(
    name="echo",
    description="Echo text",
    args=@interaction.Args::of(
    @interaction.arg_string(name="text", description="Text to echo"),
    ),
    handler=Raw(_ => ()),
    )
    json_inspect(command.spec().to_json(), content={
    "name": "echo",
    "description": "Echo text",
    "options": [
    {
    "type": 3,
    "name": "text",
    "description": "Text to echo",
    "required": true,
    },
    ],
    })
    }

    slash_group

    fn slash_group(name~ : String, description~ : String, children~ : Array[
    SlashChild
    ], default_member_permissions? :
    Permissions
    , nsfw? : Bool, integration_types? : Array[
    ApplicationIntegrationType
    ], contexts? : Array[
    InteractionContextType
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    Command
    [Unit]

    Define a slash command whose children dispatch by submitted option path.

    string_choice

    fn string_choice(name : String, value : String, name_localizations? : Map[String, String]) ->
    CommandOptionChoice

    Describe a string-valued option choice.

    string_select

    fn string_select(custom_id~ : String, options~ : Array[
    SelectOption
    ], placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool) ->
    Component

    Build a string select menu.

    subcommand

    fn[A] subcommand(name~ : String, description~ : String, args~ :
    Args
    [A], handler~ :
    InteractionHandler
    [A], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    SlashChild

    Define a typed slash subcommand.

    subcommand_group

    fn subcommand_group(name~ : String, description~ : String, children~ : Array[
    SlashChild
    ], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) ->
    SlashChild

    Define a slash subcommand group. Nested groups are retained for validation and rejected by App::validate, never by panic or abort here.

    text_display

    fn text_display(content : String) ->
    Component

    Build a text display block (components v2): markdown content rendered in the message body. Reply and send layers set the required IS_COMPONENTS_V2 message flag automatically.

    text_field

    fn text_field(custom_id~ : String, label~ : String, style? :
    TextInputStyle
    , description? : String, placeholder? : String, min_length? : Int, max_length? : Int, value? : String) ->
    ModalField
    [String]

    Define a modern label-wrapped text input. value is its static prefill; Modal::show(values=...) can override it for one response.

    text_input

    fn text_input(custom_id~ : String, style? :
    TextInputStyle
    , placeholder? : String, min_length? : Int, max_length? : Int, required? : Bool, value? : String) ->
    Component

    Build a modal text input. style picks single-line Short (default) or multi-line Paragraph; value prefills the field. Wrap it with label before placing it in a modal (the typed ModalField builders handle this).

    thumbnail

    fn thumbnail(url~ : String, description? : String, spoiler? : Bool) ->
    Component

    Build a thumbnail accessory (components v2) from an image url (https://... or attachment://<filename>).

    user_command

    Define a USER context-menu command.

    test "inspect context-menu command kinds" {
    let user = @app.user_command(name="Inspect user", handler=Raw(_ => ()))
    let message = @app.message_command(
    name="Inspect message",
    handler=Raw(_ => ()),
    )
    json_inspect(user.spec().to_json(), content={
    "name": "Inspect user",
    "description": "",
    "type": 2,
    })
    json_inspect(message.spec().to_json(), content={
    "name": "Inspect message",
    "description": "",
    "type": 3,
    })
    }

    user_select

    fn user_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool) ->
    Component

    Build a user select menu; the chosen users arrive in resolved and via the component context's selected_users.

    verify_signature

    fn verify_signature(public_key~ : String, signature~ : String, timestamp~ : String, body~ : BytesView) -> Bool

    Verify a Discord HTTP interaction signature without retaining the key.

    This convenience function validates and expands public_key on every call. Long-lived adapters should construct and reuse an InteractionVerifier instead. Malformed keys and signatures return false.