An experimental Discord library for MoonBit: native/JS/Wasm REST, interactions, and gateway bots; native voice with DAVE E2EE.
Dependencies
Status: experimental. Minor releases may contain breaking changes; see the changelog for migration notes.
moon update
moon add gaato/discord///|
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()
}| 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/voice | Experimental native voice gateway v8, DAVE, RTP, and Opus send/receive | Yes | No | No |
| 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 | Rate limiter trait + in-memory implementation | 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 |
moon build --target wasm --release src/examples/interactions_http
moonrun --policy discord-policy.json _build/wasm/release/build/examples/interactions_http/interactions_http.wasm{
"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"]
}
}bot.on(@discord.Events::message_create(), (ctx, event) => {
let message = event.message
println("\{message.author.username}: \{message.content}")
})///|
let bot = @discord.Bot(app, token~, shards=Auto)///|
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"])
}///|
async fn handle_http_interaction(
app : @discord.App,
group : @async.TaskGroup[Unit],
token : String,
body : Json,
) -> Json? {
app.serve(group, token~).handle(body)
}| Host | Checked here | Remaining host integration |
|---|---|---|
| Cloudflare Workers | Signed requests and deferred REST in local workerd | Production deployment |
| Deno | Deno.serve adapter and signed requests | Hosted lifecycle |
| Bun | Shared MoonBit handler and Bun.serve loopback endpoint | Hosted lifecycle |
| Node.js | Shared MoonBit handler and Web APIs | HTTP hosting and process lifecycle |
| Vercel Functions | Node.js fetch adapter and waitUntil handoff | Vercel packaging and hosted lifecycle |
| Fastly Compute | FetchEvent and synchronous lifetime registration with a fake event | Fastly compilation and hosted execution |
| Lambda Function URL | Payload v2 body and response conversion with local events | AWS invocation and deployment |
///|
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()
}let id : @model.MessageId = @model.Id::parse("175928847299117063")
println(id.timestamp_ms()) // 1462015105796let 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)
|> ignoreclient.guild_ref(guild_id).emoji_ref(emoji_id).edit(name="renamed") |> ignoremoon 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 typemoon ide doc "@discord"
moon ide doc "@http.Client::create_message"
moon ide doc "@model.Message"
moon ide doc "@util.channel_permissions"test "opt into channel obfuscation ahead of general availability" {
let capabilities = @model.GatewayCapabilities::channel_obfuscation()
inspect(capabilities.bits(), content="32768")
}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, 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.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()
}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) -> 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]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")
}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) -> 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, client? : Client, token? : String, application_id? : Id[ApplicationMarker], path? : String, deadline_ms? : 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) -> 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) -> 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; native voice with DAVE E2EE.
Dependencies