From e9cf9485e5c8eef0c5ac0d5ae4343c2c069d2604 Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Wed, 29 Sep 2021 22:47:07 +0100 Subject: [PATCH 1/2] Re-implement all handler-functions to take two arguments. --- src/cljukebox/handlers.clj | 33 ++++++++++++++++--------------- src/cljukebox/handlers/clear.clj | 11 +++++++---- src/cljukebox/handlers/leave.clj | 13 +++++++----- src/cljukebox/handlers/loop.clj | 17 +++++++++------- src/cljukebox/handlers/play.clj | 33 ++++++++++++++----------------- src/cljukebox/handlers/prefix.clj | 18 ++++++++--------- src/cljukebox/handlers/queue.clj | 15 ++++++++------ src/cljukebox/handlers/skip.clj | 19 ++++++++++-------- src/cljukebox/util.clj | 6 ++++++ 9 files changed, 92 insertions(+), 73 deletions(-) diff --git a/src/cljukebox/handlers.clj b/src/cljukebox/handlers.clj index 9bb0e73..9d6b3ef 100644 --- a/src/cljukebox/handlers.clj +++ b/src/cljukebox/handlers.clj @@ -19,22 +19,23 @@ "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 `" - :fields (mapv (fn [[k {:keys [doc]}]] - {:name k - :value doc}) - base-handlers)})))) +(defn help-handler + ([{:keys [message-channel content] :as data}] + (if-let [command (util/get-arguments content)] + (help-handler data {:command command}) + (util/send-embed message-channel {:title "Help Menu" + :description "For more information about specific commands, use `help `" + :fields (mapv (fn [[k {:keys [doc]}]] + {:name k + :value doc}) + base-handlers)}))) + ([{:keys [message-channel] :as data} {:keys [command] :as opts}] + (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))))) (def handlers (assoc base-handlers "help" {:handler-fn help-handler})) diff --git a/src/cljukebox/handlers/clear.clj b/src/cljukebox/handlers/clear.clj index 9d8d441..bb6ab6a 100644 --- a/src/cljukebox/handlers/clear.clj +++ b/src/cljukebox/handlers/clear.clj @@ -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." diff --git a/src/cljukebox/handlers/leave.clj b/src/cljukebox/handlers/leave.clj index 07e1d1d..052fe0f 100644 --- a/src/cljukebox/handlers/leave.clj +++ b/src/cljukebox/handlers/leave.clj @@ -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" diff --git a/src/cljukebox/handlers/loop.clj b/src/cljukebox/handlers/loop.clj index 93bc68f..39303d9 100644 --- a/src/cljukebox/handlers/loop.clj +++ b/src/cljukebox/handlers/loop.clj @@ -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" diff --git a/src/cljukebox/handlers/play.clj b/src/cljukebox/handlers/play.clj index 4925bfd..5586491 100644 --- a/src/cljukebox/handlers/play.clj +++ b/src/cljukebox/handlers/play.clj @@ -1,24 +1,21 @@ (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" diff --git a/src/cljukebox/handlers/prefix.clj b/src/cljukebox/handlers/prefix.clj index 81ab18d..f6ca611 100644 --- a/src/cljukebox/handlers/prefix.clj +++ b/src/cljukebox/handlers/prefix.clj @@ -1,14 +1,14 @@ (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 "Command prefix is 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 `^`)" diff --git a/src/cljukebox/handlers/queue.clj b/src/cljukebox/handlers/queue.clj index c0c863a..0736c30 100644 --- a/src/cljukebox/handlers/queue.clj +++ b/src/cljukebox/handlers/queue.clj @@ -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" diff --git a/src/cljukebox/handlers/skip.clj b/src/cljukebox/handlers/skip.clj index 1e2f64c..5b0d168 100644 --- a/src/cljukebox/handlers/skip.clj +++ b/src/cljukebox/handlers/skip.clj @@ -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)" diff --git a/src/cljukebox/util.clj b/src/cljukebox/util.clj index cb44721..41a0a7f 100644 --- a/src/cljukebox/util.clj +++ b/src/cljukebox/util.clj @@ -1,5 +1,6 @@ (ns cljukebox.util (:require [clojure.edn :as edn] + [clojure.string :as string] [clojure.java.io :as io] [medley.core :as medley]) (:import [java.util.function Consumer Function] @@ -70,3 +71,8 @@ (let [minutes (int (/ (/ millis 1000) 60)) seconds (int (mod (/ millis 1000) 60))] (format "%d:%d" minutes seconds))) + +(defn get-arguments [content-with-command] + (let [args (rest (string/split content-with-command #" "))] + (cond-> args + (< (count args) 2) first))) From d51b3e350e9f31d7eab82a1ab14d2f06d8d3c4aa Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Thu, 30 Sep 2021 01:06:48 +0100 Subject: [PATCH 2/2] Add ChatInputEvent handling. --- project.clj | 5 +- src/cljukebox/core.clj | 78 +++++++++++++++++++++++++++++-- src/cljukebox/handlers.clj | 27 ++++++----- src/cljukebox/handlers/play.clj | 6 ++- src/cljukebox/handlers/prefix.clj | 5 +- src/cljukebox/util.clj | 20 ++++++++ 6 files changed, 122 insertions(+), 19 deletions(-) diff --git a/project.clj b/project.clj index 5a4f13b..0d76bc5 100644 --- a/project.clj +++ b/project.clj @@ -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"]] diff --git a/src/cljukebox/core.clj b/src/cljukebox/core.clj index 7fe5b6e..1c7b9ac 100644 --- a/src/cljukebox/core.clj +++ b/src/cljukebox/core.clj @@ -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) diff --git a/src/cljukebox/handlers.clj b/src/cljukebox/handlers.clj index 9d6b3ef..cc44ccf 100644 --- a/src/cljukebox/handlers.clj +++ b/src/cljukebox/handlers.clj @@ -21,24 +21,29 @@ (defn help-handler ([{:keys [message-channel content] :as data}] - (if-let [command (util/get-arguments content)] - (help-handler data {:command command}) + (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 `" :fields (mapv (fn [[k {:keys [doc]}]] {:name k :value doc}) - base-handlers)}))) - ([{:keys [message-channel] :as data} {:keys [command] :as opts}] - (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))))) + 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]] diff --git a/src/cljukebox/handlers/play.clj b/src/cljukebox/handlers/play.clj index 5586491..a785956 100644 --- a/src/cljukebox/handlers/play.clj +++ b/src/cljukebox/handlers/play.clj @@ -18,6 +18,10 @@ (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 " + :args [{:name "url" + :doc "URL of the track you want to play" + :required? true}] :handler-fn play-audio}) diff --git a/src/cljukebox/handlers/prefix.clj b/src/cljukebox/handlers/prefix.clj index f6ca611..61605a6 100644 --- a/src/cljukebox/handlers/prefix.clj +++ b/src/cljukebox/handlers/prefix.clj @@ -5,7 +5,7 @@ ([{: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 "Command prefix is currently set to `%s`" (util/get-prefix guild-id))))) + (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)))) @@ -13,4 +13,7 @@ (def handler-data {:doc "Sets the server wide command prefix (default is `^`)" :usage-str "prefix " + :args [{:name "new-prefix" + :doc "New prefix string for the bot to use for commands" + :required? true}] :handler-fn set-prefix}) diff --git a/src/cljukebox/util.clj b/src/cljukebox/util.clj index 41a0a7f..052b449 100644 --- a/src/cljukebox/util.clj +++ b/src/cljukebox/util.clj @@ -6,6 +6,8 @@ (:import [java.util.function Consumer Function] discord4j.core.object.entity.channel.MessageChannel discord4j.core.event.domain.message.MessageCreateEvent + discord4j.core.object.command.ApplicationCommandInteractionOption + discord4j.core.event.domain.interaction.ChatInputInteractionEvent discord4j.core.spec.EmbedCreateSpec)) (defn read-config [] @@ -39,6 +41,24 @@ :member member :voice-channel (some-> member .getVoiceState .block .getChannel .block)})) +(defn chat-input-event->map [^ChatInputInteractionEvent chat-input-event] + (let [interaction (.getInteraction chat-input-event) + member (some-> interaction .getMember (.orElse nil))] + {:guild-id (some-> interaction .getGuildId (.orElse nil) .asString) + :message-channel (.getChannel interaction) + :member member + :voice-channel (some-> member .getVoiceState .block .getChannel .block) + :command (.getCommandName chat-input-event) + :args (some->> (.getOptions chat-input-event) + (not-empty) + (map (fn [^ApplicationCommandInteractionOption option] + (let [k (keyword (.getName option)) + v (some-> (.getValue option) + (.orElse nil) + (.asString))] + [k v]))) + (into {}))})) + (defn merge-to-config [m] (let [current-config @!config updated-config (medley/deep-merge current-config m)]