Liking cljdoc? Tell your friends :D

slash

A small Clojure library designed to handle and route Discord interactions, both for gateway events and incoming webhooks.

slash is environment-agnostic, extensible through middleware and works directly with Clojure data (no JSON parsing/printing included).

Clojars Project

slash is currently in a Proof-of-Concept-phase and more features are to be added.
Such features include:

  • Add functions to create components, similar to slash command helpers
  • Add more middleware: argument validation, permission checks, ...

Command Structure Definition

slash provides utilities to define slash commands in slash.command.structure.

Once you are familiar with how slash commands are structured, the functions should be self-explanatory.

Examples:

(require '[slash.command.structure :refer :all])

(def input-option (option "input" "Your input" :string :required true))

(def echo-command
  (command
   "echo"
   "Echoes your input"
   :options
   [input-option]))

(def fun-commands
  (command
   "fun"
   "Fun commands"
   :options
   [(sub-command
     "reverse"
     "Reverse the input"
     :options
     [input-option
      (option "words" "Reverse words instead of characters?" :boolean)])
    (sub-command
     "mock"
     "Spongebob-mock the input"
     :options
     [input-option])]))

Routing

You can use slash to handle interaction events based on their type.

(slash.core/route-interaction handler-map interaction-event)

handler-map is a map containing handlers for the different types of interactions that may occur. E.g.

{:ping ping-handler
 :application-command command-handler
 :message-component component-handler}

You can find default handler maps for both gateway and webhook environments in slash.gateway/slash.webhook respectively.

Commands

slash offers further routing middleware and utilities specifically for slash commands. The API is heavily inspired by compojure.

Simple, single-command example:

(require '[slash.command :as cmd] 
         '[slash.response :refer [channel-message ephemeral]]) ; The response namespace provides utility functions to create interaction responses

(cmd/defhandler echo-handler
  ["echo"] ; Command path
  _interaction ; Interaction binding - whatever you put here will be bound to the entire interaction
  [input] ; Command options - can be either a vector or a custom binding (symbol, map destructuring, ...)
  (channel-message {:content input}))

You can now use echo-handler as a command handler to call with a command interaction event and it will return the response if it is an echo command or nil if it's not.

An example with multiple (sub-)commands:

(require '[clojure.string :as str])

(cmd/defhandler reverse-handler
  ["reverse"]
  _
  [input words]
  (channel-message
   {:content (if words
               (->> #"\s+" (str/split input) reverse (str/join " "))
               (str/reverse input))}))

(cmd/defhandler mock-handler
  ["mock"]
  _
  [input]
  (channel-message
   {:content (->> input
                  (str/lower-case)
                  (map #(cond-> % (rand-nth [true false]) Character/toUpperCase))
                  str/join)}))
                  
(cmd/defhandler unknown-handler
  [unknown] ; Placeholders can be used in paths too
  {{{user-id :id} :user} :member} ; Using the interaction binding to get the user who ran the command
  _ ; no options
  (-> (channel-message {:content (str "I don't know the command `" unknown "`, <@" user-id ">.")})
      ephemeral))
      
(cmd/defpaths command-paths
  (cmd/group ["fun"] ; common prefix for all following commands
    reverse-handler 
    mock-hander
    unknown-handler))

Similar to the previous example, command-paths can now be used as a command handler. It will call each of its nested handlers with the interaction and stop once a handler is found that does not return nil.

Full Webhook Example

For this example, I use the ring webserver specification.

Using ring-json and ring-discord-auth we can create a ring handler for accepting outgoing webhooks.

(require '[slash.webhook :refer [webhook-defaults]]
         '[ring-discord-auth.ring :refer [wrap-authenticate]]
         '[ring.middleware.json :refer [wrap-json-body wrap-json-response]])

(def ring-handler
  (-> (partial slash.core/route-interaction
               (assoc webhook-defaults :application-command command-paths))
      wrap-json-response
      (wrap-json-body {:keyword? true})
      (wrap-authenticate "application public key")))

Full Gateway Example

For this example, I use discljord.

You also see the use of the wrap-response-return middleware for the interaction handler, which allows you to simply return the interaction responses from your handlers and let the middleware respond via REST. You only need to provide a callback that specifies how to respond to the interaction (as I'm using discljord here, I used its functions for this purpose).

(require '[discljord.messaging :as rest]
         '[discljord.connections :as gateway]
         '[discljord.events :as events]
         '[clojure.core.async :as a]
         '[slash.gateway :refer [gateway-defaults wrap-response-return]])

(let [rest-conn (rest/start-connection! "bot token")
      event-channel (a/chan 100)
      gateway-conn (gateway/connect-bot! "bot token" event-channel :intents #{})
      event-handler (-> slash.core/route-interaction
                        (partial (assoc gateway-defaults :application-command command-paths))
                        (wrap-response-return (fn [id token {:keys [type data]}]
                                                (rest/create-interaction-response! rest-conn id token type :data data))))]
  (events/message-pump! event-channel (partial events/dispatch-handlers {:interaction-create event-handler})))

License

Copyright © 2021 JohnnyJayJay

Licensed under the MIT license.

Can you improve this documentation? These fine people already did:
JohnnyJayJay, Johnny & Kiran Shila
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close