ddiscord 0.3.0
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
- 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
- Components V2 coverage with runnable examples
- State, cache, rate limiting, services, tasks, and Lua/plugin support
- Production-minded runtime controls (dispatch backpressure, error surfacing, and telemetry) with built-in queue health and real uptime tracking
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 docs/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;
@Command("ping", description: "Check the bot latency", routes: CommandRoute.Prefix)
void handlePing(CommandContext 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
@Command("userinfo", description: "Show information about a user", routes: CommandRoute.Slash)
void handleUserInfo(CommandContext 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(CommandContext 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();
}
Opening a modal
@Command("report", description: "Open a report modal", routes: CommandRoute.Slash)
void handleReport(CommandContext ctx)
{
auto modal = Modal("report_modal", "Report User")
.addTextInput(TextInput("reason", "Reason"));
ctx.showModal(modal).await();
}
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 now 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(HybridCommandContext 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 now 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 now 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(CommandContext 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.
Library Status
| Area | Status | Notes |
|---|---|---|
| REST core | usable | real HTTP transport, command sync, messages, 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 | growing | buttons, selects, modals, and Components V2 are present and evolving |
| State, cache, tasks | available | cache store, scoped state, and scheduled tasks are already shipped |
| Lua and plugins | experimental | useful today, but still changing quickly |
| Voice / calls | early | only surface-level groundwork for now |
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 + Dorm
Documentation
docs/client.mdfor the runtime/client guidedocs/philosophy.mdfor project direction and engineering principlesexamples/README.mdfor the runnable consolesCHANGELOG.mdfor release notes in progress
Lua and Plugins Notes
Lua host APIs now 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
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 responseclient.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 now have typed context companions such as
ReadyEventContextandMessageCreateEventContext - command outcome events now expose route-aware helpers like
prefix,slash,contextMenu, andhybrid @Eventcan register event handlers throughclient.registerAllCommands!(...)
That means pre-1.0.0 consistency wins over keeping older aliases around.
- 0.3.0 released 3 days 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