General layout now making sense

This commit is contained in:
Kiran Shila
2021-09-26 19:43:50 -07:00
parent 6c8be304fd
commit cf7843005b
6 changed files with 167 additions and 146 deletions

View File

@@ -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

View File

@@ -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]))

View File

@@ -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))

View File

@@ -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)]})

View File

@@ -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))))))

View File

@@ -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))))