ddiscord ~main
UDA-first Discord bot library for D
To use this package, run the following command in your project's root directory:
Manual usage
Put the following dependency into your project's dependences section:
dDiscord <img src="logo.svg" width="24">
A Discord bot library for D-lang.
[!NOTE]
ddiscord is an early-stage project (pre-1.0.0) developed in the open with AI assistance.
This library is also used in personal projects, so changes may be frequent and occasionally breaking until a stable release.
Key Features
- UDA-first command API for prefix, slash, and hybrid commands
- Command middleware pipeline with global and named
@UseMiddleware(...)hooks - Real Discord REST and gateway connectivity
- Typed events, typed models, and typed command inputs
- Interaction helpers for replies, follow-ups, modals, autocomplete, and deferred responses
- Message-focused command helpers (
ctx.react,ctx.pin,ctx.crosspost,ctx.messageRef) - Multipart attachment uploads across message and interaction response flows
- Message lifecycle helpers (create/edit/delete/bulk delete/crosspost/pin flows) and reaction endpoints
- Guild moderation, thread management, and webhook execution REST surfaces
- Advanced escape hatches for missing API wrappers (
rest.rawand raw gateway dispatch events) - Components V2 coverage with runnable examples
- State, cache, rate limiting, services, tasks, and Lua/plugin support
- Coroutine-aware Lua runtime stepping (
evalStep/resumeStep) with auto-resume helpers - Configurable Lua runtime guardrails (execution timeout, memory limit, instruction checkpoints)
- Production-minded runtime controls (dispatch backpressure, error surfacing, and telemetry) with built-in queue health and real uptime tracking
- Runtime safety guardrails for large bots (worker-loop crash isolation, stricter REST validation, and configurable retry controls)
- Input hardening for production REST usage (safer token routing, emoji validation, and audit-log reason sanitization)
Philosophy
ddiscord is being built as a production runtime for D bots: typed APIs, explicit failure
handling, bounded backpressure, and modular internals that scale past small toy projects.
More detail is in manual/philosophy.md.
Installing
Recommended toolchains:
- DMD
>= 2.106 - LDC
>= 1.36 liblua5.4when you want Lua/plugin features
You can install ddiscord directly with DUB:
dub add ddiscord
Or add it manually to dub.json:
{
"dependencies": {
"ddiscord": "~>0.2.0"
}
}
If you want the current development head instead, work from the repository directly:
git clone https://github.com/soloverdrive/ddiscord.git
cd ddiscord
dub test
Runnable example consoles live under examples/.
Quick Example
import ddiscord;
import std.path : buildPath;
@PrefixCommand("ping", "Check the bot latency")
void handlePing(PrefixContext ctx)
{
ctx.send("Pong!").await();
}
void main()
{
auto env = loadEnv(buildPath("examples"));
auto client = new Client(ClientConfig(
token: env.get!string("DISCORD_TOKEN", env.require!string("TOKEN")),
intents: cast(uint) GatewayIntent.GuildTextCommands,
prefix: "!"
));
client.registerCommands();
client.run();
client.wait();
}
More Examples
Slash command with an ephemeral response
@SlashCommand("userinfo", "Show information about a user")
void handleUserInfo(SlashContext ctx, Nullable!User target = Nullable!User.init)
{
auto resolved = target.isNull ? ctx.user : target.get;
ctx.send("User: " ~ resolved.mention, ephemeral: true).await();
}
Native message reply versus normal send
@HybridCommand("hello", "Reply to the caller")
void handleHello(HybridContext ctx)
{
if (ctx.source == CommandSource.Prefix)
ctx.reply("Hello there.", mentionAuthor: true).await();
else
ctx.send("Hello there.", ephemeral: true).await();
}
Long-running interaction flow
@Command("build", description: "Run a longer task", routes: CommandRoute.Slash)
void handleBuild(CommandContext ctx)
{
ctx.think(ephemeral: true).await();
ctx.edit("Finished the build.").await();
}
Response payload helpers
@SlashCommand("ticket", "Open a support ticket")
void handleTicket(SlashContext ctx)
{
// Always returns a concrete Message, even for the first slash callback.
auto created = ctx.respondMessageResolved("Ticket opened.", ephemeral: true).await();
// Use the returned message payload immediately.
ctx.followupMessage("Tracking id: " ~ created.id.toString, ephemeral: true).await();
}
Use these helpers when you need the created/updated message payload in code:
ctx.sendMessage(...)/ctx.replyMessage(...)returnNullable!Messagectx.followupMessage(...)/ctx.editResponse(...)returnMessagectx.sendMessageResolved(...)guarantees aMessageby fetching interaction@originalwhen required- route aliases are available:
respondMessage(...)andrespondMessageResolved(...)onPrefixContext,SlashContext, andHybridContext
Opening a modal
@SlashCommand("report", "Open a report modal")
void handleReport(SlashContext ctx)
{
auto modal = Modal("report_modal", "Report User")
.addTextInput(TextInput("reason", "Reason"));
ctx.showModal(modal).await();
}
User-installed slash command
@SlashCommand("inbox", "Open your DM inbox")
@UserInstalledDmOnly
void handleInbox(SlashContext ctx)
{
ctx.send("Inbox ready.", ephemeral: true).await();
}
Slash autocomplete with @Autocomplete
AutocompleteChoice[] songAutocomplete(string partial)
{
return [
AutocompleteChoice("Song " ~ partial, partial ~ "-1"),
AutocompleteChoice("Song " ~ partial ~ " 2", partial ~ "-2")
];
}
@SlashCommand("play", "Play a song")
@Autocomplete!songAutocomplete("song")
void handlePlay(SlashContext ctx, string song)
{
auto _ = ctx;
ctx.send("Playing " ~ song).await();
}
Route-specific context aliases are available for clearer signatures:
PrefixContext, SlashContext, HybridContext, and ContextMenuContext.
They also include route-aware helpers (for example SlashContext.respondEphemeral(...) and
PrefixContext.replyToSource(...)) to keep handlers concise.
Event handlers with @Event
@Event
void handleReady(ReadyEventContext ctx)
{
import std.stdio : writeln;
writeln("Ready as ", ctx.selfUser.username);
}
@Event handlers can receive either the event itself, such as ReadyEvent, or its richer
context type, such as ReadyEventContext.
@Event is the event-side UDA entrypoint, matching the same registration style used for commands.
Each shipped event has its own context companion so follow-up work stays fluent without
throwing away the smaller payload structs. Those contexts expose rest, cache, services,
state, logger, and current cached entities like ctx.user, ctx.guild, ctx.channel,
ctx.message, or ctx.interaction when they are available.
@Event
void auditMessage(MessageCreateEventContext ctx)
{
import std.stdio : writeln;
auto guildText = ctx.guild.isNull ? "DM" : ctx.guild.get.name;
writeln("[", guildText, "] ", ctx.message.content);
}
Hybrid command contexts
@HybridCommand("where", "Show how the command was invoked")
void handleWhere(HybridContext ctx)
{
if (ctx.fromPrefix)
ctx.reply("You used the prefix route.").await();
else
ctx.send("You used the slash route.", ephemeral: true).await();
}
Auto Registration
The simplest path is module-local registration:
client.registerCommands();
client.registerAllCommands();
registerCommands()scans the current module and registers command handlers onlyregisterAllCommands()scans the current module and also wires@Eventhandlers, stateful command groups, and plugin descriptor types
Both helpers accept filters, so you can keep registration short without giving up control:
auto filter = CommandRegistrationFilter
.modules("app")
.exceptNames("debug")
.exceptCategories("Internal");
client.registerAllCommands(filter);
Useful filter targets include module names, owner types, command names, and @CommandCategory
values.
Built-in Help and Error Surfacing
The client ships a built-in help command by default. It uses embeds or Components V2,
supports pagination, and can be fully customized by swapping how entries and pages are rendered.
@CommandCategory("Utility")
@HybridCommand("ping", "Check the bot latency")
void handlePing(HybridContext ctx)
{
ctx.send("Pong!").await();
}
@HideFromHelp
@Command("debug-cache", routes: CommandRoute.Prefix)
void debugCache(CommandContext ctx)
{
ctx.send("cache ok").await();
}
void main()
{
auto client = new Client(ClientConfig(token: "...", intents: 0));
client.registerCommands();
client.helpBehavior.pageSize = 4;
client.errorBehavior.surfaceUnknownCommand = true;
client.errorBehavior.surfaceArgumentErrors = true;
}
Out of the box, the client can surface things like unknown commands, missing arguments, invalid
arguments, and handler failures. That behavior is also customizable through client.errorBehavior.
For quick profiles, use CommandErrorBehavior.nonVerbose() or CommandErrorBehavior.verbose().
Library Status
| Area | Status | Notes |
|---|---|---|
| REST core | usable | real HTTP transport, command sync, message lifecycle + multipart attachments, reactions, moderation, threads, webhooks, users, apps, and interaction callbacks |
| Gateway | usable | live sessions, heartbeat, resume/reconnect basics, typed dispatch integration |
| Commands | active | prefix, slash, hybrid, permissions, rate limits, and service-backed handlers |
| Components and modals | usable | buttons, selects, modals, and Components V2 builders |
| State, cache, tasks | available | cache store, scoped state, and scheduled tasks are already shipped |
| Lua and plugins | active | file-based plugins, capability-gated host APIs, and runtime sandbox controls |
| Voice / calls | early | surface-level groundwork only |
Examples
examples/start-bot: minimal env-driven startupexamples/basic-bot: prefix + slash basicsexamples/events-bot: typed event handling in isolationexamples/interactions-bot: button + modal interaction flowexamples/services-bot:@Statefulgroups with injected servicesexamples/tasks-bot: scheduled reminders and recurring task loopsexamples/full-bot: state, permissions, rate limits, and componentsexamples/plugin-bot: Lua host APIs and file-based pluginsexamples/test-bot: integration-oriented validation bot with startup REST checksexamples/help-bot: built-in help customization and error behaviorexamples/filter-bot: module auto-registration filters in practiceexamples/lua-scripting-bot: persisted user scripts with SQLite + Dormexamples/rest-ops-bot: reactions, moderation, threads, webhooks, and message lifecycle operations
Documentation
manual/client.mdfor the runtime/client guidemanual/philosophy.mdfor project direction and engineering principlesexamples/README.mdfor the runnable consolesCHANGELOG.mdfor release notes in progress
Lua and Plugins Notes
Lua host APIs include:
- state helpers:
state_get,state_set,state_has,state_del - plugin-scoped logging helpers:
log_info,log_warn,log_error - plugin context metadata:
plugin_name,plugin_version,plugin_api_version,plugin_entrypoint,plugin_sandbox
Lua runtime helpers now also include:
- namespaced API UDAs via
@LuaApi(...)(default namespaceapi) - direct value exports through
@LuaExpose(..., mode: LuaExposeMode.Value)(forauthor.usernamestyle access) - value-export mutability policy (
Auto,Mutable,ReadOnly) with automatic readonly inference fromconst/immutable LuaTable - yield/resume orchestration via
evalStep*,callStep*, andresumeStep* - coroutine payload inspection helpers:
yieldedSignalKind(...)andyieldedTableField(...)
For file-based plugins with sandbox: "untrusted" and no explicit permissions, the default
capability set is intentionally minimal (context.read) to reduce accidental overexposure.
Production hardening can be enabled with ClientConfig flags:
requireExplicitPluginPermissions, allowLoosePlugins, and allowPluginEntrypointEscape.
Current API Direction
The public naming is being tightened before 1.0.0:
ctx.send(...)is the normal response helperctx.reply(...)is the native reply helperctx.edit(...)edits the original interaction responsectx.sendMessage(...)/ctx.sendMessageResolved(...)are available when handlers need returned message payloadsclient.registerCommands()andclient.registerAllCommands()scan the current module by defaultCommandRegistrationFilternarrows auto-registration by module, owner, name, or category- built-in
helpis enabled by default and can render through embeds or Components V2 @CommandCategoryand@HideFromHelpshape the default help outputclient.errorBehaviorcontrols how command failures are surfaced back to users- events have typed context companions such as
ReadyEventContextandMessageCreateEventContext - gateway-driven
GuildMemberAddEventandPresenceUpdateEventare emitted with typed contexts GuildCreateEventandGuildDeleteEventare emitted from live gateway dispatches for cache/runtime sync flows- typed gateway coverage includes channel/message lifecycle, member removal, and typing-start dispatches
- command outcome events expose route-aware helpers like
prefix,slash,contextMenu, andhybrid @Eventhandlers can be wired throughclient.registerAllCommands()ClientConfig.logUnhandledGatewayDispatchEventscan sample-log unknown dispatch names without typed coverage
That means pre-1.0.0 consistency wins over keeping older aliases around.
- ~main released 17 hours ago
- soloverdrive/ddiscord
- MIT
- Authors:
- Dependencies:
- aurora-websocket, requests
- Versions:
-
Show all 5 versions0.3.3 2026-Apr-25 0.3.2 2026-Apr-23 0.3.0 2026-Apr-23 0.2.0 2026-Apr-19 ~main 2026-Apr-25 - Download Stats:
-
-
4 downloads today
-
25 downloads this week
-
25 downloads this month
-
25 downloads total
-
- Score:
- 0.6
- Short URL:
- ddiscord.dub.pm