Skip to content

Commit

Permalink
Merge pull request #17 from danmason/add-slash-commands
Browse files Browse the repository at this point in the history
Add slash commands
  • Loading branch information
danmason authored Oct 2, 2021
2 parents 6a5f9c1 + d51b3e3 commit 6abbdf4
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 81 deletions.
5 changes: 4 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.10.1"]
[com.discord4j/discord4j-core "3.1.7"]
[com.fasterxml.jackson.core/jackson-core "2.12.5"]
[com.discord4j/discord4j-core "3.2.0"]
[com.discord4j/discord4j-rest "3.2.0"]
[com.discord4j/discord-json "1.6.10" ]
[com.sedmelluq/lavaplayer "1.3.77" ]
[ch.qos.logback/logback-classic "1.2.3"]
[medley "1.3.0"]]
Expand Down
78 changes: 73 additions & 5 deletions src/cljukebox/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,104 @@
[cljukebox.util :as util]
[cljukebox.handlers :as handlers])
(:import [discord4j.core DiscordClient GatewayDiscordClient]
[discord4j.core.object.presence Presence Status Activity]
[discord4j.core.object.presence ClientPresence Status ClientActivity]
[discord4j.discordjson.json ApplicationCommandOptionData ApplicationCommandRequest]
[discord4j.core.object.command ApplicationCommandOption$Type ApplicationCommand]
discord4j.discordjson.json.ApplicationCommandData
discord4j.core.event.domain.lifecycle.ReadyEvent
discord4j.core.event.domain.interaction.ChatInputInteractionEvent
discord4j.core.event.domain.message.MessageCreateEvent
discord4j.rest.RestClient
reactor.core.publisher.Mono)
(:gen-class))

(def !gateway-client (atom nil))

(defn on-message [message-event]
(let [{:keys [guild-id message-channel content] :as data} (util/message-event->map message-event)
(let [{:keys [guild-id content] :as data} (util/message-event->map message-event)
prefix (util/get-prefix guild-id)
handler-fn (handlers/get-handler-fn content prefix)]
(when handler-fn
(handler-fn data))
(Mono/empty)))

(defn on-chat-input [chat-input-event]
(let [{:keys [command args] :as data} (util/chat-input-event->map chat-input-event)
handler-fn (get-in handlers/handlers [command :handler-fn])]
(.subscribe (.reply chat-input-event "Command received"))
(handler-fn data args)
(Mono/empty)))

(defn on-bot-ready [^ReadyEvent ready-event]
(-> ready-event
(.getSelf)
(.getClient)
(.updatePresence (Presence/online (Activity/playing "Type '^help' for a list of commands")))
(.updatePresence (ClientPresence/online (ClientActivity/playing "Type '^help' for a list of commands")))
(.block))
(Mono/empty))

;; Useful for cleanup
(defn remove-all-command-definitions [^RestClient rest-client]
(let [application-id (-> rest-client .getApplicationId .block)
application-service (-> rest-client .getApplicationService)
commands (-> application-service
(.getGlobalApplicationCommands application-id)
(.collectMap (util/as-function (fn [x] (.name x))))
(.block))]
(run!
(fn [[x ^ApplicationCommandData command-data]]
(let [cmd-id (-> (.id command-data) (Long/parseLong))]
(-> application-service
(.deleteGlobalApplicationCommand application-id cmd-id)
(.subscribe))))
commands)))

(defn add-command-definitions [^RestClient rest-client]
(let [application-id (-> rest-client .getApplicationId .block)
application-service (-> rest-client .getApplicationService)]
(run!
(fn [[name {:keys [doc args] :as handler-info}]]
(let [base-command-request (-> (ApplicationCommandRequest/builder)
(.name name)
(.description doc))
args-as-options (mapv
(fn [{:keys [name doc required?]}]
(-> (ApplicationCommandOptionData/builder)
(.name name)
(.description doc)
(.type (.getValue ApplicationCommandOption$Type/STRING))
(.required required?)
(.build)))
args)
command-request (.build (reduce
(fn [cmd-request option-data]
(.addOption cmd-request option-data))
base-command-request
args-as-options))]
(-> application-service
(.createGlobalApplicationCommand application-id command-request)
(.subscribe))))
handlers/handlers)))

(defn handle-client [^GatewayDiscordClient client]
;; Logout client on shutdown
(.addShutdownHook (Runtime/getRuntime)
(Thread. ^Runnable (fn [] (some-> client .logout .block))))

;; Add global application commands to client
(add-command-definitions (.getRestClient client))

;; Handle events
(let [on-login (-> client
(.on ReadyEvent (util/as-function on-bot-ready))
(.then))
on-message (-> client
(.on MessageCreateEvent (util/as-function on-message))
(.then))]
(.and on-login on-message)))
(.then))
on-chat-input (-> client
(.on ChatInputInteractionEvent (util/as-function on-chat-input))
(.then))]
(-> on-login (.and on-message) (.and on-chat-input))))

(defn start-bot! [token]
(-> (DiscordClient/create token)
Expand Down
40 changes: 23 additions & 17 deletions src/cljukebox/handlers.clj
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,31 @@
"loop" loop/handler-data
"clear" clear/handler-data})

(defn help-handler [{:keys [message-channel guild-id content] :as data}]
(let [prefix (util/get-prefix guild-id)
[command] (rest (string/split content #" "))]
(if command
(if-let [{:keys [doc usage-str]} (get base-handlers command)]
(util/send-embed message-channel {:title command
:description doc
:fields [{:name "Usage Example"
:value (format "`%s`" usage-str)}]})
(util/send-message message-channel (format "*%s* is not an existing command" command)))
(util/send-embed message-channel {:title "Help Menu"
:description "For more information about specific commands, use `help <command>`"
:fields (mapv (fn [[k {:keys [doc]}]]
{:name k
:value doc})
base-handlers)}))))
(defn help-handler
([{:keys [message-channel content] :as data}]
(let [command (util/get-arguments content)]
(help-handler data {:command command})))
([{:keys [message-channel] :as data} {:keys [command] :as opts}]
(if command
(if-let [{:keys [long-doc doc usage-str]} (get base-handlers command)]
(util/send-embed message-channel {:title command
:description (or long-doc doc)
:fields [{:name "Usage Example"
:value (format "`%s`" usage-str)}]})
(util/send-message message-channel (format "*%s* is not an existing command" command)))
(util/send-embed message-channel {:title "Help Menu"
:description "For more information about specific commands, use `help <command>`"
:fields (mapv (fn [[k {:keys [doc]}]]
{:name k
:value doc})
base-handlers)}))))

(def handlers
(assoc base-handlers "help" {:handler-fn help-handler}))
(assoc base-handlers "help" {:doc "Outputs information about the various commands on the bot"
:args [{:name "command"
:required? false
:doc "Specific command you wish to ask for information on"}]
:handler-fn help-handler}))

(defn get-handler-fn [content prefix]
(some (fn [[k v]]
Expand Down
11 changes: 7 additions & 4 deletions src/cljukebox/handlers/clear.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
(:require [cljukebox.player :as player]
[cljukebox.util :as util]))

(defn clear-bot [{:keys [message-channel guild-id] :as data}]
(let [{:keys [scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)]
(.clear scheduler)
(util/send-message message-channel ":wastebasket: **Bot queue cleared!**")))
(defn clear-bot
([data]
(clear-bot data nil))
([{:keys [message-channel guild-id] :as data} _opts]
(let [{:keys [scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)]
(.clear scheduler)
(util/send-message message-channel ":wastebasket: **Bot queue cleared!**"))))

(def handler-data
{:doc "Will skip the currently playing track and remove all songs from the queue."
Expand Down
13 changes: 8 additions & 5 deletions src/cljukebox/handlers/leave.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
(:require [cljukebox.player :as player]
[cljukebox.util :as util]))

(defn leave-voice-channel [{:keys [message-channel guild-id] :as data}]
(when-let [connection (player/get-current-connection guild-id)]
(util/send-message message-channel "Leaving voice channel.")
(-> connection .disconnect .block)
(swap! player/!voice-connections dissoc guild-id)))
(defn leave-voice-channel
([data]
(leave-voice-channel data nil))
([{:keys [message-channel guild-id] :as data} _opts]
(when-let [connection (player/get-current-connection guild-id)]
(util/send-message message-channel "Leaving voice channel.")
(-> connection .disconnect .block)
(swap! player/!voice-connections dissoc guild-id))))

(def handler-data
{:doc "Leave the currently connected voice channel"
Expand Down
17 changes: 10 additions & 7 deletions src/cljukebox/handlers/loop.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

(def !loop (atom false))

(defn loop-audio [{:keys [message-channel guild-id] :as data}]
(let [{:keys [scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)
should-loop (swap! !loop not)]
(.setLoop scheduler should-loop)
(if should-loop
(util/send-message message-channel ":repeat: **Loop enabled!**")
(util/send-message message-channel ":repeat: **Loop disabled!**"))))
(defn loop-audio
([data]
(loop-audio data nil))
([{:keys [message-channel guild-id] :as data} _opts]
(let [{:keys [scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)
should-loop (swap! !loop not)]
(.setLoop scheduler should-loop)
(if should-loop
(util/send-message message-channel ":repeat: **Loop enabled!**")
(util/send-message message-channel ":repeat: **Loop disabled!**")))))

(def handler-data
{:doc "Will loop all songs that play on the bot - these will play until skipped"
Expand Down
39 changes: 20 additions & 19 deletions src/cljukebox/handlers/play.clj
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
(ns cljukebox.handlers.play
(:require [clojure.string :as string]
[cljukebox.player :as player]
(:require [cljukebox.player :as player]
[cljukebox.util :as util]))

(defn play-audio [{:keys [voice-channel message-channel guild-id content] :as data}]
(let [[_ url] (string/split content #" ")]
(cond
(nil? voice-channel)
(util/send-message message-channel "You're not in a voice channel - join one to queue songs on the bot.")

(nil? url)
(util/send-message message-channel "No media URL has been provided.")

:else
(let [{:keys [scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)]
(player/connect-to-voice guild-manager voice-channel)
(.loadItemOrdered player/player-manager
guild-manager
url
(player/mk-audio-handler scheduler message-channel))))))
(defn play-audio
([{:keys [voice-channel message-channel content] :as data}]
(if-let [url (util/get-arguments content)]
(play-audio data {:url url})
(util/send-message message-channel "No media URL has been provided.")))
([{:keys [voice-channel message-channel guild-id] :as data} {:keys [url] :as opts}]
(if-not voice-channel
(util/send-message message-channel "You're not in a voice channel - join one to queue songs on the bot.")
(let [{:keys [scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)]
(player/connect-to-voice guild-manager voice-channel)
(.loadItemOrdered player/player-manager
guild-manager
url
(player/mk-audio-handler scheduler message-channel))))))

(def handler-data
{:doc "Will add audio to the bot's playlist - if not currently playing anything, will join the bot to the calling user's voice channel. For a list of supported sources/file formats, see here: https://github.com/sedmelluq/lavaplayer#supported-formats"
{:doc "Add a track to the bot's playlist"
:long-doc "Add a track to the bot's playlist - if not currently playing anything, will join the bot to the calling user's voice channel. For a list of supported sources/file formats, see here: https://github.com/sedmelluq/lavaplayer#supported-formats"
:usage-str "play <media-url>"
:args [{:name "url"
:doc "URL of the track you want to play"
:required? true}]
:handler-fn play-audio})
21 changes: 12 additions & 9 deletions src/cljukebox/handlers/prefix.clj
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
(ns cljukebox.handlers.prefix
(:require [cljukebox.util :as util]
[clojure.string :as string]))
(:require [cljukebox.util :as util]))

(defn set-prefix [{:keys [message-channel guild-id content] :as data}]
(let [split-data (string/split content #" ")]
(if (= 2 (count split-data))
(let [[_ new-prefix] split-data]
(util/merge-to-config {guild-id {:prefix new-prefix}})
(util/send-message message-channel (format "Command prefix set to `%s`" new-prefix)))
(util/send-message message-channel (format "Command prefix is currently set to `%s`" (util/get-prefix guild-id))))))
(defn set-prefix
([{:keys [content message-channel guild-id] :as data}]
(if-let [new-prefix (util/get-arguments content)]
(set-prefix data {:new-prefix new-prefix})
(util/send-message message-channel (format "Need to supply new bot prefix (currently set to `%s`)" (util/get-prefix guild-id)))))
([{:keys [message-channel guild-id] :as data} {:keys [new-prefix] :as opts}]
(util/merge-to-config {guild-id {:prefix new-prefix}})
(util/send-message message-channel (format "Command prefix set to `%s`" new-prefix))))

(def handler-data
{:doc "Sets the server wide command prefix (default is `^`)"
:usage-str "prefix <new-prefix>"
:args [{:name "new-prefix"
:doc "New prefix string for the bot to use for commands"
:required? true}]
:handler-fn set-prefix})
15 changes: 9 additions & 6 deletions src/cljukebox/handlers/queue.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
{:name (format "%d: %s" idx title)
:value (format "**Author**: %s | **Length**: %s" author length)}))

(defn audio-queue [{:keys [message-channel guild-id] :as data}]
(let [{:keys [scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)
track-queue (.queue scheduler)]
(util/send-embed message-channel {:title "Bot Queue"
:description "Currently queued songs on the bot:"
:fields (map-indexed create-track-field track-queue)})))
(defn audio-queue
([data]
(audio-queue data nil))
([{:keys [message-channel guild-id] :as data} _opts]
(let [{:keys [scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)
track-queue (.queue scheduler)]
(util/send-embed message-channel {:title "Bot Queue"
:description "Currently queued songs on the bot:"
:fields (map-indexed create-track-field track-queue)}))))

(def handler-data
{:doc "Outputs the current player queue for the server"
Expand Down
19 changes: 11 additions & 8 deletions src/cljukebox/handlers/skip.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
(:require [cljukebox.player :as player]
[cljukebox.util :as util]))

(defn skip-audio [{:keys [message-channel guild-id] :as data}]
(let [{:keys [player scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)
current-song (.getPlayingTrack player)]
(if current-song
(let [{:keys [title]} (player/track->map current-song)]
(.skip scheduler)
(util/send-message message-channel (format "Track: `%s` has been skipped" title)))
(util/send-message message-channel "No song is currently playing on the bot - nothing skipped"))))
(defn skip-audio
([data]
(skip-audio data nil))
([{:keys [message-channel guild-id] :as data} _opts]
(let [{:keys [player scheduler] :as guild-manager} (player/get-guild-audio-manager guild-id)
current-song (.getPlayingTrack player)]
(if current-song
(let [{:keys [title]} (player/track->map current-song)]
(.skip scheduler)
(util/send-message message-channel (format "Track: `%s` has been skipped" title)))
(util/send-message message-channel "No song is currently playing on the bot - nothing skipped")))))

(def handler-data
{:doc "Skips the currently playing song, playing the next song in the queue (if any is present)"
Expand Down
Loading

0 comments on commit 6abbdf4

Please sign in to comment.