Skip to content

Instantly share code, notes, and snippets.

@JohnnyJayJay
Last active February 19, 2023 09:34
Show Gist options
  • Save JohnnyJayJay/5c089c1f1da1118a9e81539652cec733 to your computer and use it in GitHub Desktop.
Save JohnnyJayJay/5c089c1f1da1118a9e81539652cec733 to your computer and use it in GitHub Desktop.
Quick primer for slash commands in discljord

Working with Slash Commands in discljord

At the end of 2020, Discord introduced a new feature that is available to bots: Slash Commands.
Slash Commands belong to a new feature category called "Interactions" which finally allows bots to enhance the Discord UI. As such, the way slash commands (and upcoming interactions such as clickable buttons aka. "components") work is quite different from other parts of the API.

What exactly are Slash Commands?

Slash commands are Discord entities that you can create, edit and delete through requests. Registered commands have a name and a description and are accessible in Discord clients by typing /name.
There are two types of commands: guild and global. As the names indicate, commands of the former type are only accessible in one specific guild while the others are accessible everywhere your bot is (including DMs). Additionally, guild commands are updated instantly, while it may take up to one hour until updates to global commands are recognised. Therefore, I will focus on guild commands in this short writeup, but the endpoints for global commands are exactly the same (except that you don't need to provide a guild id).

When a command is executed by a user, you will receive an :interaction-create gateway event that contains things like the command options used, an id and a token to reference the interaction but also information about the user and the guild id, if any. Slash commands can also be received through webhooks, but discljord currently does not support this.

Finally, bots can send a response to the interaction. They can send "regular" messages, ephemeral messages (messages that are not stored and only visible to the one user), defer a response or just acknowledge the interaction without sending anything additional. The important part is that bots do respond within a small time frame, otherwise users will see an "Interaction failed" message.

In discljord

I will walk you through the creation and use of a simple command.
You can follow along in a repl. Prerequisites:

(require '[discljord.messaging :as msg])
(require '[discljord.connections :as conn])
(require '[discljord.formatting :as fmt])
(require '[clojure.core.async :as a])

(def token "YOUR_TOKEN")
(def guild-id "Id of the guild where you want to test (your bot needs to be on here!)")
(def app-id "Id of your bot")

(def api (msg/start-connection! token))
(def events (a/chan 100 (comp (filter (comp #{:interaction-create} first)) (map second))))
(def conn (conn/connect-bot! token events :intents #{}))

Following this, for slash command management I will only use the endpoints with guild in them. If you replace guild with global you get the corresponding endpoint for global slash commands.

Creating a Command

Use msg/create-guild-application-command! to create a single command, or msg/bulk-overwrite-guild-application-commands! to register a list of commands all at once (particularly useful for your production bots).
As always, discljord's structure of a Discord entity mirrors the one used in the raw API. So to understand the command structure as written in discljord, you need to understand the structure specified by Discord.

Let's create a command greet that can greet the executor, or, optionally, another user.

(def greet-options
  [{:type 6 ; The type of the option. In this case, 6 - user. See the link to the docs above for all types.
    :name "User"
    :description "The user to greet"}])

; Params: guild id (omit for global commands), command name, command description, optionally command options
@(msg/create-guild-application-command! api app-id guild-id "hello" "Say hi to someone" :options greet-options)

You should get back the registered command object as a response - it now has an id as well, which you can use to make edits via msg/edit-guild-application-command!. Or you can delete it via msg/delete-guild-application-command!.

Using the Command

You should now already be able to call the command in the guild in your Discord client - if not, hit Ctrl + R to refresh.
However if you execute it right now, it will wait for a few seconds and then tell you that the interaction has failed. That is because you haven't set up any handling for the command yet. We'll do that now.

(defn handle-command
  ; `target-id` will be the user id of the user to greet (if set)
  [{:keys [id token] {{user-id :id} :user} :member {[{target :value}] :options} :data}]
  (msg/create-interaction-response! api id token 4 :data {:content (str "Hello, " (fmt/mention-user (or target-id user-id)) " :smile:")}))

This function receives an interaction object and extracts information from it (id, token, user who excuted it, value of the option we specified).
It then sends a response to that interaction, in this case of type 4 (meaning "channel message response"). There are also some other types, most notably 5 (defer channel message response) to get more time to compute the response and send it later by editing the response (msg/edit-original-interaction-response!).
The :data option in this case is just the message you want to send. It can contain embeds, attachments and everything else you're used to. Except when you mark it as ephemeral by setting :flags 64 in the map: Then, the message will only be visible to the user and cannot contain advanced message features.

Now you only need to call this function every time you get an event from a command. You can do it like this, for example:

(a/go-loop [] 
  (when-let [interaction (a/<! events)]
    (handle-command interaction)
    (recur)))

Now you should get responses when sending commands.

Note that this code assumes that it only gets interaction events for the specific command we registered. If you were to add another command, it would get the same response because we make no distinction. In real code, you'll want to handle interaction events in a proper event handler function and check the type of the interaction (2 for slash commands) as well as the name of the command that was executed (you get that in the interaction object too).

Some Advanced Features

Permissions

You can set permissions for slash commands. Unfortunately this feature leaves much to be desired, as Discord only allows you to

  • Disable all access to a command by default
  • Set overrides for individual roles and guild members

This means it is virtually impossible to make something like sensible defaults (e.g. "only members with ban permissions may execute my /tempban command").

See Discord's documentation and msg/get-guild-application-command-permissions!, msg/get-application-command-permissions!, msg/edit-application-command-permissions!.

Followup Messages

Commands are not restricted to just one response. Using msg/create-followup-message!, you can create additional messages that reference the same interaction as your initial response. Note that this is also on a limited time frame, albeit a much longer one than msg/create-interaction-response!. Followup messages work pretty much exactly like webhook messages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment