mirror of
https://github.com/kiranshila/Doplarr.git
synced 2026-03-31 06:24:10 -04:00
General layout now making sense
This commit is contained in:
@@ -30,8 +30,7 @@
|
||||
(defn additional-options [result]
|
||||
(a/go
|
||||
(let [quality-profiles (a/<! (impl/quality-profiles))
|
||||
{:keys [default-quality-profile
|
||||
partial-seasons]} env]
|
||||
{:keys [default-quality-profile]} env]
|
||||
{:quality-profile-id (cond
|
||||
(= 1 (count quality-profiles)) (->> quality-profiles
|
||||
first
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
(spec/def ::default-quality-profile string?)
|
||||
(spec/def ::default-language-profile string?)
|
||||
(spec/def ::role-id string?)
|
||||
(spec/def ::max-results pos-int?)
|
||||
|
||||
; Complete Config
|
||||
(spec/def ::config (spec/keys :req-un [(or (and ::sonarr-url ::sonarr-api)
|
||||
@@ -28,4 +29,5 @@
|
||||
:opt-un [::partial-seasons
|
||||
::default-quality-profile
|
||||
::default-language-profile
|
||||
::role-id]))
|
||||
::role-id
|
||||
::max-results]))
|
||||
|
||||
@@ -1,59 +1,112 @@
|
||||
(ns doplarr.core
|
||||
(:require
|
||||
[clojure.core.cache.wrapped :as cache]
|
||||
[integrant.core :as ig]
|
||||
[doplarr.config :as config]
|
||||
[doplarr.interaction-state-machine :as ism]
|
||||
[doplarr.discord :as discord]
|
||||
[discljord.messaging :as m]
|
||||
[discljord.connections :as c]
|
||||
[discljord.events :as e]
|
||||
[config.core :refer [env]]
|
||||
[clojure.core.async :as a])
|
||||
[clojure.core.async :as a]
|
||||
[doplarr.discord :as discord])
|
||||
(:gen-class))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;; Backend public interfaces
|
||||
(def backends [:radarr :sonarr :overseerr])
|
||||
(def backend-fns [:search :request :additional-options])
|
||||
|
||||
(def media-backends {:movie [:overseerr :radarr]
|
||||
:series [:overseerr :sonarr]})
|
||||
|
||||
(defn derive-backend! [backend]
|
||||
(derive (keyword "backend" (name backend)) :doplarr/backend))
|
||||
|
||||
; Generate Parent-Child Relationships
|
||||
(run! derive-backend! backends)
|
||||
|
||||
; System configuration
|
||||
(def config
|
||||
(-> (into {} (for [b backends]
|
||||
[(keyword "backend" (name b)) {:ns b}]))
|
||||
(assoc :doplarr/backends
|
||||
(into {} (for [[media backends] media-backends
|
||||
:let [backend (first (keep (config/available-backends) backends))]]
|
||||
[media (ig/ref (keyword "backend" (name backend)))]))
|
||||
:doplarr/cache {:ttl 900000}
|
||||
:discord/events {:size 100}
|
||||
:discord/bot-id (promise)
|
||||
:discord/gateway {:event (ig/ref :discord/events)}
|
||||
:discord/messaging nil)))
|
||||
|
||||
(defmethod ig/init-key :doplarr/backend [_ {:keys [ns]}]
|
||||
(zipmap backend-fns (for [f backend-fns
|
||||
:let [ns (str "doplarr.backends." (name ns))
|
||||
sym (symbol ns (name f))]]
|
||||
(requiring-resolve sym))))
|
||||
|
||||
(defmethod ig/init-key :doplarr/backends [_ m]
|
||||
m)
|
||||
|
||||
(defmethod ig/init-key :doplarr/cache [_ {:keys [ttl]}]
|
||||
(cache/ttl-cache-factory {} :ttl ttl))
|
||||
|
||||
(defmethod ig/init-key :discord/bot-id [_ p]
|
||||
p)
|
||||
|
||||
(defmethod ig/init-key :discord/events [_ {:keys [size]}]
|
||||
(a/chan size))
|
||||
|
||||
(defmethod ig/init-key :discord/gateway [_ {:keys [event]}]
|
||||
(c/connect-bot! (:bot-token env) event :intents #{:guilds}))
|
||||
|
||||
(defmethod ig/init-key :discord/messaging [_ _]
|
||||
(m/start-connection! (:bot-token env)))
|
||||
|
||||
(defmethod ig/halt-key! :discord/events [_ chan]
|
||||
(a/close! chan))
|
||||
|
||||
(defmethod ig/halt-key! :discord/messaging [_ chan]
|
||||
(m/stop-connection! chan))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;; Gateway event handlers
|
||||
(defmulti handle-event
|
||||
(fn [event-type _]
|
||||
(defmulti handle-event!
|
||||
(fn [_ event-type _]
|
||||
event-type))
|
||||
|
||||
(defmethod handle-event :interaction-create
|
||||
[_ data]
|
||||
(defmethod handle-event! :interaction-create
|
||||
[system _ data]
|
||||
(let [interaction (discord/interaction-data data)]
|
||||
(case (:type interaction)
|
||||
:application-command (ism/start-interaction interaction)
|
||||
:message-component (ism/continue-interaction interaction))))
|
||||
:application-command (ism/start-interaction! system interaction)
|
||||
:message-component (ism/continue-interaction! system interaction))))
|
||||
|
||||
(defmethod handle-event :ready
|
||||
[_ {{id :id} :user}]
|
||||
(swap! discord/state assoc :id id))
|
||||
(defmethod handle-event! :ready
|
||||
[{:discord/keys [bot-id]} _ {{id :id} :user}]
|
||||
(deliver bot-id id))
|
||||
|
||||
(defmethod handle-event :guild-create
|
||||
[_ {:keys [id]}]
|
||||
(let [guild-id id
|
||||
[{command-id :id}] @(discord/register-commands guild-id)]
|
||||
(defmethod handle-event! :guild-create
|
||||
[{:discord/keys [bot-id messaging] :as system} _ {:keys [id]}]
|
||||
(let [media-types (keys (:doplarr/backends system))
|
||||
[{command-id :id}] @(discord/register-commands media-types @bot-id messaging id)]
|
||||
(when (:role-id env)
|
||||
(discord/set-permission guild-id command-id))))
|
||||
(discord/set-permission @bot-id messaging id command-id))))
|
||||
|
||||
(defmethod handle-event :default
|
||||
[_ _])
|
||||
(defmethod handle-event! :default
|
||||
[_ _ _])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;; Bot startup and entry point
|
||||
(defn run []
|
||||
(let [event-ch (a/chan 100)
|
||||
connection-ch (c/connect-bot! (:bot-token env) event-ch :intents #{:guilds})
|
||||
messaging-ch (m/start-connection! (:bot-token env))
|
||||
init-state {:connection connection-ch
|
||||
:event event-ch
|
||||
:messaging messaging-ch}]
|
||||
(reset! discord/state init-state)
|
||||
(try (e/message-pump! event-ch handle-event)
|
||||
(finally
|
||||
(m/stop-connection! messaging-ch)
|
||||
(a/close! event-ch)))))
|
||||
(defn start-bot! []
|
||||
(let [{:discord/keys [events] :as system} (ig/init config)]
|
||||
(try (e/message-pump! events (partial handle-event! (select-keys system [:doplarr/backends
|
||||
:doplarr/cache
|
||||
:discord/messaging
|
||||
:discord/bot-id])))
|
||||
(finally (ig/halt! system)))))
|
||||
|
||||
(defn -main
|
||||
[& _]
|
||||
(when-let [config-error (config/validate-config)]
|
||||
(ex-info "Error in configuration" {:spec-error config-error})
|
||||
(System/exit -1))
|
||||
(run)
|
||||
(start-bot!)
|
||||
(shutdown-agents))
|
||||
|
||||
@@ -2,35 +2,21 @@
|
||||
(:require
|
||||
[config.core :refer [env]]
|
||||
[discljord.messaging :as m]
|
||||
[clojure.core.cache.wrapped :as cache]
|
||||
[com.rpl.specter :as s]))
|
||||
|
||||
(defonce state (atom nil))
|
||||
(defonce cache (cache/ttl-cache-factory {} :ttl 900000)) ; 15 Minute cache expiration, coinciding with the interaction token
|
||||
|
||||
(def channel-timeout 600000)
|
||||
|
||||
(defn request-command []
|
||||
(defn request-command [media-types]
|
||||
{:name "request"
|
||||
:description "Request media"
|
||||
:default_permission (boolean (not (:role-id env)))
|
||||
:options
|
||||
[{:type 1
|
||||
:name "series"
|
||||
:description "Request a series"
|
||||
:options
|
||||
[{:type 3
|
||||
:name "term"
|
||||
:description "Search term"
|
||||
:required true}]}
|
||||
{:type 1
|
||||
:name "movie"
|
||||
:description "Request a movie",
|
||||
:options
|
||||
[{:type 3
|
||||
:name "term"
|
||||
:description "Search term"
|
||||
:required true}]}]})
|
||||
(into [] (for [media media-types]
|
||||
{:type 1
|
||||
:name media
|
||||
:description (str "Request " media)
|
||||
:options [{:type 3
|
||||
:name "query"
|
||||
:description "Query"
|
||||
:required true}]}))})
|
||||
|
||||
(defn content-response [content]
|
||||
{:content content
|
||||
@@ -44,62 +30,20 @@
|
||||
2 :button
|
||||
3 :select-menu})
|
||||
|
||||
(def max-results (delay (:max-results env 10)))
|
||||
|
||||
(def request-thumbnail
|
||||
{:series "https://thetvdb.com/images/logo.png"
|
||||
:movie "https://i.imgur.com/44ueTES.png"})
|
||||
|
||||
;; Discljord setup
|
||||
(defn register-commands [guild-id]
|
||||
(defn register-commands [media-types bot-id messaging guild-id]
|
||||
(m/bulk-overwrite-guild-application-commands!
|
||||
(:messaging @state)
|
||||
(:id @state)
|
||||
guild-id
|
||||
[request-command]))
|
||||
messaging bot-id guild-id
|
||||
[(request-command media-types)]))
|
||||
|
||||
(defn set-permission [guild-id command-id]
|
||||
(defn set-permission [bot-id messaging guild-id command-id]
|
||||
(m/edit-application-command-permissions!
|
||||
(:messaging @state)
|
||||
(:id @state)
|
||||
guild-id
|
||||
command-id
|
||||
[{:id (:role-id env)
|
||||
:type 1
|
||||
:permission true}]))
|
||||
|
||||
(defn interaction-response [interaction-id interaction-token type & {:keys [ephemeral? content components embeds]}]
|
||||
(m/create-interaction-response!
|
||||
(:messaging @state)
|
||||
interaction-id
|
||||
interaction-token
|
||||
type
|
||||
:data
|
||||
(cond-> {}
|
||||
ephemeral? (assoc :flags 64)
|
||||
content (assoc :content content)
|
||||
components (assoc :components components)
|
||||
embeds (assoc :embeds embeds))))
|
||||
|
||||
(defn followup-repsonse [interaction-token & {:keys [ephermeral? content components embeds]}]
|
||||
(m/create-followup-message!
|
||||
(:messaging @state)
|
||||
(:id @state)
|
||||
interaction-token
|
||||
(cond-> {}
|
||||
ephermeral? (assoc :flags 64)
|
||||
content (assoc :content content)
|
||||
components (assoc :components components)
|
||||
embeds (assoc :embeds embeds))))
|
||||
|
||||
(defn update-interaction-response [interaction-token & {:keys [content components embeds]}]
|
||||
(m/edit-original-interaction-response!
|
||||
(:messaging @state)
|
||||
(:id @state)
|
||||
interaction-token
|
||||
:content content
|
||||
:components components
|
||||
:embeds embeds))
|
||||
messaging bot-id guild-id command-id
|
||||
[{:id (:role-id env) :type 1 :permission true}]))
|
||||
|
||||
(defn application-command-interaction-option-data [app-com-int-opt]
|
||||
[(keyword (:name app-com-int-opt))
|
||||
@@ -184,4 +128,3 @@
|
||||
(defn request-alert [selection & {:keys [season profile]}]
|
||||
{:content "This has been requested!"
|
||||
:embeds [(selection-embed selection :season season :profile profile)]})
|
||||
|
||||
|
||||
@@ -4,36 +4,72 @@
|
||||
[doplarr.discord :as discord]
|
||||
[clojure.core.async :as a]
|
||||
[com.rpl.specter :as s]
|
||||
[discljord.messaging :as m]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn start-interaction [interaction]
|
||||
(let [uuid (str (java.util.UUID/randomUUID))
|
||||
id (:id interaction)
|
||||
token (:token interaction)
|
||||
payload-opts (:options (:payload interaction))
|
||||
request-type (first (keys payload-opts))
|
||||
query (s/select-one [request-type :term] payload-opts)]
|
||||
(discord/interaction-response id token 5 :ephemeral? true)
|
||||
(a/go
|
||||
(let [results (->> (a/<! (((search-fn @backend) request-type) query))
|
||||
(into [] (take @discord/max-results)))]
|
||||
(swap! discord/cache assoc uuid {:results results
|
||||
:request-type request-type
|
||||
:token token
|
||||
:last-modified (System/currentTimeMillis)})
|
||||
(discord/update-interaction-response token (discord/search-response results uuid))))))
|
||||
(def channel-timeout 600000)
|
||||
|
||||
(defn continue-interaction [interaction]
|
||||
(defn start-interaction! [system interaction]
|
||||
(a/go
|
||||
(let [uuid (str (java.util.UUID/randomUUID))
|
||||
id (:id interaction)
|
||||
token (:token interaction)
|
||||
payload-opts (:options (:payload interaction))
|
||||
media-type (first (keys payload-opts))
|
||||
query (s/select-one [media-type :query] payload-opts)
|
||||
{:discord/keys [messaging bot-id]} system
|
||||
bot-id @bot-id]
|
||||
; Send the ack for delayed response
|
||||
@(m/create-interaction-response! messaging id token 5 :data {:ephemeral? true})
|
||||
; Search for results
|
||||
(let [results (->> (a/<! ((get-in system [:doplarr/backends media-type :search]) query))
|
||||
(take (:max-results env 10))
|
||||
(into []))]
|
||||
; Setup ttl cache entry
|
||||
(swap! (:doplarr/cache system) assoc uuid {:results results
|
||||
:media-type media-type
|
||||
:token token
|
||||
:last-modified (System/currentTimeMillis)})
|
||||
; Create dropdown for search results
|
||||
@(m/edit-original-interaction-response! messaging bot-id token (discord/search-response results uuid))))))
|
||||
|
||||
(defmulti process-event! (fn [event _ _ _] event))
|
||||
|
||||
(defmethod process-event! "result-select" [_ interaction uuid system]
|
||||
(a/go
|
||||
(let [{:doplarr/keys [cache backends]} system
|
||||
{:discord/keys [messaging bot-id]} system
|
||||
bot-id @bot-id
|
||||
{:keys [results media-type token]} (get @cache uuid)
|
||||
result (nth results (discord/dropdown-result interaction))
|
||||
add-opts (a/<! ((get-in backends [media-type :additional-options]) result))
|
||||
pending-opts (->> add-opts
|
||||
(filter #(vector? (second %)))
|
||||
(into {}))
|
||||
ready-opts (apply (partial dissoc add-opts) (keys pending-opts))]
|
||||
; Start setting up the payload
|
||||
(swap! cache assoc-in [uuid :payload] result)
|
||||
; Merge in the opts that are already satisfied
|
||||
(swap! cache update-in [uuid :payload] merge ready-opts)
|
||||
(if (empty? pending-opts)
|
||||
nil ; Go to final request screen
|
||||
nil ; Query user for all pending options
|
||||
))))
|
||||
(defn continue-interaction! [system interaction]
|
||||
(let [[event uuid] (str/split (s/select-one [:payload :component-id] interaction) #":")
|
||||
now (System/currentTimeMillis)]
|
||||
now (System/currentTimeMillis)
|
||||
{:keys [token id]} interaction
|
||||
{:discord/keys [messaging bot-id]} system
|
||||
{:doplarr/keys [cache]} system
|
||||
bot-id @bot-id]
|
||||
; Send the ack
|
||||
(discord/interaction-response (:id interaction) (:token interaction) 6)
|
||||
@(m/create-interaction-response! messaging id token 6)
|
||||
; Check last modified
|
||||
(let [{:keys [token last-modified]} (get @discord/cache uuid)]
|
||||
(if (> (- now last-modified) discord/channel-timeout)
|
||||
(let [{:keys [token last-modified]} (get @cache uuid)]
|
||||
(if (> (- now last-modified) channel-timeout)
|
||||
; Update interaction with timeout message
|
||||
(discord/update-interaction-response token (discord/content-response "Request timed out, please try again."))
|
||||
@(m/edit-original-interaction-response! messaging bot-id token (discord/content-response "Request timed out, please try again"))
|
||||
; Move through the state machine to update cache side effecting new components
|
||||
(do
|
||||
(swap! discord/cache assoc-in [uuid :last-modified] now)
|
||||
(process-event event interaction uuid))))))
|
||||
(swap! cache assoc-in [uuid :last-modified] now)
|
||||
(process-event! event interaction uuid system))))))
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
(ns doplarr.system
|
||||
(:require [integrant.core :as ig]
|
||||
[doplarr.config :as config]))
|
||||
(:require
|
||||
[doplarr.config :as config]))
|
||||
|
||||
(def backends [:radarr :sonarr :overseerr])
|
||||
(def backend-fns [:search :request :additional-options])
|
||||
|
||||
(def media-backends {:movie [:overseerr :radarr]
|
||||
:series [:overseerr :sonarr]
|
||||
; :book [:readarr]
|
||||
; :music [:lidarr]
|
||||
})
|
||||
:series [:overseerr :sonarr]})
|
||||
|
||||
(defn derive-backend! [backend]
|
||||
(derive (keyword "backend" (name backend)) :doplarr/backend))
|
||||
@@ -17,12 +14,3 @@
|
||||
; Generate Parent-Child Relationships
|
||||
(run! derive-backend! backends)
|
||||
|
||||
(def config
|
||||
(into {} (for [b backends]
|
||||
[(keyword "backend" (name b)) {:ns b}])))
|
||||
|
||||
(defmethod ig/init-key :doplarr/backend [_ {:keys [ns]}]
|
||||
(zipmap backend-fns (for [f backend-fns
|
||||
:let [ns (str "doplarr.backends." (name ns))
|
||||
sym (symbol ns (name f))]]
|
||||
(requiring-resolve sym))))
|
||||
|
||||
Reference in New Issue
Block a user