An experimental Discord library for MoonBit: native/JS/Wasm REST, interactions, and gateway bots. Voice is in gaato/discord_voice.
Dependencies
Status: experimental. Minor releases may contain breaking changes; the changelog lists each one with a migration note.
moon update
moon add gaato/discord
moon add moonbitlang/async///|
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()
}| Package | What it is | Native | JS | Wasm¹ |
|---|---|---|---|---|
| gaato/discord | Facade: golden-path names a typical application uses directly | Yes | Yes² | Yes² |
| gaato/discord/model | Pure data: ~24 entity domains, gateway payloads, zero IO | Yes | Yes | Yes |
| gaato/discord/telemetry | Structured REST, gateway, and dispatch observability values | Yes | Yes | Yes |
| gaato/discord/http | REST Client, routes, rate limiting, multipart uploads | Yes | Yes | Yes |
| gaato/discord/gateway | Shard: connection state machine, heartbeat, resume | Yes | Yes | Yes |
| gaato/discord/interaction | Command/component builders, typed args and autocomplete data | Yes | Yes | Yes |
| gaato/discord/framework | Interaction routing, response gates, low-level response contexts | Yes | Yes | Yes |
| gaato/discord/app | Gateway-free typed commands/components/modals, HTTP endpoint, sync and policy | Yes | Yes | Yes |
| gaato/discord/bot | Gateway executor and typed gateway event descriptors | Yes | Yes | Yes |
| gaato/discord/endpoint_http | Signed-interactions HTTP server (serve_interactions) | Yes | No | Yes |
| gaato/discord/cache | Opt-in, gateway-driven in-memory cache | Yes | Yes | Yes |
| gaato/discord/util | Pure helpers: permissions, mentions, timestamps, CDN URLs | Yes | Yes | Yes |
| gaato/discord/verify | Pure MoonBit Ed25519 request verification | Yes | Yes | Yes |
| gaato/discord/ratelimit | In-memory Discord rate limiter for the gaato/sdk-runtime RateLimiter trait | Yes | Yes | Yes |
| gaato/discord/cooldown | Command cooldown store trait + in-memory fixed windows | Yes | Yes | Yes |
| gaato/discord/queue | Identify queue trait + in-memory implementation | Yes | Yes | Yes |
| gaato/discord/coordinator | Experimental TCP coordinator for multi-process Identify, REST limits, and cooldowns | Yes | No | Yes |
| gaato/discord/testkit | Deterministic interaction and model fixtures | Yes | Yes | Yes |
test "opt into channel obfuscation ahead of general availability" {
let capabilities = @model.GatewayCapabilities::channel_obfuscation()
inspect(capabilities.bits(), content="32768")
}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")
}
}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()))
}fn arg_attachment(name~ : String, description~ : String, file_types? : Array[FileTypeFilter], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> Arg[Attachment]fn arg_channel(name~ : String, description~ : String, channel_types? : Array[ChannelType], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> Arg[Id[ChannelMarker]]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]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]fn arg_mentionable(name~ : String, description~ : String, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> Arg[Id[GenericMarker]]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]fn arg_role(name~ : String, description~ : String, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> Arg[Id[RoleMarker]]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]fn arg_user(name~ : String, description~ : String, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> Arg[Id[UserMarker]]fn button(custom_id~ : String, label? : String, style? : ButtonStyle, emoji? : Emoji, disabled? : Bool) -> Componentfn channel_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool, required? : Bool, channel_types? : Array[ChannelType]) -> Componenttest "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
}fn file_field(custom_id~ : String, label~ : String, description? : String, min_values? : Int, max_values? : Int, file_types? : Array[FileTypeFilter]) -> ModalField[Array[Attachment]]fn file_upload(custom_id~ : String, min_values? : Int, max_values? : Int, required? : Bool, file_types? : Array[FileTypeFilter]) -> Componentfn int_choice(name~ : String, value~ : Int64, name_localizations? : Map[String, String]) -> CommandOptionChoicefn mentionable_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool, required? : Bool) -> Componentfn message_command(name~ : String, handler~ : InteractionHandler[Message], default_member_permissions? : Permissions, nsfw? : Bool, integration_types? : Array[ApplicationIntegrationType], contexts? : Array[InteractionContextType], name_localizations? : Map[String, String]) -> Command[Message]fn[S, A] modal(custom_id~ : String, title~ : String, state~ : CustomIdCodec[S], fields~ : ModalFields[A]) -> Modal[S, A]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)
}fn number_choice(name~ : String, value~ : Double, name_localizations? : Map[String, String]) -> CommandOptionChoicefn role_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool, required? : Bool) -> Componentfn select_field(custom_id~ : String, label~ : String, options~ : Array[SelectOption], description? : String, placeholder? : String, min_values? : Int, max_values? : Int) -> ModalField[Array[String]]fn select_option(label~ : String, value~ : String, description? : String, emoji? : Emoji, default? : Bool) -> SelectOptionasync 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) -> InteractionsServerfn[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]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,
},
],
})
}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]fn string_choice(name~ : String, value~ : String, name_localizations? : Map[String, String]) -> CommandOptionChoicefn string_select(custom_id~ : String, options~ : Array[SelectOption], placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool, required? : Bool) -> Componentfn[A] subcommand(name~ : String, description~ : String, args~ : Args[A], handler~ : InteractionHandler[A], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> SlashChildfn subcommand_group(name~ : String, description~ : String, children~ : Array[SlashChild], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> SlashChildfn text_field(custom_id~ : String, label~ : String, style? : TextInputStyle, description? : String, placeholder? : String, min_length? : Int, max_length? : Int, value? : String) -> ModalField[String]fn text_input(custom_id~ : String, style? : TextInputStyle, placeholder? : String, min_length? : Int, max_length? : Int, required? : Bool, value? : String) -> Componentfn user_command(name~ : String, handler~ : InteractionHandler[TargetUser], default_member_permissions? : Permissions, nsfw? : Bool, integration_types? : Array[ApplicationIntegrationType], contexts? : Array[InteractionContextType], name_localizations? : Map[String, String]) -> Command[TargetUser]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,
})
}fn user_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool, required? : Bool) -> Componentfn verify_signature(public_key~ : String, signature~ : String, timestamp~ : String, body~ : BytesView) -> BoolInstall
Download zipAn experimental Discord library for MoonBit: native/JS/Wasm REST, interactions, and gateway bots. Voice is in gaato/discord_voice.
Dependencies