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.raw and 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.4 when 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(...) return Nullable!Message
  • ctx.followupMessage(...) / ctx.editResponse(...) return Message
  • ctx.sendMessageResolved(...) guarantees a Message by fetching interaction @original when required
  • route aliases are available: respondMessage(...) and respondMessageResolved(...) on PrefixContext, SlashContext, and HybridContext

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 only
  • registerAllCommands() scans the current module and also wires @Event handlers, 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

AreaStatusNotes
REST coreusablereal HTTP transport, command sync, message lifecycle + multipart attachments, reactions, moderation, threads, webhooks, users, apps, and interaction callbacks
Gatewayusablelive sessions, heartbeat, resume/reconnect basics, typed dispatch integration
Commandsactiveprefix, slash, hybrid, permissions, rate limits, and service-backed handlers
Components and modalsusablebuttons, selects, modals, and Components V2 builders
State, cache, tasksavailablecache store, scoped state, and scheduled tasks are already shipped
Lua and pluginsactivefile-based plugins, capability-gated host APIs, and runtime sandbox controls
Voice / callsearlysurface-level groundwork only

Examples

Documentation

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 namespace api)
  • direct value exports through @LuaExpose(..., mode: LuaExposeMode.Value) (for author.username style access)
  • value-export mutability policy (Auto, Mutable, ReadOnly) with automatic readonly inference from const/immutable LuaTable
  • yield/resume orchestration via evalStep*, callStep*, and resumeStep*
  • coroutine payload inspection helpers: yieldedSignalKind(...) and yieldedTableField(...)

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 helper
  • ctx.reply(...) is the native reply helper
  • ctx.edit(...) edits the original interaction response
  • ctx.sendMessage(...) / ctx.sendMessageResolved(...) are available when handlers need returned message payloads
  • client.registerCommands() and client.registerAllCommands() scan the current module by default
  • CommandRegistrationFilter narrows auto-registration by module, owner, name, or category
  • built-in help is enabled by default and can render through embeds or Components V2
  • @CommandCategory and @HideFromHelp shape the default help output
  • client.errorBehavior controls how command failures are surfaced back to users
  • events have typed context companions such as ReadyEventContext and MessageCreateEventContext
  • gateway-driven GuildMemberAddEvent and PresenceUpdateEvent are emitted with typed contexts
  • GuildCreateEvent and GuildDeleteEvent are 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, and hybrid
  • @Event handlers can be wired through client.registerAllCommands()
  • ClientConfig.logUnhandledGatewayDispatchEvents can sample-log unknown dispatch names without typed coverage

That means pre-1.0.0 consistency wins over keeping older aliases around.

Authors:
  • ddiscord contributors
Dependencies:
aurora-websocket, requests
Versions:
0.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
Show all 5 versions
Download Stats:
  • 4 downloads today

  • 25 downloads this week

  • 25 downloads this month

  • 25 downloads total

Score:
0.6
Short URL:
ddiscord.dub.pm