commit 36dad21f438e51e79277d9230dfc4053d7dc3d15 Author: @s.roertgen Date: Fri Aug 2 18:28:22 2024 +0200 some stuff kind of works now diff --git a/package.json b/package.json new file mode 100644 index 0000000..4ff444d --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "ied", + "scripts": { + "ancient": "clojure -Sdeps '{:deps {com.github.liquidz/antq {:mvn/version \"RELEASE\"}}}' -m antq.core", + "watch": "npx shadow-cljs watch app browser-test karma-test", + "release": "npx shadow-cljs release app", + "build-report": "npx shadow-cljs run shadow.cljs.build-report app target/build-report.html" + }, + "dependencies": { + "@noble/secp256k1": "^2.1.0", + "react": "17.0.2", + "react-dom": "17.0.2" + }, + "devDependencies": { + "shadow-cljs": "2.26.2" + } +} diff --git a/shadow-cljs.edn b/shadow-cljs.edn new file mode 100644 index 0000000..50a7259 --- /dev/null +++ b/shadow-cljs.edn @@ -0,0 +1,39 @@ +{:nrepl {:port 8777} + + :source-paths ["src" "test"] + + :dependencies + [[reagent "1.1.1"] + [re-frame "1.4.2"] + [day8.re-frame/tracing "0.6.2"] + [bidi "2.1.6"] + [clj-commons/pushy "0.3.10"] + [binaryage/devtools "1.0.6"] + [day8.re-frame/re-frame-10x "1.9.3"] + + [funcool/promesa "11.0.678"] + [nilenso/wscljs "0.2.0"] + ] + + :dev-http + {8280 "resources/public" + 8290 "target/browser-test"} + + :builds + {:app + {:target :browser + :output-dir "resources/public/js/compiled" + :asset-path "/js/compiled" + :modules + {:app {:init-fn ied.core/init}} + :devtools + {:preloads [day8.re-frame-10x.preload]} + :dev + {:compiler-options + {:closure-defines + {re-frame.trace.trace-enabled? true + day8.re-frame.tracing.trace-enabled? true}}} + :release + {:build-options + {:ns-aliases + {day8.re-frame.tracing day8.re-frame.tracing-stubs}}}}}} diff --git a/src/ied/config.cljs b/src/ied/config.cljs new file mode 100644 index 0000000..481c258 --- /dev/null +++ b/src/ied/config.cljs @@ -0,0 +1,4 @@ +(ns ied.config) + +(def debug? + ^boolean goog.DEBUG) diff --git a/src/ied/core.cljs b/src/ied/core.cljs new file mode 100644 index 0000000..5418d94 --- /dev/null +++ b/src/ied/core.cljs @@ -0,0 +1,24 @@ +(ns ied.core + (:require + [reagent.dom :as rdom] + [re-frame.core :as re-frame] + [ied.events :as events] + [ied.routes :as routes] + [ied.views :as views] + [ied.config :as config])) + +(defn dev-setup [] + (when config/debug? + (println "dev mode"))) + +(defn ^:dev/after-load mount-root [] + (re-frame/clear-subscription-cache!) + (let [root-el (.getElementById js/document "app")] + (rdom/unmount-component-at-node root-el) + (rdom/render [views/main-panel] root-el))) + +(defn init [] + (routes/start!) + (re-frame/dispatch-sync [::events/initialize-db]) + (dev-setup) + (mount-root)) diff --git a/src/ied/db.cljs b/src/ied/db.cljs new file mode 100644 index 0000000..d41f915 --- /dev/null +++ b/src/ied/db.cljs @@ -0,0 +1,8 @@ +(ns ied.db) + +(def default-db + {:name "re-frame" + :show-add-event false + :events nil + :pk nil + :sockets []}) diff --git a/src/ied/events.cljs b/src/ied/events.cljs new file mode 100644 index 0000000..75d8c3f --- /dev/null +++ b/src/ied/events.cljs @@ -0,0 +1,164 @@ +(ns ied.events + (:require + [re-frame.core :as re-frame] + [ied.db :as db] + [day8.re-frame.tracing :refer-macros [fn-traced]] + [ied.subs :as subs] + [ied.nostr :as nostr] + + [promesa.core :as p] + [wscljs.client :as ws] + [wscljs.format :as fmt])) + +(re-frame/reg-event-db + ::initialize-db + (fn-traced [_ _] + db/default-db)) + +(re-frame/reg-event-fx + ::navigate + (fn-traced [_ [_ handler]] + {:navigate handler})) + +(re-frame/reg-event-fx + ::set-active-panel + (fn-traced [{:keys [db]} [_ active-panel]] + {:db (assoc db :active-panel active-panel)})) + +;; Database Event? +(re-frame/reg-event-fx + ::save-event + ;; TODO if EOSE retrieved end connection identified by uri +;; TODO make events a set (?) + (fn-traced [{:keys [db]} [_ [uri event]]] + (println uri event) + (when (= (first event) "EVENT") + {:db (update db :events conj event)}))) + +(defn handlers + [ws-uri] + {:on-message (fn [e] (re-frame/dispatch [::save-event [ws-uri (-> (.-data e) + js/JSON.parse + (js->clj :keywordize-keys true))]])) + :on-open #(prn "Opening a new connection") + :on-close #(prn "Closing a connection")}) + +(defn create-socket + [uri] + (ws/create uri (handlers uri))) + +(re-frame/reg-event-fx + ::create-websocket + (fn-traced [{:keys [db]} [_ ws]] + {:db (update db :sockets conj (merge ws {:socket (create-socket (:uri ws))}))})) + +(re-frame/reg-event-fx + ::connect-to-websocket + (fn-traced [{:keys [db]} [_ _]] + {::connect-to-websocket-fx _})) ;; TODO identify the socket to connect to + +(re-frame/reg-fx + ::connect-to-websocket-fx + (fn [_] + (let [sockets (re-frame/subscribe [::subs/sockets])] + (println "Sockets: " @sockets) + (ws/send (:socket (first @sockets)) ["REQ" "4242" {:kinds [1 30142] + :limit 10}] fmt/json) + ; (ws/close (:socket (first @sockets))) ;; should be handled otherwise (?) + ))) + +;; TODO use id to close socket +;; add connection status to socket +;; render connect / disconnect button based on status +(re-frame/reg-event-fx + ::close-connection-to-websocket + (fn-traced [{:keys [db]} [_ _]] + {::close-connection-to-websocket-fx _})) + +(re-frame/reg-fx + ::close-connection-to-websocket-fx + (fn [_] + (let [sockets (re-frame/subscribe [::subs/sockets])] + (ws/close (:socket (first @sockets)))))) + +;; TODO +(re-frame/reg-event-fx + ::send-to-relays + (fn-traced [cofx [_ signedEvent]] + {::send-to-relays-fx signedEvent})) + +(re-frame/reg-fx + ::send-to-relays-fx + (fn [signedEvent] + (let [sockets (re-frame/subscribe [::subs/sockets])] + (.log js/console (clj->js ["EVENT" signedEvent])) + (ws/send (:socket (first @sockets)) ["EVENT" signedEvent] fmt/json)))) + +(re-frame/reg-event-db + ::update-websockets + (fn [db [_ sockets]] + (assoc db :sockets sockets))) + +(re-frame/reg-event-fx + ::remove-websocket + (fn-traced [{:keys [db]} [_ socket]] + {::remove-websocket-fx (:id socket)})) + +(re-frame/reg-fx + ::remove-websocket-fx + (fn [id] + (let [sockets (re-frame/subscribe [::subs/sockets]) + filtered (filter #(not= id (:id %)) @sockets)] ;; TODO maybe this can also be done using the URI + (re-frame/dispatch [::update-websockets filtered])))) + +(re-frame/reg-event-db + ::toggle-show-add-event + (fn [db _] + (assoc db :show-add-event (not (:show-add-event db))))) + +(re-frame/reg-event-fx + ::publish-resource + [(re-frame/inject-cofx :now)] + (fn-traced [cofx [_ resource]] + (let [event {:kind 30142 + :created_at (:now cofx) + :content "hello world" + ::tags [] + ;;:tags [["author" "" (:author resource)]] + }] + {::publish-resource-fx event}))) + +(re-frame/reg-fx + ::publish-resource-fx + (fn [resource] + (p/let [signedEvent (.nostr.signEvent js/window (clj->js resource))] + (re-frame/dispatch [::send-to-relays signedEvent])))) + +(re-frame/reg-event-fx + ::login-with-extension + (fn-traced [cofx [_ _]] + {::login-with-extension-fx _})) + +(re-frame/reg-fx + ::login-with-extension-fx + (fn [db _] + (p/let [pk (.nostr.getPublicKey js/window)] + (re-frame/dispatch [::save-pk pk])))) + +(re-frame/reg-event-db + ::save-pk + (fn-traced [db [_ pk]] + (assoc db :pk pk))) + +(re-frame/reg-event-db + ::logout + (fn-traced [db _] + (assoc db :pk nil))) + +(re-frame/reg-cofx + :now + (fn [cofx _data] ;; _data unused + (assoc cofx :now (quot (.now js/Date) 1000)))) + +(comment + (quot (.now js/Date) 1000)) diff --git a/src/ied/nostr.cljs b/src/ied/nostr.cljs new file mode 100644 index 0000000..9c46503 --- /dev/null +++ b/src/ied/nostr.cljs @@ -0,0 +1,52 @@ +(ns ied.nostr + (:require + [promesa.core :as p] + ["@noble/secp256k1" :as secp])) + +(defn event-to-serialized-json [m] + (let [event [0 + (:pubkey m) + (:created_at m) + (:kind m) + (:tags m) + (:content m)]] + + (js/JSON.stringify (clj->js event)))) + +(defn pad-start [s len pad] + (let [padding (apply str (repeat (- len (count s)) pad))] + (str padding s))) + +(defn to-hex [b] + (pad-start (.toString b 16) 2 "0")) + +(defn byte-array-to-hex [byte-array] + (apply str (map to-hex byte-array))) + +(defn sha256 [text] + (p/let [encoder (new js/TextEncoder) + data (.encode encoder text) + hash (.crypto.subtle.digest js/window "SHA-256" data) + hashArray (.from js/Array (new js/Uint8Array hash)) + byteArray (js/Uint8Array. hashArray) + hexArray (byte-array-to-hex byteArray)] + hexArray)) + +(defn signEvent [hashedEvent sk] + (.sign secp hashedEvent sk)) + +(comment + (.getPublicKey secp (.utils.randomPrivateKey secp))) + +(comment + (p/let [event {:content "hello world" + :created_at 1722606842 + :kind 1 + :pubkey "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + :tags []} + + serialized-event (event-to-serialized-json event) + id (sha256 serialized-event)] + (println serialized-event) + (println id))) + diff --git a/src/ied/routes.cljs b/src/ied/routes.cljs new file mode 100644 index 0000000..3b162b1 --- /dev/null +++ b/src/ied/routes.cljs @@ -0,0 +1,44 @@ +(ns ied.routes + (:require + [bidi.bidi :as bidi] + [pushy.core :as pushy] + [re-frame.core :as re-frame] + [ied.events :as events])) + +(defmulti panels identity) +(defmethod panels :default [] [:div "No panel found for this route."]) + +(def routes + (atom + ["/" {"" :home + "about" :about + "settings" :settings}])) + +(defn parse + [url] + (bidi/match-route @routes url)) + +(defn url-for + [& args] + (apply bidi/path-for (into [@routes] args))) + +(defn dispatch + [route] + (let [panel (keyword (str (name (:handler route)) "-panel"))] + (re-frame/dispatch [::events/set-active-panel panel]))) + +(defonce history + (pushy/pushy dispatch parse)) + +(defn navigate! + [handler] + (pushy/set-token! history (url-for handler))) + +(defn start! + [] + (pushy/start! history)) + +(re-frame/reg-fx + :navigate + (fn [handler] + (navigate! handler))) diff --git a/src/ied/subs.cljs b/src/ied/subs.cljs new file mode 100644 index 0000000..ea1a48f --- /dev/null +++ b/src/ied/subs.cljs @@ -0,0 +1,33 @@ +(ns ied.subs + (:require + [re-frame.core :as re-frame])) + +(re-frame/reg-sub + ::name + (fn [db] + (:name db))) + +(re-frame/reg-sub + ::active-panel + (fn [db _] + (:active-panel db))) + +(re-frame/reg-sub + ::sockets + (fn [db _] + (:sockets db))) + +(re-frame/reg-sub + ::events + (fn [db _] + (:events db))) + +(re-frame/reg-sub + ::show-add-event + (fn [db _] + (:show-add-event db))) + +(re-frame/reg-sub + ::pk + (fn [db _] + (:pk db))) diff --git a/src/ied/views.cljs b/src/ied/views.cljs new file mode 100644 index 0000000..d90fd23 --- /dev/null +++ b/src/ied/views.cljs @@ -0,0 +1,160 @@ +(ns ied.views + (:require + [re-frame.core :as re-frame] + [ied.events :as events] + [ied.routes :as routes] + [ied.subs :as subs] + + [reagent.core :as reagent])) + +;; add resource form +(defn add-resource-form + [name uri author] + (let [s (reagent/atom {:name name + :uri uri + :author author})] + (fn [] + [:form {:on-submit (fn [e] + (.preventDefault e) + ;; do something with the state @s + )} + [:label {:for name} "Name: "] + [:input {:type :text :name :name + :value (:name @s) + :on-change (fn [e] + (swap! s assoc :name (-> e .-target .-value)))}] + [:label {:for uri} "Uri: "] + [:input {:type :text :name :uri + :value (:uri @s) + :on-change (fn [e] + (swap! s assoc :uri (-> e .-target .-value)))}] + [:label {:for uri} "Author: "] + [:input {:type :text :name :author + :value (:author @s) + :on-change (fn [e] + (swap! s assoc :author (-> e .-target .-value)))}] + [:button {:on-click #(re-frame/dispatch [::events/publish-resource {:name (:name @s) + :uri (:uri @s) + :author (:author @s)}])} + "Publish Resource"]]))) + +;; events +(defn events-panel [] + (let [events (re-frame/subscribe [::subs/events]) + show-add-event (re-frame/subscribe [::subs/show-add-event])] + [:div + [:p {:on-click #(re-frame/dispatch [::events/toggle-show-add-event])} + (if @show-add-event "X" "Add Resource!")] + (when @show-add-event + [add-resource-form]) + [:p (str "Num of events: " (count @events))] + (if (> (count @events) 0) + (doall + (for [event @events] + ; [:li (:content (nth event 2 {:content "hello"}))] + [:li {:key (:id event)} (get (nth event 2) :content "")] + )) + [:p "no events there"])])) + +;; relays +(defn add-relay-form + [name uri] + (let [s (reagent/atom {:name name + :uri uri})] + (fn [] + [:form {:on-submit (fn [e] + (.preventDefault e) + ;; do something with the state @s + )} + [:label {:for name} "Name: "] + [:input {:type :text :name :name + :value (:name @s) + :on-change (fn [e] + (swap! s assoc :name (-> e .-target .-value)))}] + [:label {:for uri} "Uri: "] + [:input {:type :text :name :uri + :value (:uri @s) + :on-change (fn [e] + (swap! s assoc :uri (-> e .-target .-value)))}] + [:button {:on-click #(re-frame/dispatch [::events/create-websocket {:name (:name @s) + :id (random-uuid) + :uri (:uri @s)}])} + + "Add Relay"]]))) + +(defn relays-panel + [] + (let [sockets (re-frame/subscribe [::subs/sockets])] + [:div + [add-relay-form] + + (when (> (count @sockets) 0) + [:ul + (doall + (for [socket @sockets] + [:li {:key (:id socket)} + [:span (:name socket)] + [:button {:on-click #(re-frame/dispatch [::events/connect-to-websocket])} "Load events"] + [:button {:on-click #(re-frame/dispatch [::events/close-connection-to-websocket])} "Disconnect"] + [:button {:on-click #(re-frame/dispatch [::events/remove-websocket socket])} "Remove relay"]]))])])) + +;; Header + +(defn header [] + (let [pk (re-frame/subscribe [::subs/pk])] + [:div + [:a {:on-click #(re-frame/dispatch [::events/navigate :home])} + "home"] + "|" + [:a {:on-click #(re-frame/dispatch [::events/navigate :about])} + "about"] + "|" + [:a {:on-click #(re-frame/dispatch [::events/navigate :settings])} + "settings"] + "|" + (if @pk + [:a {:on-click #(re-frame/dispatch [::events/logout])} "logout"] + [:a {:on-click #(re-frame/dispatch [::events/login-with-extension])} "login"])])) + +;; Settings +(defn settings-panel [] + [:div + [:h1 "Settings"] + [relays-panel]]) + +(defmethod routes/panels :settings-panel [] [settings-panel]) + +;; Home +(defn home-panel [] + (let [name (re-frame/subscribe [::subs/name]) + events (re-frame/subscribe [::subs/events])] + [:div + [:h1 + (str "Hello from " @name ". This is the Home Page.")] + + [events-panel] + [:p (count @events)]])) + +(defmethod routes/panels :home-panel [] [home-panel]) + +;; about +(defn about-panel [] + [:div + [:h1 "This is the About Page."] + + [:div + [:a {:on-click #(re-frame/dispatch [::events/navigate :home])} + "go to Home Page"]]]) + +(defmethod routes/panels :about-panel [] [about-panel]) + +;; main +(defn main-panel [] + (let [active-panel (re-frame/subscribe [::subs/active-panel])] + [:div + [header] + (routes/panels @active-panel)])) + +(comment + + (.log js/console "Hello World"))