diff --git a/ops/cloud_init.sh b/ops/cloud_init.sh index 960f644..bb34a33 100644 --- a/ops/cloud_init.sh +++ b/ops/cloud_init.sh @@ -211,7 +211,7 @@ After=txor.service Restart=always RestartSec=1 WorkingDirectory=/home/compass/app/current -ExecStart=/usr/local/bin/clojure -J-Dclojure.main.report=stderr -A:prod -M -m co.gaiwan.compass run --env prod --config /home/compass/config.edn +ExecStart=/usr/local/bin/clojure -J-Dclojure.main.report=stderr -Dclojure.server.repl='{:port 5555 :accept clojure.core.server/repl}' -A:prod -M -m co.gaiwan.compass run --env prod --config /home/compass/config.edn User=compass [Install] diff --git a/resources/co/gaiwan/compass/config.edn b/resources/co/gaiwan/compass/config.edn index fdb87c1..7843864 100644 --- a/resources/co/gaiwan/compass/config.edn +++ b/resources/co/gaiwan/compass/config.edn @@ -1,4 +1,6 @@ {:port 8099 :tito/event-slug "heart-of-clojure/2024" :uploads/dir "uploads" - :dynamic-routes? false} + :dynamic-routes? false + :tito/sync-interval-seconds 900 ;; every 15 minutes + } diff --git a/resources/co/gaiwan/compass/system.edn b/resources/co/gaiwan/compass/system.edn index eeaf8dd..3e2a766 100644 --- a/resources/co/gaiwan/compass/system.edn +++ b/resources/co/gaiwan/compass/system.edn @@ -1,5 +1,6 @@ -{:compass/http {:port #config :port - :router #ig/ref :compass/router +{:compass/http {:port #config :port + :router #ig/ref :compass/router :dynamic? #config :dynamic-routes?} :compass/router {:dynamic? #config :dynamic-routes?} - :compass/db {:url #config :datomic/url}} + :compass/db {:url #config :datomic/url} + :tito/sync {:interval-seconds #config :tito/sync-interval-seconds}} diff --git a/src/co/gaiwan/compass/db/queries.clj b/src/co/gaiwan/compass/db/queries.clj index a1a8e6e..a564404 100644 --- a/src/co/gaiwan/compass/db/queries.clj +++ b/src/co/gaiwan/compass/db/queries.clj @@ -5,14 +5,22 @@ (defn all-sessions [] - (sort-by :session/time - (db/q - '[:find - [(pull ?e [* {:session/type [*] - :session/location [*]}]) ...] - :where - [?e :session/title]] - (db/db)))) + (sort-by + :session/time + (db/q + '[:find + [(pull ?e [* {:session/type [*] + :session/location [*]}]) ...] + :where + [?e :session/title]] + (db/db)))) (defn all-users [] - ) + (sort-by + :public-profile/name + (db/q + '[:find + [(pull ?e [*]) ...] + :where + [?e :public-profile/name]] + (db/db)))) diff --git a/src/co/gaiwan/compass/db/schema.clj b/src/co/gaiwan/compass/db/schema.clj index 88bebbb..f856666 100644 --- a/src/co/gaiwan/compass/db/schema.clj +++ b/src/co/gaiwan/compass/db/schema.clj @@ -5,14 +5,40 @@ ;; keyword | long | ref | string | symbol | tuple | uuid | uri (def schema - [[:user/uuid :uuid "Unique user identifier" :identity] - [:user/email :string "User email" :identity] - [:user/name :string "User name, e.g. 'Arne'"] - [:user/handle :string "User handle, e.g. 'sunnyplexus'"] - [:user/title :string "User's job title or any description, e.g. 'CEO of Gaiwan'"] - [:user/image-path :string "User image path in the compass web server"] - - [:discord/id :string "Unique user id on discord, a 'snowflake', i.e. uint64 encoded as string"] + [;; Start user entity + [:user/uuid :uuid "Unique user identifier" :identity] + [:user/contacts :ref "People you connected with / accepted a connection + request from. A :u/c B means that user A agrees to show their public profile + to user B. When two people connect we create connections in both directions, + each person can subsequently revoke their side of the connection (I no longer + want to share my details with that person). Similarly if person B does not + accept the connection, we only add it on one side, so person A can only see + B's public profile, B sees A's private profile. It should not be visible to B + that A did not accept. (they don't know if what they are seeing is public or + private.)" :many] + + [:public-profile/name :string "Publicly visible user name, e.g. 'Arne'"] + [:public-profile/avatar-url :string "Relative or absolute URL of the user's avatar"] + [:public-profile/bio :string "Free-form Markdown field"] + [:public-profile/hidden? :boolean "Hide this profile from listings or attendance lists"] + [:public-profile/links :ref "Links that are publicly visible" :many] + + [:private-profile/name :string "User name visible to contacts"] + [:private-profile/links :ref "Links that are only visible to contacts" :many] + [:private-profile/bio :string "Free-form Markdown field"] + ;; End user entity + + [:profile-link/user :ref "User this link belongs too"] + [:profile-link/type :string "`mastodon`, `linkedin`, `personal-site`, etc."] + [:profile-link/href :string "http/mailto URL"] + + ;; [:user/email :string "User email" :identity] + ;; [:user/name :string "User name, e.g. 'Arne'"] + ;; [:user/handle :string "User handle, e.g. 'sunnyplexus'"] + ;; [:user/title :string "User's job title or any description, e.g. 'CEO of Gaiwan'"] + ;; [:user/image-path :string "User image path in the compass web server"] + + [:discord/id :string "Unique user id on discord, a 'snowflake', i.e. uint64 encoded as string" :identity] [:discord/access-token :string "Discord OAuth2 access-token"] [:discord/expires-at :instant "Expiration timestamp for the OAuth2 token"] [:discord/refresh-token :string "Discord OAuth2 refresh-token"] diff --git a/src/co/gaiwan/compass/html/navigation.clj b/src/co/gaiwan/compass/html/navigation.clj index 0d02fef..f9ed3d7 100644 --- a/src/co/gaiwan/compass/html/navigation.clj +++ b/src/co/gaiwan/compass/html/navigation.clj @@ -54,7 +54,7 @@ #_[:pre (pr-str user)] [:ul [:li - (if-let [name (:user/name user)] + (if-let [name (:public-profile/name user)] [:<> [:p "Welcome, " name] [:a {:href "/logout"} "Sign out"]] diff --git a/src/co/gaiwan/compass/html/profiles.clj b/src/co/gaiwan/compass/html/profiles.clj index c1016c1..57479be 100644 --- a/src/co/gaiwan/compass/html/profiles.clj +++ b/src/co/gaiwan/compass/html/profiles.clj @@ -34,10 +34,10 @@ (o/defstyled profile-detail :div#detail [image-frame :w-100px] ([{:discord/keys [access-token id refresh-token expires-at avatar-url] - :user/keys [email handle name uuid title image-path] :as user}] + :user/keys [email handle name uuid title] :as user}] [:<> [image-frame {:profile/image - (if-let [image (or image-path avatar-url)] + (if-let [image (or (:public-profile/avatar-url user) avatar-url)] (str "url(" image ")") (str "var(--gradient-" (inc (rand-int 7)) ")"))} user] [:div.details @@ -58,10 +58,6 @@ [:label {:for "name"} "Display Name"] [:input {:id "name" :name "name" :type "text" :required true :min-length 2}]] - [:div - [:label {:for "title"} "title"] - [:input {:id "title" :name "title" :type "text" - :min-length 2}]] [:div [:label {:for "image"} "Profile Image"] [:input {:id "image" :name "image" :type "file" :accept "image/png, image/jpeg"}]] diff --git a/src/co/gaiwan/compass/model/session.clj b/src/co/gaiwan/compass/model/session.clj index f43a727..c7ab04d 100644 --- a/src/co/gaiwan/compass/model/session.clj +++ b/src/co/gaiwan/compass/model/session.clj @@ -19,13 +19,13 @@ ;; first make sure that user is already login (some? user) (or - ;; Condition 1: organized property record the user's :db/id + ;; Condition 1: organized property record the user's :db/id (= (:db/id user) (:db/id organized)) - ;; Condition 2: organized property record the user's group :db/id + ;; Condition 2: organized property record the user's group :db/id (some (comp #{(:db/id user)} :db/id) (:user-group/users organized)) - ;; Condition 3: The user belongs to orga group + ;; Condition 3: The user belongs to orga group (some :user-group/orga (:user-group/_users user)))))) @@ -102,11 +102,12 @@ (< (count participants) capacity)) sessions)) +(def default-filters + {:include-past false}) + (defn apply-filters [sessions user filters] - (def sessions sessions) - (def f filters) (reduce (fn [sessions [k v]] (apply-filter sessions user k v)) sessions - filters)) + (merge default-filters filters))) diff --git a/src/co/gaiwan/compass/routes/filters.clj b/src/co/gaiwan/compass/routes/filters.clj index 29a77fb..cc16a38 100644 --- a/src/co/gaiwan/compass/routes/filters.clj +++ b/src/co/gaiwan/compass/routes/filters.clj @@ -2,10 +2,8 @@ "Filtering behavior" (:require [co.gaiwan.compass.html.filters :as filters] - [co.gaiwan.compass.http.response :as redirect])) - -(def defaults - {:include-past false}) + [co.gaiwan.compass.http.response :as redirect] + [co.gaiwan.compass.model.session :as session])) (defn GET-filters [req] {:html/layout false @@ -18,7 +16,7 @@ :location "/" :session (assoc session :session-filters - (merge defaults + (merge session/default-filters (update-vals params keyword)))})) (defn routes [] diff --git a/src/co/gaiwan/compass/routes/oauth.clj b/src/co/gaiwan/compass/routes/oauth.clj index 9f7fd6c..e1ebf5a 100644 --- a/src/co/gaiwan/compass/routes/oauth.clj +++ b/src/co/gaiwan/compass/routes/oauth.clj @@ -45,13 +45,12 @@ :else (let [{:keys [access_token refresh_token expires_in]} body - {: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)) + {:keys [id global_name email username] :as fetch} (discord/fetch-user-info access_token) + _ (tap> {:fetch fetch}) + user-uuid (:user/uuid (d/entity (db/db) [:discord/id id]) (random-uuid)) tx-data [{:user/uuid user-uuid - :user/email email - :user/name global_name - :user/handle username + :public-profile/name global_name :discord/id id :discord/access-token access_token :discord/refresh-token refresh_token @@ -87,6 +86,6 @@ ["/logout" {:get {:handler (fn [req] (assoc - (response/redirect "/") - :flash "You were signed out" - :session {}))}}]]) + (response/redirect "/") + :flash "You were signed out" + :session {}))}}]]) diff --git a/src/co/gaiwan/compass/routes/profiles.clj b/src/co/gaiwan/compass/routes/profiles.clj index f7605f3..697eeaf 100644 --- a/src/co/gaiwan/compass/routes/profiles.clj +++ b/src/co/gaiwan/compass/routes/profiles.clj @@ -4,6 +4,7 @@ [clojure.java.io :as io] [co.gaiwan.compass.config :as config] [co.gaiwan.compass.db :as db] + [co.gaiwan.compass.db.queries :as q] [co.gaiwan.compass.html.profiles :as h] [co.gaiwan.compass.http.response :as response] [ring.util.response :as ring-response])) @@ -17,10 +18,9 @@ (:identity req)]}) (defn params->profile-data - [{:keys [name title user-id] :as params}] + [{:keys [name user-id] :as params}] {:db/id (parse-long user-id) - :user/name name - :user/title title}) + :public-profile/name name}) (defn POST-save-profile "Save profile to DB @@ -34,7 +34,7 @@ file-id (str (:db/id identity)) filepath (str (config/value :uploads/dir) "/" file-id "_" filename) {:keys [tempids]} @(db/transact [(merge - {:user/image-path (str "/" filepath)} + {:public-profile/avatar-url (str "/" filepath)} (params->profile-data params))])] ;; (tap> req) ;; Copy the image file content to the uploads directory @@ -49,7 +49,8 @@ (ring-response/not-found "File not found")))) (defn GET-attendees [req] - ) + (let [attendees (q/all-users)] + {:html/body [:p "TODO"] #_[attendees/user-list attendees]})) (defn routes [] [["/profile" diff --git a/src/co/gaiwan/compass/routes/ticket.clj b/src/co/gaiwan/compass/routes/ticket.clj index bb86f55..2ee13c2 100644 --- a/src/co/gaiwan/compass/routes/ticket.clj +++ b/src/co/gaiwan/compass/routes/ticket.clj @@ -29,7 +29,7 @@ (if-let [ticket (tito/find-unassigned-ticket (str/upper-case ref) email)] (do @(db/transact - [:db/add (:db/id ticket) :tito.ticket/assigned-to [:user/uuid (:user/uuid identity)]]) + [[:db/add (:db/id ticket) :tito.ticket/assigned-to [:user/uuid (:user/uuid identity)]]]) (discord/assign-ticket-role (:discord/id identity) ticket) (response/redirect "/" {:flash [:p "Ticket connection successful! You should now have the appropriate roles in our Discord server."]})) (response/redirect "/connect-ticket" {:status :found diff --git a/src/co/gaiwan/compass/services/tito.clj b/src/co/gaiwan/compass/services/tito.clj index fb69803..5e04001 100644 --- a/src/co/gaiwan/compass/services/tito.clj +++ b/src/co/gaiwan/compass/services/tito.clj @@ -14,7 +14,9 @@ [co.gaiwan.compass.config :as config] [co.gaiwan.compass.db :as db] [co.gaiwan.compass.util :as util] - [hato.client :as hato])) + [hato.client :as hato] + [integrant.core :as ig] + [io.pedestal.log :as log])) (def API_ENDPOINT (str "https://api.tito.io/v3/" (config/value :tito/event-slug) "/")) @@ -128,6 +130,21 @@ (not [?ticket :tito.ticket/assigned-to _])] (db/db) reference email)) +(defmethod ig/init-key :tito/sync [_ {:keys [interval-seconds]}] + (log/info :tito/starting-sync-loop {:interval-seconds interval-seconds}) + (let [stop? (volatile! false)] + (future + (while (not @stop?) + (try + (sync!) + (catch Exception e + (log/error :tito/sync-failed {} :exception e))) + (Thread/sleep (* 1000 interval-seconds)))) + stop?)) + +(defmethod ig/halt-key! :tito/sync [_ stop?] + (vreset! stop? true)) + (comment (sync!)