From abbbd8f1a51dfff72b591521d765d2c295b721de Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Sat, 20 Jul 2024 16:00:23 +0200 Subject: [PATCH 01/15] specify correct bot user data in configs --- resources/co/gaiwan/compass/prod.edn | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/co/gaiwan/compass/prod.edn b/resources/co/gaiwan/compass/prod.edn index 15c1197..d91020f 100644 --- a/resources/co/gaiwan/compass/prod.edn +++ b/resources/co/gaiwan/compass/prod.edn @@ -1,2 +1,4 @@ {:port 8080 - :compass/origin "https://compass.heartofclojure.eu"} + :compass/origin "https://compass.heartofclojure.eu" + :discord/client-id "1228962758796775444" + :discord/public-key "f58a0fd30eaecaa690e8065717f61988d6fbdb5ee11658469d86ad016380656e"} From 8cc3c3a01f2949b7443e0df04ee8cfa5e105da8a Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Sat, 20 Jul 2024 16:29:11 +0200 Subject: [PATCH 02/15] add function to get current access token and refresh if necessary --- src/co/gaiwan/compass/http/oauth.clj | 112 ++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/src/co/gaiwan/compass/http/oauth.clj b/src/co/gaiwan/compass/http/oauth.clj index bb8133b..de7afb4 100644 --- a/src/co/gaiwan/compass/http/oauth.clj +++ b/src/co/gaiwan/compass/http/oauth.clj @@ -3,12 +3,17 @@ [clojure.string :as str] [co.gaiwan.compass.config :as config] [co.gaiwan.compass.db :as db] - [lambdaisland.uri :as uri])) + [co.gaiwan.compass.util :as util] + [datomic.api :as d] + [hato.client :as hato] + [lambdaisland.uri :as uri] + [ring.util.response :as response]) + (:import (java.time Instant))) (def discord-oauth-endpoint "https://discord.com/oauth2/authorize") (def discord-api-endpoint "https://discord.com/api/v10") -(def default-scopes ["email" "identify"]) +(def default-scopes ["email" "identify" "guilds.join" "role_connections.write"]) (defn flow-init-url ([] @@ -27,3 +32,106 @@ :scope (str/join " " scopes) :state state}))))) +;; Add as bot to server +#_ +(println (str (flow-init-url ["bot" "applications.commands"]))) + +(defn request-token [params] + (hato/post + (str discord-api-endpoint "/oauth2/token") + {:as :auto + :form-params params + :basic-auth + {:user (config/value :discord/client-id) + :pass (config/value :discord/client-secret)}})) + +(defn exchange-code [code] + (request-token + {:grant_type "authorization_code" + :code code + :redirect_uri (str (config/value :compass/origin) "/oauth2/discord/callback")})) + +(defn refresh-token [refresh-token] + (request-token + {:grant_type "refresh_token" + :refresh_token refresh-token})) + +(defn- expires-in->instant + [expires-in] + (.plusSeconds (Instant/now) (- expires-in 60))) + +(defn current-access-token + "Get a valid Discord bearer access token for a user. + + This assumes there already is an access token in the database. + The function performs a token refresh if the old access token is considered to be expired. + If refreshing the token fails, returns `nil`. + There are no guarantees regarding the lifetime of the returned access token, so this function + should be called for every request to Discord." + [user-id] + (let [oauth-data + (db/q '[:find (pull ?u [:discord/access-token + :discord/refresh-token + :discord/expires-at]) + :in $ ?uid + :where [?u :user/uuid ?uid]] + (db/db) + user-id)] + ;; check if token is expired, do a refresh in that case + (if (.isAfter (Instant/now) (:discord/expires-at oauth-data)) + (let [{:keys [body status]} (refresh-token (:discord/refresh-token oauth-data))] + (when (= status 200) + (db/transact [{:user/uuid user-id + :discord/access-token (:access_token body) + :discord/refresh-token (:refresh_token body) + :discord/expires-at (expires-in->instant (:discord/expires_in body))}]) + (:access_token body))) + (:discord/access-token oauth-data)))) + +(defn fetch-user-info [token] + (:body + (hato/get (str discord-api-endpoint "/users/@me") + {:as :auto + :headers {"Authorization" (str "Bearer " token)}}))) + +(defn GET-callback [{:keys [query-params]}] + (let [code (get query-params "code") + {:keys [status body]} (exchange-code code)] + (if (not= 200 status) + {:status 302 + :headers {"Location" "/"} + :flash [:p + "Discord OAuth2 exchange failed." + [:pre (util/pprint-str body)]] + :session {:identity nil}} + (let [{:keys [access_token refresh_token expires_in]} body + {:keys [id global_name email username]} (fetch-user-info access_token) + user-uuid (:user/uuid (d/entity (db/db) [:user/email email]) (random-uuid)) + tx-data + [{:user/uuid user-uuid + :user/email email + :user/name global_name + :user/handle username + :discord/id id + :discord/access-token access_token + :discord/refresh-token refresh_token + :discord/expires-at (expires-in->instant expires_in)}]] + (def tx-data tx-data) + @(db/transact tx-data ) + {:status 302 + :headers {"Location" "/"} + :flash [:p "You are signed in!"] + :session {:identity user-uuid}})))) + +(defn routes [] + ["" + ["/oauth2" + ["/discord" + ["/callback" + {:get {:handler GET-callback}}]]] + ["/logout" + {:get {:handler (fn [req] + (assoc + (response/redirect "/") + :flash "You were signed out" + :session {}))}}]]) From 95b05ae412ae95ce5d165517bae0d5ff399a12b1 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Sat, 10 Aug 2024 21:25:56 +0200 Subject: [PATCH 03/15] feat: join-server oauth function --- src/co/gaiwan/compass/http/oauth.clj | 33 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/co/gaiwan/compass/http/oauth.clj b/src/co/gaiwan/compass/http/oauth.clj index de7afb4..a2efc39 100644 --- a/src/co/gaiwan/compass/http/oauth.clj +++ b/src/co/gaiwan/compass/http/oauth.clj @@ -7,14 +7,20 @@ [datomic.api :as d] [hato.client :as hato] [lambdaisland.uri :as uri] - [ring.util.response :as response]) + [ring.util.response :as response] + [io.pedestal.log :as log]) (:import (java.time Instant))) (def discord-oauth-endpoint "https://discord.com/oauth2/authorize") (def discord-api-endpoint "https://discord.com/api/v10") + (def default-scopes ["email" "identify" "guilds.join" "role_connections.write"]) +(defn bot-auth-headers [] + {"Authorization" (str "Bot " (config/value :discord/bot-token))}) + + (defn flow-init-url ([] (flow-init-url nil)) @@ -88,11 +94,22 @@ (:access_token body))) (:discord/access-token oauth-data)))) + (defn fetch-user-info [token] (:body (hato/get (str discord-api-endpoint "/users/@me") {:as :auto - :headers {"Authorization" (str "Bearer " token)}}))) + :oauth-token token}))) + +(defn join-server [token] + (let [{:keys [id username]} (fetch-user-info token)] + (log/trace :discord/adding-user username) + (hato/put + (str discord-api-endpoint "/guilds/" (config/value :discord/server-id) "/members/" id) + {:as :auto + :content-type :json + :form-params {:access_token token} + :headers (bot-auth-headers)}))) (defn GET-callback [{:keys [query-params]}] (let [code (get query-params "code") @@ -115,12 +132,14 @@ :discord/id id :discord/access-token access_token :discord/refresh-token refresh_token - :discord/expires-at (expires-in->instant expires_in)}]] - (def tx-data tx-data) - @(db/transact tx-data ) + :discord/expires-at (expires-in->instant expires_in)}] + {:keys [status]} (join-server access_token)] + @(db/transact tx-data) {:status 302 :headers {"Location" "/"} - :flash [:p "You are signed in!"] + :flash [:p "You are signed in!" + (when-not (= status 200) + [:br "Unfortunately, adding you to our Discord server didn't work."])] :session {:identity user-uuid}})))) (defn routes [] @@ -128,7 +147,7 @@ ["/oauth2" ["/discord" ["/callback" - {:get {:handler GET-callback}}]]] + {:get {:handler #'GET-callback}}]]] ["/logout" {:get {:handler (fn [req] (assoc From fa625dd1ae850be3a81dcd856f4cbd23f1855d66 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Wed, 14 Aug 2024 22:33:19 +0200 Subject: [PATCH 04/15] fix: duplicate oauth namespaces, join server response check --- src/co/gaiwan/compass/http/oauth.clj | 73 ++++---------------------- src/co/gaiwan/compass/routes/oauth.clj | 44 ++++------------ src/co/gaiwan/compass/util.clj | 4 ++ 3 files changed, 24 insertions(+), 97 deletions(-) diff --git a/src/co/gaiwan/compass/http/oauth.clj b/src/co/gaiwan/compass/http/oauth.clj index a2efc39..768fd65 100644 --- a/src/co/gaiwan/compass/http/oauth.clj +++ b/src/co/gaiwan/compass/http/oauth.clj @@ -4,12 +4,9 @@ [co.gaiwan.compass.config :as config] [co.gaiwan.compass.db :as db] [co.gaiwan.compass.util :as util] - [datomic.api :as d] [hato.client :as hato] [lambdaisland.uri :as uri] - [ring.util.response :as response] - [io.pedestal.log :as log]) - (:import (java.time Instant))) + [io.pedestal.log :as log])) (def discord-oauth-endpoint "https://discord.com/oauth2/authorize") (def discord-api-endpoint "https://discord.com/api/v10") @@ -20,7 +17,6 @@ (defn bot-auth-headers [] {"Authorization" (str "Bot " (config/value :discord/bot-token))}) - (defn flow-init-url ([] (flow-init-url nil)) @@ -62,10 +58,6 @@ {:grant_type "refresh_token" :refresh_token refresh-token})) -(defn- expires-in->instant - [expires-in] - (.plusSeconds (Instant/now) (- expires-in 60))) - (defn current-access-token "Get a valid Discord bearer access token for a user. @@ -90,7 +82,7 @@ (db/transact [{:user/uuid user-id :discord/access-token (:access_token body) :discord/refresh-token (:refresh_token body) - :discord/expires-at (expires-in->instant (:discord/expires_in body))}]) + :discord/expires-at (util/expires-in->instant (:discord/expires_in body))}]) (:access_token body))) (:discord/access-token oauth-data)))) @@ -102,55 +94,12 @@ :oauth-token token}))) (defn join-server [token] - (let [{:keys [id username]} (fetch-user-info token)] - (log/trace :discord/adding-user username) - (hato/put - (str discord-api-endpoint "/guilds/" (config/value :discord/server-id) "/members/" id) - {:as :auto - :content-type :json - :form-params {:access_token token} - :headers (bot-auth-headers)}))) - -(defn GET-callback [{:keys [query-params]}] - (let [code (get query-params "code") - {:keys [status body]} (exchange-code code)] - (if (not= 200 status) - {:status 302 - :headers {"Location" "/"} - :flash [:p - "Discord OAuth2 exchange failed." - [:pre (util/pprint-str body)]] - :session {:identity nil}} - (let [{:keys [access_token refresh_token expires_in]} body - {:keys [id global_name email username]} (fetch-user-info access_token) - user-uuid (:user/uuid (d/entity (db/db) [:user/email email]) (random-uuid)) - tx-data - [{:user/uuid user-uuid - :user/email email - :user/name global_name - :user/handle username - :discord/id id - :discord/access-token access_token - :discord/refresh-token refresh_token - :discord/expires-at (expires-in->instant expires_in)}] - {:keys [status]} (join-server access_token)] - @(db/transact tx-data) - {:status 302 - :headers {"Location" "/"} - :flash [:p "You are signed in!" - (when-not (= status 200) - [:br "Unfortunately, adding you to our Discord server didn't work."])] - :session {:identity user-uuid}})))) - -(defn routes [] - ["" - ["/oauth2" - ["/discord" - ["/callback" - {:get {:handler #'GET-callback}}]]] - ["/logout" - {:get {:handler (fn [req] - (assoc - (response/redirect "/") - :flash "You were signed out" - :session {}))}}]]) + (let [{:keys [id username]} (fetch-user-info token) + response + (hato/put + (str discord-api-endpoint "/guilds/" (config/value :discord/server-id) "/members/" id) + {:as :auto + :content-type :json + :form-params {:access_token token} + :headers (bot-auth-headers)})] + (log/trace :discord/user-add username :discord/add-guild-member-response response))) diff --git a/src/co/gaiwan/compass/routes/oauth.clj b/src/co/gaiwan/compass/routes/oauth.clj index 5f9c1f2..cecd4c4 100644 --- a/src/co/gaiwan/compass/routes/oauth.clj +++ b/src/co/gaiwan/compass/routes/oauth.clj @@ -1,39 +1,15 @@ (ns co.gaiwan.compass.routes.oauth (:require - [clojure.string :as str] - [co.gaiwan.compass.config :as config] [co.gaiwan.compass.db :as db] [co.gaiwan.compass.html.auth :as auth-html] [co.gaiwan.compass.http.oauth :as oauth] [co.gaiwan.compass.util :as util] [datomic.api :as d] - [hato.client :as hato] - [lambdaisland.uri :as uri] [ring.util.response :as response])) -(defn exchange-code [code] - (hato/post - (str oauth/discord-api-endpoint "/oauth2/token") - {:as :auto - :form-params - {:grant_type "authorization_code" - :code code - :redirect_uri (str (config/value :compass/origin) "/oauth2/discord/callback")} - :basic-auth - {:user (config/value :discord/client-id) - :pass (config/value :discord/client-secret)}})) - -(defn fetch-user-info [token] - (:body - (hato/get (str oauth/discord-api-endpoint "/users/@me") - {:as :auto - :headers {"Authorization" (str "Bearer " token)}}))) - (defn GET-callback [{:keys [query-params]}] (let [code (get query-params "code") - state (get query-params "state") - {:keys [status body]} (exchange-code code) - redirect-url (:oauth/redirect-url (db/entity [:oauth/state-id (parse-uuid state)]))] + {:keys [status body]} (oauth/exchange-code code)] (if (not= 200 status) {:status 302 :headers {"Location" "/"} @@ -42,25 +18,24 @@ [:pre (util/pprint-str body)]] :session {:identity nil}} (let [{:keys [access_token refresh_token expires_in]} body - {:keys [id global_name email username] :as user-info} (fetch-user-info access_token) - avatar-url (str "https://cdn.discordapp.com/avatars/" id "/" (:avatar user-info) ".png") - ;;_ (prn :avatar-url avatar-url) + {:keys [id global_name email username]} (oauth/fetch-user-info access_token) user-uuid (:user/uuid (d/entity (db/db) [:user/email email]) (random-uuid)) tx-data [{:user/uuid user-uuid :user/email email :user/name global_name :user/handle username - :discord/avatar-url avatar-url :discord/id id :discord/access-token access_token :discord/refresh-token refresh_token - :discord/expires-at (.plusSeconds (java.time.Instant/now) (- expires_in 60))}]] - (def tx-data tx-data) + :discord/expires-at (util/expires-in->instant expires_in)}] + {:keys [status]} (oauth/join-server access_token)] @(db/transact tx-data) {:status 302 - :headers {"Location" (or redirect-url "/")} - #_#_:flash [:p "You are signed in!"] + :headers {"Location" "/"} + :flash [:p "You are signed in!" + (when-not (= 2 (quot status 100)) + [:br "Unfortunately, adding you to our Discord server didn't work."])] :session {:identity user-uuid}})))) (defn GET-login [req] @@ -72,8 +47,7 @@ ["/oauth2" ["/discord" ["/callback" - {:get {:handler GET-callback}}]]] - ["/login" {:get {:handler GET-login}}] + {:get {:handler #'GET-callback}}]]] ["/logout" {:get {:handler (fn [req] (assoc diff --git a/src/co/gaiwan/compass/util.clj b/src/co/gaiwan/compass/util.clj index 1414414..b145884 100644 --- a/src/co/gaiwan/compass/util.clj +++ b/src/co/gaiwan/compass/util.clj @@ -96,3 +96,7 @@ (vector? res) (mapv deep-underscorize-keys res) (map? res) (update-vals (underscorize-keys res) deep-underscorize-keys) :else res)) + +(defn expires-in->instant + [expires-in] + (.plusSeconds (Instant/now) (- expires-in 60))) From 3fcc33be8cbb48b970946a7f8c09948e7f0588d5 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 15:07:37 +0200 Subject: [PATCH 05/15] chore: separate oauth and discord specific stuff --- src/co/gaiwan/compass/http/oauth.clj | 32 ++++++-------------------- src/co/gaiwan/compass/routes/oauth.clj | 5 ++-- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/co/gaiwan/compass/http/oauth.clj b/src/co/gaiwan/compass/http/oauth.clj index 768fd65..9fc687f 100644 --- a/src/co/gaiwan/compass/http/oauth.clj +++ b/src/co/gaiwan/compass/http/oauth.clj @@ -1,4 +1,8 @@ (ns co.gaiwan.compass.http.oauth + "Namespace for generic OAuth2 handling (authorization code flow). + + For now, this contains Discord-specific scopes and URLs, like the routes/oauth namespace as well. + This can eventually be changed." (:require [clojure.string :as str] [co.gaiwan.compass.config :as config] @@ -6,17 +10,13 @@ [co.gaiwan.compass.util :as util] [hato.client :as hato] [lambdaisland.uri :as uri] - [io.pedestal.log :as log])) + [io.pedestal.log :as log]) + (:import (java.time Instant))) (def discord-oauth-endpoint "https://discord.com/oauth2/authorize") -(def discord-api-endpoint "https://discord.com/api/v10") - (def default-scopes ["email" "identify" "guilds.join" "role_connections.write"]) -(defn bot-auth-headers [] - {"Authorization" (str "Bot " (config/value :discord/bot-token))}) - (defn flow-init-url ([] (flow-init-url nil)) @@ -40,7 +40,7 @@ (defn request-token [params] (hato/post - (str discord-api-endpoint "/oauth2/token") + "https://discord.com/api/oauth2/token" {:as :auto :form-params params :basic-auth @@ -85,21 +85,3 @@ :discord/expires-at (util/expires-in->instant (:discord/expires_in body))}]) (:access_token body))) (:discord/access-token oauth-data)))) - - -(defn fetch-user-info [token] - (:body - (hato/get (str discord-api-endpoint "/users/@me") - {:as :auto - :oauth-token token}))) - -(defn join-server [token] - (let [{:keys [id username]} (fetch-user-info token) - response - (hato/put - (str discord-api-endpoint "/guilds/" (config/value :discord/server-id) "/members/" id) - {:as :auto - :content-type :json - :form-params {:access_token token} - :headers (bot-auth-headers)})] - (log/trace :discord/user-add username :discord/add-guild-member-response response))) diff --git a/src/co/gaiwan/compass/routes/oauth.clj b/src/co/gaiwan/compass/routes/oauth.clj index cecd4c4..f4ec096 100644 --- a/src/co/gaiwan/compass/routes/oauth.clj +++ b/src/co/gaiwan/compass/routes/oauth.clj @@ -4,6 +4,7 @@ [co.gaiwan.compass.html.auth :as auth-html] [co.gaiwan.compass.http.oauth :as oauth] [co.gaiwan.compass.util :as util] + [co.gaiwan.compass.services.discord :as discord] [datomic.api :as d] [ring.util.response :as response])) @@ -18,7 +19,7 @@ [:pre (util/pprint-str body)]] :session {:identity nil}} (let [{:keys [access_token refresh_token expires_in]} body - {:keys [id global_name email username]} (oauth/fetch-user-info access_token) + {:keys [id global_name email username]} (discord/fetch-user-info access_token) user-uuid (:user/uuid (d/entity (db/db) [:user/email email]) (random-uuid)) tx-data [{:user/uuid user-uuid @@ -29,7 +30,7 @@ :discord/access-token access_token :discord/refresh-token refresh_token :discord/expires-at (util/expires-in->instant expires_in)}] - {:keys [status]} (oauth/join-server access_token)] + {:keys [status]} (discord/join-server access_token)] @(db/transact tx-data) {:status 302 :headers {"Location" "/"} From 22cd3cf03aefeaac009965360141cbc2820ceb99 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 15:51:11 +0200 Subject: [PATCH 06/15] feat: role connection metadata --- resources/public/css/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/public/css/styles.css b/resources/public/css/styles.css index b01a098..9d32633 100644 --- a/resources/public/css/styles.css +++ b/resources/public/css/styles.css @@ -56,7 +56,7 @@ body #app { } body #app >main { - padding: var(--size-2); + padding: 0.5rem; } ul, ol { From 4ca77bfc8cc5b43f892b38ae600245bcf05b6df9 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 16:42:23 +0200 Subject: [PATCH 07/15] feat: function to find assigned tito ticket from reference and email --- src/co/gaiwan/compass/services/tito.clj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/co/gaiwan/compass/services/tito.clj b/src/co/gaiwan/compass/services/tito.clj index 39e4598..f20a1de 100644 --- a/src/co/gaiwan/compass/services/tito.clj +++ b/src/co/gaiwan/compass/services/tito.clj @@ -110,6 +110,23 @@ (registrations-tx))) (db/transact (tickets-tx))) +(defn find-assigned-ticket + "Look up a ticket from a registration reference and an email address. + + Returns nil if not found or a ticket map (including release information) if found." + [reference email] + (db/q + '[:find + (pull ?ticket [* {:tito.ticket/release [*]}]) . + :in $ ?ref ?email + :where + [?reg :tito.registration/reference ?ref] + (or [?reg :tito.registration/state "complete"] + [?reg :tito.registration/state "incomplete"]) + [?ticket :tito.ticket/registration ?reg] + [?ticket :tito.ticket/email ?email]] + (db/db) reference email)) + (comment (sync!) From 3df8a0b01d8674524d0bc9795eb1ef85f7917851 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 16:58:03 +0200 Subject: [PATCH 08/15] fix: tito sync race condition and faulty test data generation --- src/co/gaiwan/compass/services/tito.clj | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/co/gaiwan/compass/services/tito.clj b/src/co/gaiwan/compass/services/tito.clj index f20a1de..bf60c34 100644 --- a/src/co/gaiwan/compass/services/tito.clj +++ b/src/co/gaiwan/compass/services/tito.clj @@ -105,10 +105,10 @@ :tito.release/slug slug})) (defn sync! [] - (db/transact - (concat (releases-tx) - (registrations-tx))) - (db/transact (tickets-tx))) + @(db/transact + (concat (releases-tx) + (registrations-tx))) + @(db/transact (tickets-tx))) (defn find-assigned-ticket "Look up a ticket from a registration reference and an email address. @@ -145,7 +145,8 @@ (faker/fake {:id (partial rand-int 9999999) :reference #"[A-Z0-9]{4}" :email [:internet :email] - :name [:name :name]})) + :name [:name :name] + :state "complete"})) tickets (for [{:keys [reference id]} registrations i (map inc (range (rand-nth [1 1 1 2 3])))] From a77d4a283839b71b21efc9caad4d1bd0ece84748 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 17:17:27 +0200 Subject: [PATCH 09/15] feat: discord service namespace (+ role connection function) --- src/co/gaiwan/compass/services/discord.clj | 89 ++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/co/gaiwan/compass/services/discord.clj diff --git a/src/co/gaiwan/compass/services/discord.clj b/src/co/gaiwan/compass/services/discord.clj new file mode 100644 index 0000000..09c5a26 --- /dev/null +++ b/src/co/gaiwan/compass/services/discord.clj @@ -0,0 +1,89 @@ +(ns co.gaiwan.compass.services.discord + (:require + [co.gaiwan.compass.config :as config] + [co.gaiwan.compass.services.tito :as tito] + [hato.client :as hato] + [io.pedestal.log :as log] + [clojure.string :as str] + [clojure.set :refer [map-invert]])) + +(def discord-api-endpoint "https://discord.com/api/v10") + +(defn bot-auth-headers [] + {"Authorization" (str "Bot " (config/value :discord/bot-token))}) + +(defn discord-bot-request + ([method endpoint] + (discord-bot-request method endpoint nil)) + ([method endpoint body] + (hato/request + (cond-> {:method method + :url (str discord-api-endpoint endpoint) + :as :auto + :headers (bot-auth-headers)} + body (assoc :content-type :json :form-params body))))) + +(defn fetch-user-info [token] + (:body + (hato/get (str discord-api-endpoint "/users/@me") + {:as :auto + :oauth-token token}))) + +(defn join-server [token] + (let [{:keys [id username]} (fetch-user-info token) + response + (discord-bot-request + :put + (str "/guilds/" (config/value :discord/server-id) "/members/" id) + {:access_token token})] + (log/trace :discord/user-add username :discord/add-guild-member-response response) + response)) + +(def role-connection-metadata + "Metadata field for roles linked to ticket type/tier. Type/tier is represented by an integer." + [{:type 7 ;; BOOLEAN_EQUAL + :key "tito_ticket_holder" + :name "Ticket holder?" + :description "User must be a ticket holder"} + {:type 3 ;; INTEGER_EQUAL + :key "tito_release_slug_idx" + :name "Tito release type" + :description + (->> (config/value :discord/ticket-roles) + (map-indexed (fn [i v] (str i " = " v))) + (str/join ", ") + (str "User must have a specific type of ticket: "))}]) + +(defn get-application + [] + (:body (discord-bot-request :get "/applications/@me"))) + +(defn update-role-connection + "Update a user's Discord role connection according to tito ticket information. + + `token` is an OAuth2 token with role_connections.write scope. + `ticket` is a ticket map containing the associated release or `nil`, if the user should not be considered a ticket holder (anymore)." + [token {{:keys [tito.release/slug]} :tito.ticket/release :as ticket}] + (hato/put + (str discord-api-endpoint "/users/@me/applications/" (:id (get-application)) "/role-connection") + {:as :auto + :oauth-token token + :content-type :json + :form-params + {:metadata + ;; Find if release slug is among the "special" role slugs declared in the config + (let [slug-idx (->> (config/value :discord/ticket-roles) + (map-indexed vector) + (some (fn [[i v]] (when (= v slug) i))))] + ;; Ticket holder property is "true" if user _has_ ticket + (cond-> {"tito_ticket_holder" (if (some? ticket) "1" "0")} + ;; slug idx is set if user has ticket with special release type + slug-idx (assoc "tito_release_slug_idx" (str slug-idx))))}})) + +(comment + ;; Register role connection metadata + (discord-bot-request :put (str "/applications/" (:id (get-application)) "/role-connections/metadata") role-connection-metadata) + ;; get user role connection + (hato/get (str discord-api-endpoint "/users/@me/applications/" (:id (get-application)) "/role-connection") + {:oauth-token "" + :as :auto})) From 717afb442874012f3aa192dd5bcdef262128ae5e Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 17:56:37 +0200 Subject: [PATCH 10/15] role connections: add platform name, remove stringification --- src/co/gaiwan/compass/services/discord.clj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/co/gaiwan/compass/services/discord.clj b/src/co/gaiwan/compass/services/discord.clj index 09c5a26..140626a 100644 --- a/src/co/gaiwan/compass/services/discord.clj +++ b/src/co/gaiwan/compass/services/discord.clj @@ -70,15 +70,16 @@ :oauth-token token :content-type :json :form-params - {:metadata + {:platform_name "Compass" + :metadata ;; Find if release slug is among the "special" role slugs declared in the config (let [slug-idx (->> (config/value :discord/ticket-roles) (map-indexed vector) (some (fn [[i v]] (when (= v slug) i))))] ;; Ticket holder property is "true" if user _has_ ticket - (cond-> {"tito_ticket_holder" (if (some? ticket) "1" "0")} + (cond-> {"tito_ticket_holder" (if (some? ticket) 1 0)} ;; slug idx is set if user has ticket with special release type - slug-idx (assoc "tito_release_slug_idx" (str slug-idx))))}})) + slug-idx (assoc "tito_release_slug_idx" slug-idx)))}})) (comment ;; Register role connection metadata From fd4bbe566c860ef803686a263ba7cbf11d479cad Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 21:11:05 +0200 Subject: [PATCH 11/15] change role connections to direct role assignment --- src/co/gaiwan/compass/services/discord.clj | 78 ++++++++++++---------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/src/co/gaiwan/compass/services/discord.clj b/src/co/gaiwan/compass/services/discord.clj index 140626a..d092391 100644 --- a/src/co/gaiwan/compass/services/discord.clj +++ b/src/co/gaiwan/compass/services/discord.clj @@ -39,47 +39,57 @@ (log/trace :discord/user-add username :discord/add-guild-member-response response) response)) -(def role-connection-metadata - "Metadata field for roles linked to ticket type/tier. Type/tier is represented by an integer." - [{:type 7 ;; BOOLEAN_EQUAL - :key "tito_ticket_holder" - :name "Ticket holder?" - :description "User must be a ticket holder"} - {:type 3 ;; INTEGER_EQUAL - :key "tito_release_slug_idx" - :name "Tito release type" - :description - (->> (config/value :discord/ticket-roles) - (map-indexed (fn [i v] (str i " = " v))) - (str/join ", ") - (str "User must have a specific type of ticket: "))}]) - (defn get-application [] (:body (discord-bot-request :get "/applications/@me"))) -(defn update-role-connection - "Update a user's Discord role connection according to tito ticket information. +;; NOTE: we originally wanted to use the linked roles Discord feature, but the UX of that turned out to be crap. +;; So now we don't use it anymore and assign configured roles directly +(comment + (def role-connection-metadata + "Metadata field for roles linked to ticket type/tier. Type/tier is represented by an integer." + [{:type 7 ;; BOOLEAN_EQUAL + :key "tito_ticket_holder" + :name "Ticket holder?" + :description "User must be a ticket holder"} + {:type 3 ;; INTEGER_EQUAL + :key "tito_release_slug_idx" + :name "Tito release type" + :description + (->> (config/value :discord/ticket-roles) + (map-indexed (fn [i v] (str i " = " v))) + (str/join ", ") + (str "User must have a specific type of ticket: "))}]) + + (defn update-role-connection + "Update a user's Discord role connection according to tito ticket information. `token` is an OAuth2 token with role_connections.write scope. `ticket` is a ticket map containing the associated release or `nil`, if the user should not be considered a ticket holder (anymore)." - [token {{:keys [tito.release/slug]} :tito.ticket/release :as ticket}] - (hato/put - (str discord-api-endpoint "/users/@me/applications/" (:id (get-application)) "/role-connection") - {:as :auto - :oauth-token token - :content-type :json - :form-params - {:platform_name "Compass" - :metadata - ;; Find if release slug is among the "special" role slugs declared in the config - (let [slug-idx (->> (config/value :discord/ticket-roles) - (map-indexed vector) - (some (fn [[i v]] (when (= v slug) i))))] - ;; Ticket holder property is "true" if user _has_ ticket - (cond-> {"tito_ticket_holder" (if (some? ticket) 1 0)} - ;; slug idx is set if user has ticket with special release type - slug-idx (assoc "tito_release_slug_idx" slug-idx)))}})) + [token {{:keys [tito.release/slug]} :tito.ticket/release :as ticket}] + (hato/put + (str discord-api-endpoint "/users/@me/applications/" (:id (get-application)) "/role-connection") + {:as :auto + :oauth-token token + :content-type :json + :form-params + {:platform_name "Compass" + :metadata + ;; Find if release slug is among the "special" role slugs declared in the config + (let [slug-idx (->> (config/value :discord/ticket-roles) + (map-indexed vector) + (some (fn [[i v]] (when (= v slug) i))))] + ;; Ticket holder property is "true" if user _has_ ticket + (cond-> {"tito_ticket_holder" (if (some? ticket) 1 0)} + ;; slug idx is set if user has ticket with special release type + slug-idx (assoc "tito_release_slug_idx" slug-idx)))}}))) + +(defn assign-ticket-role + [user-id {{:keys [tito.release/slug]} :tito.ticket/release :as _ticket}] + (let [role-endpoint (str "/guilds/" (config/value :discord/server-id) "/members/" user-id "/roles/")] + (discord-bot-request :put (str role-endpoint (config/value :discord/ticket-holder-role))) + (when-let [special-role (get (config/value :discord/ticket-roles) slug)] + (discord-bot-request :put (str role-endpoint special-role))))) (comment ;; Register role connection metadata From 9fea290e81eb8faf7ac6bfd25bcb8d7cd6951d79 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 21:13:54 +0200 Subject: [PATCH 12/15] adjust prod config back to before --- resources/co/gaiwan/compass/prod.edn | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/co/gaiwan/compass/prod.edn b/resources/co/gaiwan/compass/prod.edn index d91020f..15c1197 100644 --- a/resources/co/gaiwan/compass/prod.edn +++ b/resources/co/gaiwan/compass/prod.edn @@ -1,4 +1,2 @@ {:port 8080 - :compass/origin "https://compass.heartofclojure.eu" - :discord/client-id "1228962758796775444" - :discord/public-key "f58a0fd30eaecaa690e8065717f61988d6fbdb5ee11658469d86ad016380656e"} + :compass/origin "https://compass.heartofclojure.eu"} From 44102abe0fb2f9fbab1f4314b3caef5b074012fd Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 21:14:27 +0200 Subject: [PATCH 13/15] replace mock data --- data/tito/registrations.edn | 90 ++++++++++--------- data/tito/tickets.edn | 167 +++++++++++++++++++----------------- 2 files changed, 137 insertions(+), 120 deletions(-) diff --git a/data/tito/registrations.edn b/data/tito/registrations.edn index ffd628e..56cc5b8 100644 --- a/data/tito/registrations.edn +++ b/data/tito/registrations.edn @@ -1,40 +1,50 @@ -({:email "elisabeth.rau@rauandsons.net", - :id 3694535, - :name "Elisabeth Rau", - :reference "UAA3"} - {:email "ellsworth81@doyle-doyle.info", - :id 5870786, - :name "Ellsworth Doyle", - :reference "AZCE"} - {:email "willie97@schowalterschow.name", - :id 7495219, - :name "Willie Schowalter", - :reference "U1L3"} - {:email "augustus@rogahnrogahnand.net", - :id 928267, - :name "Augustus Rogahn", - :reference "7N7G"} - {:email "preston.rice@ricericeandrice.biz", - :id 1295322, - :name "Preston Rice", - :reference "722H"} - {:email "myrtis85@gorczanyinc.com", - :id 7430047, - :name "Myrtis Gorczany", - :reference "ZDAO"} - {:email "glynda82@nikolausnikolau.com", - :id 8078654, - :name "Glynda Nikolaus", - :reference "TFYW"} - {:email "aurelia@damoredamoreand.com", - :id 9037751, - :name "Aurelia D'Amore", - :reference "W2WB"} - {:email "janay@predovicpredovi.io", - :id 2405625, - :name "Janay Predovic", - :reference "B01E"} - {:email "juana@powlowskipowlow.co", - :id 8833862, - :name "Juana Powlowski", - :reference "78W6"}) +({:email "damon82@purdypurdyandpu.biz", + :id 3943464, + :name "Amb. Damon Purdy", + :reference "QUTU", + :state "complete"} + {:email "calvin@howellhowelland.name", + :id 7425449, + :name "Pres. Calvin Howell", + :reference "8A3F", + :state "complete"} + {:email "brendonrogahn@rogahn-rogahn.org", + :id 8328992, + :name "Brendon Rogahn", + :reference "M0Q4", + :state "complete"} + {:email "reanna68@kingkingandking.info", + :id 4421735, + :name "Reanna King", + :reference "7TL1", + :state "complete"} + {:email "thomashodkiewicz@hodkiewiczandso.biz", + :id 2195685, + :name "Thomas Hodkiewicz", + :reference "22G5", + :state "complete"} + {:email "adelina@kreigerllc.info", + :id 662414, + :name "Rep. Adelina Kreiger", + :reference "9891", + :state "complete"} + {:email "nichelle5@langworthinc.biz", + :id 7191893, + :name "Ms. Nichelle Langworth", + :reference "T7Y3", + :state "complete"} + {:email "guillermina.sawayn@sawaynsawaynand.co", + :id 6741258, + :name "Msgr. Guillermina Sawayn", + :reference "41L3", + :state "complete"} + {:email "zachariah@mrazmrazandmraz.co", + :id 5894895, + :name "Zachariah Mraz", + :reference "P3C3", + :state "complete"} + {:email "mitchell@donnellydonnell.info", + :id 1123123, + :name "Mitchell Donnelly CPA", + :reference "M80Q", + :state "complete"}) diff --git a/data/tito/tickets.edn b/data/tito/tickets.edn index 10826cc..ef9031e 100644 --- a/data/tito/tickets.edn +++ b/data/tito/tickets.edn @@ -1,91 +1,98 @@ -({:email "thuy.green@greengroup.net", - :id 9350106, - :name "Thuy Green", - :reference "UAA3-1", - :registration-id 3694535, - :release-id 1494672, - :state "reminder"} - {:email "santiago.mraz@mrazandsons.name", - :id 1409765, - :name "Santiago Mraz", - :reference "AZCE-1", - :registration-id 5870786, - :release-id 1492465, +({:email "yetta@gusikowski-gusi.info", + :id 1511427, + :name "Yetta Gusikowski", + :reference "QUTU-1", + :registration-id 3943464, + :release-id 1499881, :state "reminder"} - {:email "tomiquigley@quigley-quigley.com", - :id 2292401, - :name "Tomi Quigley", - :reference "U1L3-1", - :registration-id 7495219, - :release-id 1492844, - :state "incomplete"} - {:email "werner@collierinc.net", - :id 7892017, - :name "Werner Collier", - :reference "7N7G-1", - :registration-id 928267, + {:email "ferdinand@emmerichemmeric.biz", + :id 4091138, + :name "Ferdinand Emmerich", + :reference "QUTU-2", + :registration-id 3943464, + :release-id 1509582, + :state "complete"} + {:email "rodrick76@okunevaokunevaa.biz", + :id 3046436, + :name "Rodrick Okuneva", + :reference "QUTU-3", + :registration-id 3943464, :release-id 1509582, :state "new"} - {:email "bella@klingklingandkl.com", - :id 5004792, - :name "Prof. Bella Kling", - :reference "722H-1", - :registration-id 1295322, - :release-id 1492846, - :state "reminder"} - {:email "osvaldowilliamson@williamson-will.io", - :id 2094205, - :name "Osvaldo Williamson", - :reference "ZDAO-1", - :registration-id 7430047, - :release-id 1494678, + {:email "lenhickle@hickle-hickle.com", + :id 3774432, + :name "Len Hickle", + :reference "8A3F-1", + :registration-id 7425449, + :release-id 1492845, + :state "new"} + {:email "josefine@herzogherzogand.name", + :id 3923717, + :name "Josefine Herzog I", + :reference "M0Q4-1", + :registration-id 8328992, + :release-id 1503936, + :state "complete"} + {:email "vannessalittle@little-little.net", + :id 6905618, + :name "Vannessa Little", + :reference "7TL1-1", + :registration-id 4421735, + :release-id 1503936, :state "incomplete"} - {:email "adalberto@huelsllc.org", - :id 9301116, - :name "The Hon. Adalberto Huels", - :reference "TFYW-1", - :registration-id 8078654, - :release-id 1507003, + {:email "guadalupe80@fritschfritscha.org", + :id 8806928, + :name "Guadalupe Fritsch", + :reference "22G5-1", + :registration-id 2195685, + :release-id 1494678, :state "incomplete"} - {:email "alton35@walter-walter.co", - :id 4821390, - :name "Alton Walter", - :reference "TFYW-2", - :registration-id 8078654, - :release-id 1494672, + {:email "muriel@murraygroup.biz", + :id 8009514, + :name "Muriel Murray", + :reference "22G5-2", + :registration-id 2195685, + :release-id 1503936, :state "incomplete"} - {:email "marquis26@keeblergroup.info", - :id 5249373, - :name "Marquis Keebler", - :reference "TFYW-3", - :registration-id 8078654, + {:email "sharonlind@lind-lind.biz", + :id 6813580, + :name "Sharon Lind I", + :reference "9891-1", + :registration-id 662414, :release-id 1494678, + :state "complete"} + {:email "jasper@osinskiosinskia.net", + :id 4875657, + :name "Jasper Osinski", + :reference "T7Y3-1", + :registration-id 7191893, + :release-id 1494672, + :state "new"} + {:email "keshia.kerluke@kerlukellc.net", + :id 7202708, + :name "Keshia Kerluke DDS", + :reference "41L3-1", + :registration-id 6741258, + :release-id 1492844, :state "reminder"} - {:email "elmawalter@waltergroup.com", - :id 7506318, - :name "Elma Walter", - :reference "W2WB-1", - :registration-id 9037751, - :release-id 1492846, - :state "reminder"} - {:email "tanja83@weberllc.org", - :id 1736733, - :name "Tanja Weber", - :reference "B01E-1", - :registration-id 2405625, - :release-id 1509582, + {:email "maxwell@hintzinc.info", + :id 4196913, + :name "Maxwell Hintz", + :reference "41L3-2", + :registration-id 6741258, + :release-id 1494672, :state "incomplete"} - {:email "trentfeest@feest-feest.net", - :id 4358782, - :name "Trent Feest DO", - :reference "B01E-2", - :registration-id 2405625, + {:email "bok@kunze-kunze.net", + :id 7193569, + :name "Bok Kunze IV", + :reference "P3C3-1", + :registration-id 5894895, :release-id 1494678, :state "incomplete"} - {:email "teddy@dibbertandsons.org", - :id 3883988, - :name "Prof. Teddy Dibbert", - :reference "78W6-1", - :registration-id 8833862, - :release-id 1503936, - :state "complete"}) + {:email "william@mclaughlininc.net", + :id 8690234, + :name "William McLaughlin III", + :reference "M80Q-1", + :registration-id 1123123, + :release-id 1507003, + :state "reminder"}) From 8eec8a0d695fa69ae4f65ca8986b18e2662bbdf3 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 21:17:29 +0200 Subject: [PATCH 14/15] revert css change --- resources/public/css/styles.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/public/css/styles.css b/resources/public/css/styles.css index 9d32633..41981d5 100644 --- a/resources/public/css/styles.css +++ b/resources/public/css/styles.css @@ -56,7 +56,7 @@ body #app { } body #app >main { - padding: 0.5rem; + padding: var(--size-2); } ul, ol { @@ -516,4 +516,4 @@ body { display: grid; grid-template-columns: 10rem 1fr; gap: var(--size-2); -} \ No newline at end of file +} From 8d1411d1e1f979ca7b9e9e03c0fca0aeaa163c99 Mon Sep 17 00:00:00 2001 From: JohnnyJayJay Date: Thu, 15 Aug 2024 21:21:15 +0200 Subject: [PATCH 15/15] remove role connections oauth scope --- src/co/gaiwan/compass/http/oauth.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/co/gaiwan/compass/http/oauth.clj b/src/co/gaiwan/compass/http/oauth.clj index 9fc687f..f55fa57 100644 --- a/src/co/gaiwan/compass/http/oauth.clj +++ b/src/co/gaiwan/compass/http/oauth.clj @@ -15,7 +15,7 @@ (def discord-oauth-endpoint "https://discord.com/oauth2/authorize") -(def default-scopes ["email" "identify" "guilds.join" "role_connections.write"]) +(def default-scopes ["email" "identify" "guilds.join"]) (defn flow-init-url ([]