Sign in

    discord

    An experimental Discord library for MoonBit: native/JS/Wasm REST, interactions, and gateway bots. Voice is in gaato/discord_voice.

    discord
    bot
    gateway
    api
    interactions
    Download zip
    Author
    Version
    0.13.0
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    195

    #discord.mbt

    CI License

    A Discord application library for MoonBit: typed interaction declarations, API models, a rate-limited REST client, signed HTTP interactions, and Gateway bots on the native, JavaScript, and moonrun Wasm backends. Like twilight, it is a set of loosely coupled packages with an App layer on top.

    Status: experimental. Minor releases may contain breaking changes; the changelog lists each one with a migration note.

    #Install

    Install git (Moon uses it to clone the package registry), then:

    moon update moon add gaato/discord moon add moonbitlang/async

    #Quickstart

    Build the interaction App, then hand it to the Gateway Bot. This bot registers and answers /echo:

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

    ///|
    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.AppBuilder()
    app.command(echo)
    let bot = @discord.BotBuilder(app.build(), token~, sync=Global)
    bot.on(@discord.Events::ready(), (_ctx, ready) => {
    println("ready as \{ready.user.username}")
    })
    bot.build().run()
    }

    Args drives both the command's registration payload and the decoding of each invocation. The same App can also be served over signed HTTP interactions instead of the Gateway. Continue with Getting started, or copy the bot template.

    #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/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/ratelimitIn-memory Discord rate limiter for the gaato/sdk-runtime RateLimiter traitYesYesYes
    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, whose host APIs come from moonbitlang/async; generic WASI runtimes, browsers, and wasm-gc are not targets. The interactions_http example shows a moonrun --policy setup.

    ² The HTTP-server exports exist on native and Wasm. A package that is empty on a backend does not provide that feature there, even if Moon accepts it as a dependency.

    Voice is a separate, native-only module: gaato/discord_voice.

    Import only what the program needs:

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

    The facade leaves lower-level names in their own packages: http for resource refs and pagination, framework for the router and the raw contexts of Raw handlers, gateway for shard transport, coordinator for multi-process coordination, telemetry for observability events, and interaction for raw option builders.

    #Guides

    The guides cover each part in depth; their code compiles and runs in the test suite.

    1. Getting started: a minimal slash-command bot and command synchronization.
    2. Commands: typed arguments, autocomplete, subcommands, context menus, and handler modes against Discord's three-second deadline.
    3. Components and modals: buttons, selects, typed modals, and component waits.
    4. Events and intents: typed Gateway subscriptions, intent derivation, sharding, saved sessions, compression, and the cache.
    5. REST: typed endpoints, resource refs, rate limits and resends, uploads, and pagination.
    6. HTTP interactions: gateway-free dispatch, the native server, and JavaScript hosts.
    7. Structuring bots: feature installers, state, configuration, checks, and cooldowns.
    8. Scaling across processes: share Identify and REST limits through the bundled coordinator.
    9. Middleware: wrap REST calls, routed interactions, and Gateway fan-out; error-policy responses and telemetry.
    10. Voice: gaato/discord_voice with DAVE encryption.
    11. The framework layer: direct routing, response gates, and handler tests without a network.
    12. Models and utilities: typed snowflakes, forward-compatible enums, and permission, mention, timestamp, and CDN helpers.

    Exact type and method documentation lives in doc comments, many with examples: browse them here on mooncakes, or run moon ide doc "@discord" (any package or symbol name works).

    #Examples

    The repository's examples/ has runnable Gateway bots, HTTP interaction servers and JavaScript host adapters, a Durable Object bot, and voice players; run one from the repository root with moon run --target native examples/slash_echo.

    #Testing

    Client::offline answers REST calls from a function you write, and gaato/discord/testkit provides deterministic interaction and model fixtures; import it only in your package's for "test" block. See the framework layer for handler tests without a network.

    #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 }

    Validated, immutable application declarations shared by gateway and HTTP executors. Build one with AppBuilder::build; every App has passed validation, so executors and attach never see an invalid declaration.

    AppBuilder

    Collects application declarations: commands, component and modal routes, autocomplete handlers, middleware, and failure hooks. build validates them into an immutable App.

    AppConfigError

    Configuration errors detected before starting an application executor.

    AppCtx

    using @gaato/discord/app { type AppCtx }

    Transport-neutral services available to interaction handlers.

    Executors construct these values; the public API exposes read-only accessors.

    AppWarning

    Something the App or its executor handled on its own but an operator should know about. Delivered to AppBuilder::on_warn; without a hook the App prints it. Show renders a one-line message for every variant, so a hook that only logs can call to_string().

    ApplicationRef

    An application id bound to the REST client that operates on it.

    Arg

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

    Args

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

    Bot

    using @gaato/discord/bot { type Bot }

    A validated gateway bot, built by BotBuilder::build. Its configuration and registrations are fixed; run connects it.

    BotBuilder

    Collects a gateway bot's configuration and registrations: typed and raw event handlers, event middleware, services, the cache, and observers. build() validates it into a Bot that can run. Like AppBuilder, registering more on the builder afterwards does not change a built Bot.

    BotConfigError

    A gateway configuration that cannot run, found by BotBuilder::build before any connection is attempted.

    BotError

    Fatal runtime failures reported by Discord.

    BotSession

    Persist a Bot gateway session together with the READY identity needed to restore event and interaction handling before Discord sends RESUMED. Optional cache contents and application-owned state are not restored. Persisting a receive sequence periodically does not guarantee exactly-once event handling after a restart.

    CacheLimit

    Bounds one cached resource by entry count, lifetime, or both.

    Negative limits clamp to zero. A max_entries of zero retains nothing, while a ttl_ms of zero makes entries expire immediately. TTL deadlines are fixed when an entry is written and are never extended by reads; expiry is enforced lazily, without background timers: every read and event first releases whatever has expired, with what it owns or holds.

    CacheLimits

    Per-resource bounds for an in-memory cache.

    Guild, channel, DM-channel, user, and recent-user limits are global. Role, member, voice-state, and presence limits apply independently per guild; message limits apply independently per channel. Everything keyed by a guild or channel leaves with it: when a cached guild or channel is deleted, evicted by its limit, or expires, its roles, members, voice states, presences, channels, threads, and messages go too, and per-guild and per-channel maps are dropped once empty. State for a guild or channel the cache does not hold (its retention is off, or the event arrived after it left) still obeys its per-guild or per-channel limit and goes when the guild or channel is deleted.

    Under the defaults every structure is bounded:
    • guilds and unavailable-guild flags by the guilds the bot is in (a new session's READY also drops the guilds it no longer lists);
    • a guild's channels, roles, members, voice states, and presences by what Discord holds per guild, times those guilds (a fresh GUILD_CREATE drops the channels and threads it no longer lists);
    • threads by the active threads of those guilds: a thread leaves when it is deleted, archived, replaced by a thread list sync, or (private) when the bot is removed from it;
    • channels without a guild (DMs and group DMs) by dm_channels, 128, evicting the least recently written with its messages;
    • messages by messages, 100 per channel, times the channels above;
    • users by the entries that hold them (members, the authors of cached messages, and the current user), plus recent_users, the 128 most recently seen users nothing holds.

    Passing CacheLimit() for dm_channels, recent_users, or messages lifts that bound. TTLs use fixed, non-sliding deadlines set at write time (re-storing an entry sets a new one) and are enforced lazily without background timers.

    CacheResources

    Selects which gateway resources an in-memory cache retains.

    Guilds, channels, roles, members, users, and voice states are enabled by default. Presences and messages are opt-in because they are high-churn and can dominate memory use on larger bots.

    ChannelRef

    A channel id bound to the REST client that operates on it.

    CheckCtx

    Read-only context passed to command checks before argument decoding and handler execution.

    Client

    Discord REST client: token + HTTP transport + rate limiter.

    Command

    An application-command definition with a context-aware decoder.

    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.

    CommandPlan

    Planned synchronizations for each requested scope, in order. Inspect report() before apply.

    CommandReply

    An initial response returned by an immediate command handler.

    CommandScope

    Which command scope a synchronization targets.

    ComponentCtx

    Context handed to a message-component (button / select) handler.

    ComponentDeferredCtx

    Component context available after a deferred callback.

    ComponentHandler

    Execution strategy for a component route.

    ComponentImmediateCtx

    Read-only component context for handlers returning their initial callback.

    ComponentReply

    Initial response returned by an immediate component handler.

    ComponentRoute

    A component's route identity and the codec shared by producer and handler.

    CooldownBucket

    Identity used to share a fixed-window command cooldown.

    CooldownOutage

    Whether a command with a cooldown runs while the cooldown store cannot answer.

    CustomIdCodec

    A reversible text encoding for the state carried in a custom id. Custom encode/decode functions and imap mappings must preserve round trips.

    CustomIdError

    A custom id that exceeds Discord's limit or cannot round-trip its segments.

    DeferredCtx

    Command context made available after the initial deferred response.

    DiscordHttpError

    Everything a REST call can raise, from client-side validation to decoded Discord API errors.

    Transport and Timeout carry the phase of the exchange that failed. Pool and Dial mean no byte of the request was sent, so Discord cannot have acted on it (is_unsent); any other phase means it may have.

    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.

    EventMiddleware

    Runs once per decoded gateway event, around the handler fan-out. next(event) spawns the typed and raw handlers for (a possibly transformed) event and returns once they are spawned, not completed. Not calling next drops the event for handlers only: cache updates, decode-error observation, collectors, and interaction routing are wire-level machinery that runs before this chain.

    EventType

    A typed projection from the general gateway event enum.

    Events

    using @gaato/discord/bot { type Events }

    Namespace for typed gateway event descriptors.

    The payload-free Resumed event and Unknown are available only through the low-level event stream.

    FailureCtx

    Context handed to the error policy when a handler fails.

    FailureOrigin

    The operation whose handler failed.

    FileUpload

    One file to upload in a multipart/form-data request, next to the payload_json part. Construct with FileUpload(...).

    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")
    }

    GatewayCompression

    Transport compression for BotBuilder(compression=...).

    GatewayCtx

    Gateway-only services passed to event and service handlers after READY.

    GuildInvocation

    Proof that an interaction was invoked in a guild: the guild id and the invoking member, both validated when the context was built.

    GuildRef

    A guild id bound to the REST client that operates on it.

    HandlerError

    Expected command failures that an error policy can present to the user.

    Raise these from handlers, checks, or app middleware; the application's ErrorPolicy turns them into user-facing responses. A permission guard computed with @util.channel_permissions typically raises MissingPermission:

    test "guard a handler with a permission requirement" {
    fn require_send_messages(permissions : @model.Permissions) -> Unit raise {
    let required = @model.Permissions::send_messages()
    if !permissions.contains(required) {
    raise @app.HandlerError::MissingPermission(required)
    }
    }

    try require_send_messages(@model.Permissions::none()) catch {
    @app.HandlerError::MissingPermission(_) => ()
    other => raise other
    } noraise {
    _ => fail("expected MissingPermission")
    }
    }

    HandlerIntake

    Whether an executor still starts handlers. An executor that stops gracefully closes its intake: from then on no handler starts, and handlers waiting for capacity give up at once; handlers already running are not affected. Pass one to App::spawner and App::attach.

    ImmediateCtx

    Read-only command context for handlers that return their initial response.

    InMemoryCache

    A gateway-driven, resource-selective in-memory cache.

    update is the only mutation entry point. Entity getters return the model value retained by the cache; callers should treat it as immutable. List getters always allocate a new outer array so callers cannot mutate the cache's collections. Archived threads are not served: Discord stops sending their events, so the cache drops them when they are archived; fetch them over REST.

    InitialResponse

    A declarative initial message response returned by an immediate handler.

    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()))
    }

    InteractionCtx

    Read-only context handed to interaction middleware, before any initial response has been sent.

    InteractionEndpoint

    A gateway-free interaction dispatcher backed by a caller-owned task group.

    InteractionHandler

    Execution strategy for a typed application command.

    InteractionHttpBody

    A callback body. Multipart chunks are written in order and retain file bytes without a second copy.

    InteractionHttpRequest

    The exact bytes received from an HTTP host. Never parse or reserialize body before verifying the Discord signature.

    InteractionHttpResponse

    A host-independent HTTP response for a Discord interaction.

    InteractionOutcome

    The result of dispatching one HTTP-delivered interaction.

    InteractionTarget

    The routed target of an interaction entering the middleware chain. Autocomplete, gateway events, and services never pass through it.

    InteractionVerifier

    A validated and expanded Discord interaction public key.

    Construct one verifier per application public key and reuse it across requests. The expanded Ed25519 key is immutable and safe to share.

    InteractionVerifierError

    Errors raised while constructing a reusable Discord interaction verifier.

    InteractionsServer

    An HTTP server dispatching Discord interactions on native or moonrun Wasm.

    InvocationScope

    The context in which an interaction was invoked.

    MemberRef

    A guild member identity bound to the REST client that operates on it.

    MessageRef

    A message id and its channel id bound to the REST client that operates on it.

    MessageUpdate

    An UPDATE_MESSAGE callback: edits the message that hosts the component (or, for a modal, the message whose component opened it). Omitted fields keep their current value.
    using @gaato/discord/app { type Modal }

    A typed modal definition: a route id with typed state, and typed fields. The decoders are retained for the matching on_modal registration while show erases them into a ModalHandle.

    ModalDeferredCtx

    Modal-submit context available after a deferred response.

    ModalField

    One typed input in a modal form.

    ModalFields

    A composable collection of typed modal inputs.

    ModalHandle

    A type-erased modal response ready to be returned by a command or component handler.

    ModalImmediateCtx

    Read-only modal-submit context for handlers returning an initial message.

    ModalOrigin

    The interaction that opened a submitted modal.

    ModalReply

    Initial response returned by an immediate modal handler.

    ModalShowError

    Why Modal::show could not prepare a modal response.

    ModalSubmitHandler

    Execution strategy for a typed modal submission. Handlers receive the decoded state and form.

    PendingEvent

    One expected gateway event, from GatewayCtx::expect_event. It is registered when created, separately from waiting, so a caller can install it before sending the command that makes Discord dispatch the event.

    ResponseState

    What has happened to an interaction's single initial callback.

    RestAccess

    How an executor reaches Discord's REST API: with a bot token, from which it builds and owns a client, or through an existing client the caller owns.

    ServerConfigError

    Configuration rejected by serve_interactions before it starts listening.

    ShardConfig

    Selects which Gateway shards this process owns.

    SlashChild

    A type-erased slash subcommand or subcommand group.

    SuggestCtx

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

    SyncReport

    Reports for each synchronized scope, in the requested order.

    TargetUser

    The resolved target of a USER context-menu command.

    UserRef

    A user id bound to the REST client that operates on it.

    WaitFrom

    Who may satisfy a component wait.

    API_VERSION

    let API_VERSION : Int

    Discord API version this library targets.

    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, required? : 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. AppBuilder::build rejects empty ids, ids over 100 characters, 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.AppBuilder()
    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.build() |> ignore
    }

    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, required? : Bool) ->
    Component

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

    Define a typed modal. Like component_route, state encodes the value carried in the custom id after custom_id:; pass CustomIdCodec::unit() for a modal without state. Handlers receive the decoded state and form.

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

    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, required? : 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, access~ :
    RestAccess
    , application_id? :
    Id
    [
    ApplicationMarker
    ], path? : String, deadline_ms? : Int, max_body_bytes? : Int, body_timeout_ms? : Int, header_timeout_ms? : Int, max_connections? : 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.

    A request body larger than max_body_bytes gets 413, and one not received within body_timeout_ms gets 408; both close the connection. Discord's interaction payloads are far below the 1 MiB default. A connection whose next request head does not arrive within header_timeout_ms, idle keep-alive time included, is closed. At most max_connections clients are served at once; more wait to be accepted. Non-positive limits raise ServerConfigError before the server listens.

    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, required? : 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 AppBuilder::build, 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, required? : 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.