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.
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.
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.
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!
.
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).
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!
.
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.