Skip to content

Extensions

Extensions are a very powerful resource in DDBKit that allows you to incorporate middleware functionality into your bot interactions.

There are two ways to use extensions:

  • Registering a DDBKitExtension type to your BotInstance.
  • Using modifiers exposed by the extension to intercept commands. And you can combine both to create a powerful extension that can do a lot of things.

Registering an extension

This is important to all end-users of extensions. If you need to use an extension that acts on a global level, you need to add some code to your bot app that runs on startup.

@main
struct MyNewBot: DiscordBotApp {
init() async {
// ...
}
func boot() async throws {
// Register the extension
RegisterExtension(MyExtension())
}
}

When your bot launches, the extension will be registered and will be able to intercept commands, as well as adding and removing commands and events.

Using extension modifiers

This is important to all end-users of extensions. This is for using an extension-provided modifier that acts on a command-scope.

Extensions can expose modifiers that can intercept actions of the command they’re attached to.

Check out this example of a hypothetical extension modifier:

Command("ping") { i in
// ...
}
.levelRequirement(at: 20) // has to be xp level 20 to use the command

In this hypothetical example, the levelRequirement modifier is provided by an extension and will only allow the command to be executed if the user has an XP level of 20 or higher. The modifier can throw early if the requirement is not met, plus they could add their own error handling too, but that wouldn’t be cool.

Maybe the modifier requires its extension to be registered for storing and retrieving XP levels.

Example extension ideas

  • XP System: An extension that tracks user XP and levels, from messages and command interactions.
  • Moderation: An extension that layers a permission system for moderation commands where your server consists of different rankings rather than moderator and admin.
  • Logging: An extension that logs all events, commands and interactions to a database for later review.
  • Custom Commands: An extension that aggregates some data and creates a command to view that data.
  • Ratelimiting: An extension that limits the number of commands a user can run in a certain time frame or bucket.

A Discord bot could encapsulate functionality and behaviour to be more modular, or just be a drop-in system. It depends on your preference and goals.

Creating an extension

Registerable Extension

To create an extension, you need to create an actor that conforms to the DDBKitExtension protocol.

actor MyExtension: DDBKitExtension {
func onBoot(_ instance: inout BotInstance) async throws {
// ...
}
}

From in this onBoot(_:) function, you can interact with BotInstance, which is the main class behind the user’s Discord bot. It contains a bunch of useful properties and functions that you can use to interact with the bot.
The onBoot(_:) function is called when BotInstance finishes setting up the environment and is ready to start the bot. It then calls the function of all registered extensions in the order they were registered.

Here’s the interface of BotInstance:

public class BotInstance {
public var globalErrorHandle: ((any GatewayManager, any Error, Interaction) async throws -> Void)?
public var events: [any BaseEvent]
public var commands: [ExtensibleCommand]
public var modalReceives: [String : [(Interaction, Interaction.ModalSubmit, DatabaseBranches) async throws -> Void]]
public var componentReceives: [String : [(Interaction, Interaction.MessageComponent, DatabaseBranches) async throws -> Void]]
/// Unique stable identifier for the app
public let id: ApplicationSnowflake
}

If you didn’t already notice inout, it means you’ve been passed a reference to the BotInstance, and you can modify it directly and your changes will be reflected in the original instance.

Here’s an example to print all registered commands.

actor PrintAllCommandsExtension: DDBKitExtension {
func onBoot(_ instance: inout BotInstance) async throws {
let registeredCommands = instance.commands.map(\.baseInfo.name)
print(registeredCommands)
}
}

You can edit the commands from here too, such as adding new commands, removing commands, or modifying existing commands.

actor PrintAllCommandsExtension: DDBKitExtension {
func onBoot(_ instance: inout BotInstance) async throws {
instance.commands = instance.commands.map {
$0
.preAction { cmd, _ in
print(cmd.baseInfo.name)
}
.postAction { cmd, _ in
print(cmd.baseInfo.name, "finished") // if it didnt error this will work
}
}
}
}

DDBKitExtension has two more methods called at other times.

actor PrintAllCommandsExtension: DDBKitExtension {
func onBoot(_ instance: inout BotInstance) async throws {
// ...
}
/// The register method allows you to create new commands and events
/// This builder can use logic to determine what to register, which you
/// can combine with `onBoot(:) async throws` to logically add handling.
func register() -> [any BotScene] {
Command("gm") { e in
try await e.respond(with: "gm")
}
}
/// Gives you direct events from the gateway. Combine this with
/// DiscordBM's `GatewayEventHandler` protocol to make event handling easier
func onEvent(_ instance: BotInstance, event: Gateway.Event) async throws {
print("\(event.type ?? "") received")
// instanciate handler with event and handle
MessageReceiveHandle(event: event).handle()
}
struct MessageReceiveHandle: GatewayEventHandler {
var event: Gateway.Event
func onMessageCreate(_ payload: Gateway.MessageCreate) async throws {
print("[\(payload.author?.username ?? "idk")] \(payload.content)")
}
}
}

Modifiers

To create a modifier, you just extend ExtensibleCommand with new methods that return type Self.

Here’s some of the interface for ExtensibleCommand:

extension ExtensibleCommand {
public func preAction(_ action: @escaping (BaseContextCommand, any GatewayManager, DiscordCache, Interaction, DatabaseBranches) async throws -> Void) -> Self
public func postAction(_ action: @escaping (BaseContextCommand, any GatewayManager, DiscordCache, Interaction, DatabaseBranches) async throws -> Void) -> Self
public func catchAction(_ action: @escaping (any Error, BaseContextCommand, any GatewayManager, DiscordCache, Interaction, DatabaseBranches) async throws -> Void) -> Self
public func boot(_ action: @escaping (BaseContextCommand, BotInstance) async throws -> Void) -> Self
}

You can combine these modifiers inside of your own modifier to create a chain of actions that will be executed as part of an interaction. The boot modifier runs your closure immediately after extension registering finishes so you can do initial setup if needed, allowing you to work with a global extension instance.

Here’s an example of a modifier that prints something before and after a command is run:

extension ExtensibleCommand {
func logUsages() -> Self {
self
.boot { cmd, _ in
print("\(cmd.baseInfo.name) command exists!")
}
.catchAction { error, cmd, _, _, i, _ in
print("\(cmd.baseInfo.name) command errored for \(i.interaction.member?.user?.username ?? "unknown user") with error: \(error)")
}
.preAction { cmd, _, _, i, _ in
print("\(cmd.baseInfo.name) command started by \(i.interaction.member?.user?.username ?? "unknown user")")
}
.postAction { cmd, _, _, i, _ in
print("\(cmd.baseInfo.name) command completed for \(i.interaction.member?.user?.username ?? "unknown user")")
}
}
}

This modifier will print the command name when the bot boots, when the command is started, when the command is completed, and when the command errors.

Command("failable") { i, _, _ in
struct Egg: Decodable {
var gm: String
}
let data = "{}".data(using: .utf8)!
_ = try JSONDecoder().decode(Egg.self, from: data)
}
.integrationType(.all, contexts: .all)
.logUsages()

In this example, this command will fail, and the modifier will print the command name, the user who started the command, and the error it threw. The postAction will not run in this case.

You can then combine all of the above with your existing skills to make a powerful extension that’ll help make people’s bots better.

Modifier utilities

You may need to interact with your global extension from within a modifier, but you can’t store a reference to it anywhere that you can access easily.

To solve this, DDBKit exposes some utilities, one of which can pull a reference from the BotInstance for you quickly and easily.

// In your modifier
extension ExtensibleCommand {
func myModifier() -> Self {
self
.boot { cmd, i in
let myExtension = i.instance[ext: Configurator.self]
myExtension.doSomething()
}
}
}

InteractionExtras Extensions

InteractionExtras is the structure passed into all interaction closures in DDBKit, which exposes it’s wrapped Interaction structure and a reference to the BotInstance. It’s extended with computed properties to expose the gateway and cache objects, and you can extend it with your own properties and methods for your extension.

public extension InteractionExtras {
// Does a thing in MyExtension with the interaction
func doSomething() async throws {
let ext = self.instance[ext: MyExtension.self]
try await ext.doSomething(self)
}
}

You can now expose methods and properties from InteractionExtras to your extension for users.