An experimental Discord library for MoonBit: typed APIs, a native gateway with voice (DAVE E2EE), and JS/serverless HTTP interactions.
Dependencies
Status: experimental, pre-1.0. APIs may change between releases.
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_int(
name="times",
description="How many times (1-5)",
min=1,
max=5,
).with_default(1L),
(text, times) => { text, times: times.to_int(), },
)
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~)
bot.on(@discord.Events::ready(), (_ctx, ready) => {
println("ready as \{ready.user.username}")
})
bot.run()
}| Package | What it is | Native | JS |
|---|---|---|---|
| gaato/discord | Facade: aliases for the types a typical application names directly | Yes | Yes* |
| gaato/discord/model | Pure data: ~24 entity domains, gateway payloads, zero IO | Yes | Yes |
| gaato/discord/telemetry | Structured REST, gateway, and dispatch observability values | Yes | Yes |
| gaato/discord/http | REST Client, routes, rate limiting, multipart uploads | Yes | Yes |
| gaato/discord/gateway | Shard: connection state machine, heartbeat, resume | Yes | No |
| gaato/discord/voice | Experimental native voice gateway v8, DAVE, RTP, and Opus send/receive | Yes | No |
| gaato/discord/interaction | Command/component builders, typed args and autocomplete data | Yes | Yes |
| gaato/discord/framework | Interaction routing, response gates, low-level response contexts | Yes | Yes |
| gaato/discord/app | Gateway-free typed commands/components/modals, HTTP endpoint, sync and policy | Yes | Yes |
| gaato/discord/bot | Native gateway executor and typed gateway event descriptors | Yes | No |
| gaato/discord/endpoint_http | Native signed-interactions HTTP server (serve_interactions) | Yes | No |
| gaato/discord/cache | Opt-in, gateway-driven in-memory cache | Yes | Yes |
| gaato/discord/util | Pure helpers: permissions, mentions, timestamps, CDN URLs | Yes | Yes |
| gaato/discord/verify | Pure MoonBit Ed25519 request verification | Yes | Yes |
| gaato/discord/ratelimit | Rate limiter trait + in-memory implementation | Yes | Yes |
| gaato/discord/queue | Identify queue trait + in-memory implementation | Yes | Yes |
| gaato/discord/coordinator | Native TCP coordinator for multi-process Identify and REST limits | Yes | No |
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 fn handle_http_interaction(
app : @discord.App,
group : @async.TaskGroup[Unit],
token : String,
body : Json,
) -> Json? {
app.serve(group, token~).handle(body)
}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 test --target native --release # debug native builds need a working tcc setup
moon test --target js --release
moon fmt
moon -C template fmt --check
moon -C tools/package_graph fmt --check
moon info --target native # regenerate pkg.generated.mbti (API review signal)moon ide doc "@discord"
moon ide doc "@http.Client::create_message"
moon ide doc "@model.Message"
moon ide doc "@util.channel_permissions"Status: experimental, pre-1.0. APIs may change between releases.
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_int(
name="times",
description="How many times (1-5)",
min=1,
max=5,
).with_default(1L),
(text, times) => { text, times: times.to_int(), },
)
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~)
bot.on(@discord.Events::ready(), (_ctx, ready) => {
println("ready as \{ready.user.username}")
})
bot.run()
}| Package | What it is | Native | JS |
|---|---|---|---|
| gaato/discord | Facade: aliases for the types a typical application names directly | Yes | Yes* |
| gaato/discord/model | Pure data: ~24 entity domains, gateway payloads, zero IO | Yes | Yes |
| gaato/discord/telemetry | Structured REST, gateway, and dispatch observability values | Yes | Yes |
| gaato/discord/http | REST Client, routes, rate limiting, multipart uploads | Yes | Yes |
| gaato/discord/gateway | Shard: connection state machine, heartbeat, resume | Yes | No |
| gaato/discord/voice | Experimental native voice gateway v8, DAVE, RTP, and Opus send/receive | Yes | No |
| gaato/discord/interaction | Command/component builders, typed args and autocomplete data | Yes | Yes |
| gaato/discord/framework | Interaction routing, response gates, low-level response contexts | Yes | Yes |
| gaato/discord/app | Gateway-free typed commands/components/modals, HTTP endpoint, sync and policy | Yes | Yes |
| gaato/discord/bot | Native gateway executor and typed gateway event descriptors | Yes | No |
| gaato/discord/endpoint_http | Native signed-interactions HTTP server (serve_interactions) | Yes | No |
| gaato/discord/cache | Opt-in, gateway-driven in-memory cache | Yes | Yes |
| gaato/discord/util | Pure helpers: permissions, mentions, timestamps, CDN URLs | Yes | Yes |
| gaato/discord/verify | Pure MoonBit Ed25519 request verification | Yes | Yes |
| gaato/discord/ratelimit | Rate limiter trait + in-memory implementation | Yes | Yes |
| gaato/discord/queue | Identify queue trait + in-memory implementation | Yes | Yes |
| gaato/discord/coordinator | Native TCP coordinator for multi-process Identify and REST limits | Yes | No |
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 fn handle_http_interaction(
app : @discord.App,
group : @async.TaskGroup[Unit],
token : String,
body : Json,
) -> Json? {
app.serve(group, token~).handle(body)
}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 test --target native --release # debug native builds need a working tcc setup
moon test --target js --release
moon fmt
moon -C template fmt --check
moon -C tools/package_graph fmt --check
moon info --target native # regenerate pkg.generated.mbti (API review signal)moon ide doc "@discord"
moon ide doc "@http.Client::create_message"
moon ide doc "@model.Message"
moon ide doc "@util.channel_permissions"struct Echo {
text : String
times : Int64?
}
impl @interaction.CommandModel for Echo with fn from_options(options) {
{ text: options.string("text"), times: options.int_opt("times"), }
}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()))
}type Spawner = async (async () -> Unit) -> Unitfn arg_attachment(name~ : String, description~ : String, 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_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 attachment_option(name : String, description : String, required? : Bool, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn boolean_option(name : String, description : String, required? : Bool, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn button(custom_id~ : String, label? : String, style? : ButtonStyle, emoji? : Emoji, disabled? : Bool) -> Componentfn channel_option(name : String, description : String, required? : Bool, channel_types? : Array[ChannelType], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn channel_select(custom_id~ : String, placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool, channel_types? : Array[ChannelType]) -> Componentfn int_choice(name : String, value : Int64, name_localizations? : Map[String, String]) -> CommandOptionChoicefn integer_option(name : String, description : String, required? : Bool, choices? : Array[CommandOptionChoice], min_value? : Int64, max_value? : Int64, autocomplete? : Bool, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn mentionable_option(name : String, description : String, required? : Bool, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn 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(sync=Disabled)
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 number_option(name : String, description : String, required? : Bool, choices? : Array[CommandOptionChoice], min_value? : Double, max_value? : Double, autocomplete? : Bool, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn role_option(name : String, description : String, required? : Bool, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn 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], sync? : Bool, 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_option(name : String, description : String, required? : Bool, choices? : Array[CommandOptionChoice], min_length? : Int, max_length? : Int, autocomplete? : Bool, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn string_select(custom_id~ : String, options~ : Array[SelectOption], placeholder? : String, min_values? : Int, max_values? : Int, disabled? : Bool) -> Componentfn sub_command(name : String, description : String, options? : Array[CommandOption], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn sub_command_group(name : String, description : String, sub_commands : Array[CommandOption], name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn[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_option(name : String, description : String, required? : Bool, name_localizations? : Map[String, String], description_localizations? : Map[String, String]) -> CommandOptionfn 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: typed APIs, a native gateway with voice (DAVE E2EE), and JS/serverless HTTP interactions.
Dependencies