wip group chat
This commit is contained in:
parent
47af4de5b3
commit
1fd85efc7d
|
@ -0,0 +1,11 @@
|
||||||
|
/target
|
||||||
|
/classes
|
||||||
|
/checkouts
|
||||||
|
pom.xml
|
||||||
|
pom.xml.asc
|
||||||
|
*.jar
|
||||||
|
*.class
|
||||||
|
/.lein-*
|
||||||
|
/.nrepl-port
|
||||||
|
.hgignore
|
||||||
|
.hg/
|
|
@ -10,7 +10,8 @@
|
||||||
[org.clojure/clojurescript "1.7.170"]
|
[org.clojure/clojurescript "1.7.170"]
|
||||||
[org.clojure/core.async "0.2.374" :exclusions [org.clojure/tools.reader]]
|
[org.clojure/core.async "0.2.374" :exclusions [org.clojure/tools.reader]]
|
||||||
[cljsjs/chance "0.7.3-0"]
|
[cljsjs/chance "0.7.3-0"]
|
||||||
[com.andrewmcveigh/cljs-time "0.4.0"]]
|
[com.andrewmcveigh/cljs-time "0.4.0"]
|
||||||
|
[cljsjs/web3 "0.15.3-0"]]
|
||||||
|
|
||||||
:plugins [[lein-cljsbuild "1.1.2" :exclusions [[org.clojure/clojure]]]]
|
:plugins [[lein-cljsbuild "1.1.2" :exclusions [[org.clojure/clojure]]]]
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@
|
||||||
|
|
||||||
:cljsbuild {:builds
|
:cljsbuild {:builds
|
||||||
[{:id "dev"
|
[{:id "dev"
|
||||||
:source-paths ["src"]
|
:source-paths ["src/cljs"]
|
||||||
:compiler {:asset-path "js/compiled/out"
|
:compiler {:asset-path "js/compiled/out"
|
||||||
:output-to "resources/public/js/compiled/protocol.js"
|
:output-to "resources/public/js/compiled/protocol.js"
|
||||||
:output-dir "resources/public/js/compiled/out"
|
:output-dir "resources/public/js/compiled/out"
|
||||||
|
@ -29,7 +30,7 @@
|
||||||
;; production. You can build this with:
|
;; production. You can build this with:
|
||||||
;; lein cljsbuild once min
|
;; lein cljsbuild once min
|
||||||
{:id "min"
|
{:id "min"
|
||||||
:source-paths ["src"]
|
:source-paths ["src/cljs"]
|
||||||
:compiler {:output-to "resources/public/js/compiled/protocol.js"
|
:compiler {:output-to "resources/public/js/compiled/protocol.js"
|
||||||
:optimizations :advanced
|
:optimizations :advanced
|
||||||
:pretty-print false}}]}
|
:pretty-print false}}]}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
(ns syng-im.protocol.api
|
||||||
|
(:require [cljs.core.async :refer [<! timeout]]
|
||||||
|
[syng-im.utils.random :as random]
|
||||||
|
[syng-im.protocol.state.state :as state :refer [set-storage
|
||||||
|
set-handler
|
||||||
|
set-connection
|
||||||
|
set-identity
|
||||||
|
connection
|
||||||
|
storage]]
|
||||||
|
[syng-im.protocol.state.delivery :refer [add-pending-message]]
|
||||||
|
[syng-im.protocol.state.group-chat :refer [save-keypair
|
||||||
|
get-keypair
|
||||||
|
get-identities
|
||||||
|
save-identities]]
|
||||||
|
[syng-im.protocol.delivery :refer [start-delivery-loop]]
|
||||||
|
[syng-im.protocol.web3 :refer [listen make-msg
|
||||||
|
post-msg
|
||||||
|
make-web3
|
||||||
|
new-identity]]
|
||||||
|
[syng-im.protocol.handler :refer [handle-incoming-whisper-msg]]
|
||||||
|
[syng-im.protocol.user-handler :refer [invoke-user-handler]]
|
||||||
|
[syng-im.utils.encryption :refer [new-keypair]])
|
||||||
|
(:require-macros [cljs.core.async.macros :refer [go]]))
|
||||||
|
|
||||||
|
(def default-content-type "text/plain")
|
||||||
|
|
||||||
|
(defn create-connection [ethereum-rpc-url]
|
||||||
|
(make-web3 ethereum-rpc-url))
|
||||||
|
|
||||||
|
(defn create-identity [connection]
|
||||||
|
(new-identity connection))
|
||||||
|
|
||||||
|
(defn my-identity []
|
||||||
|
(state/my-identity))
|
||||||
|
|
||||||
|
(defn init-protocol
|
||||||
|
"Required [handler ethereum-rpc-url storage]
|
||||||
|
Optional [whisper-identity] - if not passed a new identity is created automatically
|
||||||
|
|
||||||
|
(fn handler [{:keys [event-type...}])
|
||||||
|
|
||||||
|
:event-type can be:
|
||||||
|
|
||||||
|
:new-msg - [from payload]
|
||||||
|
:error - [error-msg details]
|
||||||
|
:msg-acked [msg-id from]
|
||||||
|
:delivery-failed [msg-id]
|
||||||
|
:new-group-chat [from group-id]
|
||||||
|
:group-chat-invite-acked [from group-id]
|
||||||
|
:initialized [identity]
|
||||||
|
|
||||||
|
:new-msg, msg-acked should be handled idempotently (may be called multiple times for the same msg-id)
|
||||||
|
"
|
||||||
|
[{:keys [handler ethereum-rpc-url storage identity]}]
|
||||||
|
(set-storage storage)
|
||||||
|
(set-handler handler)
|
||||||
|
(go
|
||||||
|
(let [connection (create-connection ethereum-rpc-url)
|
||||||
|
identity (or identity
|
||||||
|
(<! (create-identity connection)))]
|
||||||
|
(set-connection connection)
|
||||||
|
(set-identity identity)
|
||||||
|
(listen connection handle-incoming-whisper-msg)
|
||||||
|
(start-delivery-loop)
|
||||||
|
(invoke-user-handler :initialized {:identity identity}))))
|
||||||
|
|
||||||
|
(defn send-user-msg [{:keys [to content]}]
|
||||||
|
(let [{:keys [msg-id msg] :as new-msg} (make-msg {:from (state/my-identity)
|
||||||
|
:to to
|
||||||
|
:payload {:content content
|
||||||
|
:content-type default-content-type
|
||||||
|
:type :user-msg}})]
|
||||||
|
(add-pending-message msg-id msg)
|
||||||
|
(post-msg (connection) msg)
|
||||||
|
new-msg))
|
||||||
|
|
||||||
|
(defn send-group-msg [{:keys [group-id content]}]
|
||||||
|
(let [store (storage)
|
||||||
|
{public-key :public} (get-keypair store group-id)
|
||||||
|
{:keys [msg-id msg] :as new-msg} (make-msg {:from (state/my-identity)
|
||||||
|
:topics [group-id]
|
||||||
|
:encrypt true
|
||||||
|
:public-key public-key
|
||||||
|
:payload {:content content
|
||||||
|
:content-type default-content-type
|
||||||
|
:type :user-msg}})]
|
||||||
|
(add-pending-message msg-id msg {:identities (get-identities store group-id)})
|
||||||
|
(post-msg (connection) msg)
|
||||||
|
new-msg))
|
||||||
|
|
||||||
|
(defn start-group-chat [identities]
|
||||||
|
(let [group-topic (random/id)
|
||||||
|
keypair (new-keypair)]
|
||||||
|
(let [store (storage)]
|
||||||
|
(save-keypair store group-topic keypair)
|
||||||
|
(save-identities store group-topic identities))
|
||||||
|
(doseq [ident identities]
|
||||||
|
(let [{:keys [msg-id msg]} (make-msg {:from (state/my-identity)
|
||||||
|
:to ident
|
||||||
|
:payload {:type :init-group-chat
|
||||||
|
:group-topic group-topic
|
||||||
|
:identities identities
|
||||||
|
:keypair keypair}})]
|
||||||
|
(add-pending-message msg-id msg {:internal? true})
|
||||||
|
(post-msg (connection) msg)))
|
||||||
|
group-topic))
|
||||||
|
|
||||||
|
(defn current-connection []
|
||||||
|
(connection))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
[syng-im.utils.logging :as log]
|
[syng-im.utils.logging :as log]
|
||||||
[syng-im.protocol.state.delivery :as state]
|
[syng-im.protocol.state.delivery :as state]
|
||||||
[syng-im.protocol.state.state :as s]
|
[syng-im.protocol.state.state :as s]
|
||||||
[syng-im.protocol.whisper :as whisper]
|
[syng-im.protocol.web3 :as whisper]
|
||||||
[syng-im.protocol.handler :as handler])
|
[syng-im.protocol.user-handler :refer [invoke-user-handler]])
|
||||||
(:require-macros [cljs.core.async.macros :refer [go]]))
|
(:require-macros [cljs.core.async.macros :refer [go]]))
|
||||||
|
|
||||||
(def max-retry-send-count 5)
|
(def max-retry-send-count 5)
|
||||||
|
@ -39,8 +39,10 @@
|
||||||
(state/inc-retry-count msg-id))
|
(state/inc-retry-count msg-id))
|
||||||
(do
|
(do
|
||||||
(log/info "Delivery-loop: Retry-count for message" msg-id "reached maximum")
|
(log/info "Delivery-loop: Retry-count for message" msg-id "reached maximum")
|
||||||
(state/remove-pending-message msg-id)
|
(let [internal? (state/internal? msg-id)]
|
||||||
(handler/invoke-handler :delivery-failed {:msg-id msg-id})))))
|
(state/remove-pending-message msg-id)
|
||||||
|
(when-not internal?
|
||||||
|
(invoke-user-handler :delivery-failed {:msg-id msg-id})))))))
|
||||||
(recur (<! (timeout check-delivery-interval-msg))))))
|
(recur (<! (timeout check-delivery-interval-msg))))))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
(ns syng-im.protocol.group-chat
|
||||||
|
(:require [syng-im.utils.random :as random]
|
||||||
|
[syng-im.utils.encryption :refer [new-keypair]]
|
||||||
|
[syng-im.protocol.state.group-chat :refer [save-keypair]]
|
||||||
|
[syng-im.protocol.state.state :refer [connection my-identity storage]]
|
||||||
|
[syng-im.protocol.web3 :refer [make-msg]]
|
||||||
|
[syng-im.protocol.state.delivery :refer [add-pending-message]]))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
(ns syng-im.protocol.handler
|
||||||
|
(:require [cljs.reader :refer [read-string]]
|
||||||
|
[syng-im.utils.logging :as log]
|
||||||
|
[syng-im.protocol.state.state :as state :refer [storage]]
|
||||||
|
[syng-im.protocol.state.delivery :refer [internal?
|
||||||
|
update-pending-message]]
|
||||||
|
[syng-im.protocol.state.group-chat :refer [save-keypair
|
||||||
|
save-identities
|
||||||
|
chat-exists?]]
|
||||||
|
[syng-im.protocol.web3 :refer [to-ascii
|
||||||
|
make-msg
|
||||||
|
post-msg
|
||||||
|
]]
|
||||||
|
[syng-im.protocol.user-handler :refer [invoke-user-handler]]))
|
||||||
|
|
||||||
|
(defn handle-ack [from {:keys [ack-msg-id] :as payload}]
|
||||||
|
(log/info "Got ack for message:" ack-msg-id "from:" from)
|
||||||
|
(let [internal-message? (internal? ack-msg-id)]
|
||||||
|
(update-pending-message ack-msg-id from)
|
||||||
|
(when-not internal-message?
|
||||||
|
(invoke-user-handler :msg-acked {:msg-id ack-msg-id
|
||||||
|
:from from}))
|
||||||
|
(when-let [group-topic (payload :group-topic)]
|
||||||
|
(invoke-user-handler :group-chat-invite-acked {:from from
|
||||||
|
:group-id group-topic}))))
|
||||||
|
|
||||||
|
(defn send-ack
|
||||||
|
([web3 to msg-id]
|
||||||
|
(send-ack web3 to msg-id nil))
|
||||||
|
([web3 to msg-id ack-info]
|
||||||
|
(log/info "Acking message:" msg-id "To:" to)
|
||||||
|
(let [{:keys [msg]} (make-msg {:from (state/my-identity)
|
||||||
|
:to to
|
||||||
|
:payload (merge {:type :ack
|
||||||
|
:ack-msg-id msg-id}
|
||||||
|
ack-info)})]
|
||||||
|
(post-msg web3 msg))))
|
||||||
|
|
||||||
|
(defn handle-user-msg [web3 from {:keys [msg-id] :as payload}]
|
||||||
|
(send-ack web3 from msg-id)
|
||||||
|
(invoke-user-handler :new-msg {:from from
|
||||||
|
:payload payload}))
|
||||||
|
|
||||||
|
(defn handle-new-group-chat [web3 from {:keys [group-topic keypair identities msg-id]}]
|
||||||
|
(send-ack web3 from msg-id {:group-topic group-topic})
|
||||||
|
(let [store (storage)]
|
||||||
|
(when-not (chat-exists? store group-topic)
|
||||||
|
(save-keypair store group-topic keypair)
|
||||||
|
(save-identities store group-topic identities)
|
||||||
|
(invoke-user-handler :new-group-chat {:from from
|
||||||
|
:identities identities
|
||||||
|
:group-id group-topic}))))
|
||||||
|
|
||||||
|
(defn handle-incoming-whisper-msg [web3 msg]
|
||||||
|
(log/info "Got whisper message:" msg)
|
||||||
|
(let [{from :from
|
||||||
|
to :to
|
||||||
|
topics :topics ;; always empty (bug in go-ethereum?)
|
||||||
|
payload :payload
|
||||||
|
:as msg} (js->clj msg :keywordize-keys true)]
|
||||||
|
(if (= to (state/my-identity))
|
||||||
|
(let [{msg-type :type
|
||||||
|
msg-id :msg-id
|
||||||
|
:as payload} (->> (to-ascii payload)
|
||||||
|
(read-string))]
|
||||||
|
(case msg-type
|
||||||
|
:ack (handle-ack from payload)
|
||||||
|
:user-msg (handle-user-msg web3 from payload)
|
||||||
|
:init-group-chat (handle-new-group-chat web3 from payload)))
|
||||||
|
(log/warn "My identity:" (state/my-identity) "Message To:" to "Message is encrypted for someone else, ignoring"))))
|
|
@ -0,0 +1,59 @@
|
||||||
|
(ns syng-im.protocol.state.delivery
|
||||||
|
(:require [cljs-time.core :as t]
|
||||||
|
[syng-im.utils.logging :as log]
|
||||||
|
[syng-im.protocol.state.state :refer [state]])
|
||||||
|
(:require-macros [syng-im.utils.lang-macros :refer [condas->]]))
|
||||||
|
|
||||||
|
(defn inc-retry-count [msg-id]
|
||||||
|
(swap! state (fn [state]
|
||||||
|
(if (get-in state [:pending-messages msg-id])
|
||||||
|
(update-in state [:pending-messages msg-id :retry-count] inc)
|
||||||
|
state))))
|
||||||
|
|
||||||
|
(defn pending? [msg-id]
|
||||||
|
(get-in @state [:pending-messages msg-id]))
|
||||||
|
|
||||||
|
(defn push-msg-to-delivery-queue [state msg-id]
|
||||||
|
(update-in state [:delivery-queue] conj {:timestamp (t/now)
|
||||||
|
:msg-id msg-id}))
|
||||||
|
|
||||||
|
(defn add-pending-message
|
||||||
|
([msg-id msg {:keys [identities internal?] :as opts}]
|
||||||
|
(swap! state (fn [state]
|
||||||
|
(-> (assoc-in state [:pending-messages msg-id] {:msg msg
|
||||||
|
:retry-count 0
|
||||||
|
:identities (when identities
|
||||||
|
(set identities))
|
||||||
|
:internal? internal?})
|
||||||
|
(push-msg-to-delivery-queue msg-id)))))
|
||||||
|
([msg-id msg]
|
||||||
|
(add-pending-message msg-id msg nil)))
|
||||||
|
|
||||||
|
(defn pop-delivery-queue []
|
||||||
|
(swap! state update-in [:delivery-queue] pop))
|
||||||
|
|
||||||
|
(defn push-delivery-queue [msg-id]
|
||||||
|
(swap! state push-msg-to-delivery-queue msg-id))
|
||||||
|
|
||||||
|
(defn internal? [msg-id]
|
||||||
|
(get-in @state [:pending-messages msg-id :internal?]))
|
||||||
|
|
||||||
|
(defn update-pending-message [msg-id from]
|
||||||
|
(swap! state update-in [:pending-messages]
|
||||||
|
(fn [pending-msgs]
|
||||||
|
(condas-> pending-msgs msgs
|
||||||
|
(get-in msgs [msg-id :identities]) ;; test
|
||||||
|
(do
|
||||||
|
(log/info "Removing identity" from "from pending msg" msg-id)
|
||||||
|
(update-in msgs [msg-id :identities] disj from))
|
||||||
|
|
||||||
|
(empty? (get-in msgs [msg-id :identities])) ;; test
|
||||||
|
(do
|
||||||
|
(log/info "Removing message" msg-id "from pending")
|
||||||
|
(dissoc msgs msg-id))))))
|
||||||
|
|
||||||
|
(defn remove-pending-message [msg-id]
|
||||||
|
(swap! state update-in [:pending-messages] dissoc msg-id))
|
||||||
|
|
||||||
|
(defn delivery-queue []
|
||||||
|
(:delivery-queue @state))
|
|
@ -0,0 +1,28 @@
|
||||||
|
(ns syng-im.protocol.state.group-chat
|
||||||
|
(:require [syng-im.protocol.state.storage :as s]))
|
||||||
|
|
||||||
|
(defn topic-keypair-key [topic]
|
||||||
|
(str "group-chat.topic-keypair." topic))
|
||||||
|
|
||||||
|
(defn topic-identities-key [topic]
|
||||||
|
(str "group-chat.topic-identities." topic))
|
||||||
|
|
||||||
|
(defn save-keypair [storage topic keypair]
|
||||||
|
(let [key (topic-keypair-key topic)]
|
||||||
|
(s/put storage key keypair)))
|
||||||
|
|
||||||
|
(defn save-identities [storage topic identities]
|
||||||
|
(let [key (topic-identities-key topic)]
|
||||||
|
(s/put storage key identities)))
|
||||||
|
|
||||||
|
(defn get-identities [storage topic]
|
||||||
|
(let [key (topic-identities-key topic)]
|
||||||
|
(s/get storage key)))
|
||||||
|
|
||||||
|
(defn chat-exists? [storage topic]
|
||||||
|
(let [key (topic-keypair-key topic)]
|
||||||
|
(s/contains-key? storage key)))
|
||||||
|
|
||||||
|
(defn get-keypair [storage topic]
|
||||||
|
(let [key (topic-keypair-key topic)]
|
||||||
|
(s/get storage key)))
|
|
@ -4,15 +4,19 @@
|
||||||
(def state (atom {:pending-messages {}
|
(def state (atom {:pending-messages {}
|
||||||
:filters {}
|
:filters {}
|
||||||
:delivery-queue #queue []
|
:delivery-queue #queue []
|
||||||
:handler nil
|
:external-handler nil
|
||||||
:identity nil
|
:identity nil
|
||||||
:connection nil}))
|
:connection nil
|
||||||
|
:storage nil}))
|
||||||
|
|
||||||
(defn add-filter [topics filter]
|
(defn add-filter [topics filter]
|
||||||
(swap! state assoc-in [:filters topics] filter))
|
(swap! state assoc-in [:filters topics] filter))
|
||||||
|
|
||||||
|
(defn set-storage [storage]
|
||||||
|
(swap! state assoc :storage storage))
|
||||||
|
|
||||||
(defn set-handler [handler]
|
(defn set-handler [handler]
|
||||||
(swap! state assoc :handler handler))
|
(swap! state assoc :external-handler handler))
|
||||||
|
|
||||||
(defn set-identity [identity]
|
(defn set-identity [identity]
|
||||||
(swap! state assoc :identity identity))
|
(swap! state assoc :identity identity))
|
||||||
|
@ -26,5 +30,8 @@
|
||||||
(defn my-identity []
|
(defn my-identity []
|
||||||
(:identity @state))
|
(:identity @state))
|
||||||
|
|
||||||
(defn handler []
|
(defn external-handler []
|
||||||
(:handler @state))
|
(:external-handler @state))
|
||||||
|
|
||||||
|
(defn storage []
|
||||||
|
(:storage @state))
|
|
@ -0,0 +1,7 @@
|
||||||
|
(ns syng-im.protocol.state.storage
|
||||||
|
(:refer-clojure :exclude [get]))
|
||||||
|
|
||||||
|
(defprotocol Storage
|
||||||
|
(put [this key value])
|
||||||
|
(get [this key])
|
||||||
|
(contains-key? [this key]))
|
|
@ -0,0 +1,5 @@
|
||||||
|
(ns syng-im.protocol.user-handler
|
||||||
|
(:require [syng-im.protocol.state.state :as state]))
|
||||||
|
|
||||||
|
(defn invoke-user-handler [event-type params]
|
||||||
|
((state/external-handler) (assoc params :event-type event-type)))
|
|
@ -0,0 +1,94 @@
|
||||||
|
(ns syng-im.protocol.web3
|
||||||
|
(:require [cljs.core.async :refer [chan put! close! <!]]
|
||||||
|
[cljsjs.web3]
|
||||||
|
[syng-im.utils.logging :as log]
|
||||||
|
[syng-im.utils.random :as random]
|
||||||
|
[syng-im.utils.encryption :refer [encrypt]]
|
||||||
|
[syng-im.protocol.state.state :as state]
|
||||||
|
[syng-im.protocol.user-handler :refer [invoke-user-handler]])
|
||||||
|
(:require-macros [cljs.core.async.macros :refer [go]]))
|
||||||
|
|
||||||
|
(def syng-app-topic "SYNG-APP-CHAT-TOPIC")
|
||||||
|
(def syng-msg-ttl 100)
|
||||||
|
|
||||||
|
(defn from-ascii [s]
|
||||||
|
(.fromAscii js/Web3.prototype s))
|
||||||
|
|
||||||
|
(defn to-ascii [s]
|
||||||
|
(.toAscii js/Web3.prototype s))
|
||||||
|
|
||||||
|
(defn whisper [web3]
|
||||||
|
(.-shh web3))
|
||||||
|
|
||||||
|
(defn make-topics [topics]
|
||||||
|
(->> {:topics (mapv from-ascii topics)}
|
||||||
|
(clj->js)))
|
||||||
|
|
||||||
|
(defn make-web3 [rpc-url]
|
||||||
|
(->> (js/Web3.providers.HttpProvider. rpc-url)
|
||||||
|
(js/Web3.)))
|
||||||
|
|
||||||
|
(defn make-callback [{:keys [error-msg result-channel]}]
|
||||||
|
(fn [error result]
|
||||||
|
(if error
|
||||||
|
(do
|
||||||
|
(log/error (str error-msg ":") error)
|
||||||
|
(invoke-user-handler :error {:error-msg error-msg
|
||||||
|
:details error}))
|
||||||
|
(put! result-channel result))
|
||||||
|
(close! result-channel)))
|
||||||
|
|
||||||
|
(defn new-identity [web3]
|
||||||
|
(let [result-channel (chan)
|
||||||
|
callback (make-callback {:error-msg "Call to newIdentity failed"
|
||||||
|
:result-channel result-channel})]
|
||||||
|
(.newIdentity (.-shh web3) callback)
|
||||||
|
result-channel))
|
||||||
|
|
||||||
|
(defn post-msg [web3 msg]
|
||||||
|
(let [js-msg (clj->js msg)]
|
||||||
|
(log/info "Sending whisper message:" js-msg)
|
||||||
|
(-> (whisper web3)
|
||||||
|
(.post js-msg (fn [error result]
|
||||||
|
(when error
|
||||||
|
(let [error-msg "Call to shh.post() failed"]
|
||||||
|
(log/error (str error-msg ":") error)
|
||||||
|
(invoke-user-handler :error {:error-msg error-msg
|
||||||
|
:details error}))))))))
|
||||||
|
|
||||||
|
(defn make-msg
|
||||||
|
"Returns [msg-id msg], `msg` is formed for Web3.shh.post()"
|
||||||
|
[{:keys [from to ttl topics payload encrypt? public-key]
|
||||||
|
:or {ttl syng-msg-ttl
|
||||||
|
topics []}}]
|
||||||
|
(let [msg-id (random/id)]
|
||||||
|
{:msg-id msg-id
|
||||||
|
:msg (cond-> {:ttl ttl
|
||||||
|
:topics (->> (conj topics syng-app-topic)
|
||||||
|
(mapv from-ascii))
|
||||||
|
:payload (cond->> (merge payload {:msg-id msg-id})
|
||||||
|
true (str)
|
||||||
|
encrypt? (encrypt public-key)
|
||||||
|
true (from-ascii))}
|
||||||
|
from (assoc :from from)
|
||||||
|
to (assoc :to to))}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn listen
|
||||||
|
"Returns a filter which can be stopped with (stop-whisper-listener)"
|
||||||
|
[web3 msg-handler]
|
||||||
|
(let [topics [syng-app-topic]
|
||||||
|
shh (whisper web3)
|
||||||
|
filter (.filter shh (make-topics topics) (fn [error msg]
|
||||||
|
(if error
|
||||||
|
(invoke-user-handler :error {:error-msg error})
|
||||||
|
(msg-handler web3 msg))))]
|
||||||
|
(state/add-filter topics filter)))
|
||||||
|
|
||||||
|
(defn stop-listener [filter]
|
||||||
|
(.stopWatching filter))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
(ns syng-im.utils.encryption
|
||||||
|
(:require [cljsjs.chance]))
|
||||||
|
|
||||||
|
(defn new-keypair
|
||||||
|
"Returns {:private \"private key\" :public \"public key\""
|
||||||
|
[]
|
||||||
|
(let [random-fake (.guid js/chance)]
|
||||||
|
{:private random-fake
|
||||||
|
:public random-fake}))
|
||||||
|
|
||||||
|
(defn encrypt [public-key content]
|
||||||
|
content)
|
||||||
|
|
||||||
|
(defn decrypt [private-key content]
|
||||||
|
content)
|
|
@ -0,0 +1,10 @@
|
||||||
|
(ns syng-im.utils.lang-macros)
|
||||||
|
|
||||||
|
(defmacro condas->
|
||||||
|
"A mixture of cond-> and as-> allowing more flexibility in the test and step forms"
|
||||||
|
[expr name & clauses]
|
||||||
|
(assert (even? (count clauses)))
|
||||||
|
(let [pstep (fn [[test step]] `(if ~test ~step ~name))]
|
||||||
|
`(let [~name ~expr
|
||||||
|
~@(interleave (repeat name) (map pstep (partition 2 clauses)))]
|
||||||
|
~name)))
|
|
@ -0,0 +1,14 @@
|
||||||
|
(ns syng-im.utils.random
|
||||||
|
(:require [cljsjs.chance]
|
||||||
|
[cljs-time.core :as t]
|
||||||
|
[cljs-time.format :as tf]))
|
||||||
|
|
||||||
|
(defn timestamp []
|
||||||
|
(.getTime (js/Date.)))
|
||||||
|
|
||||||
|
(defn id []
|
||||||
|
(str (timestamp) "-" (.guid js/chance)))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(id)
|
||||||
|
)
|
|
@ -1,60 +0,0 @@
|
||||||
(ns syng-im.protocol.api
|
|
||||||
(:require [cljs.core.async :refer [<! timeout]]
|
|
||||||
[syng-im.protocol.state.state :as state]
|
|
||||||
[syng-im.protocol.delivery :as delivery]
|
|
||||||
[syng-im.protocol.state.delivery :as delivery-state]
|
|
||||||
[syng-im.protocol.whisper :as whisper]
|
|
||||||
[syng-im.protocol.handler :as h])
|
|
||||||
(:require-macros [cljs.core.async.macros :refer [go]]))
|
|
||||||
|
|
||||||
(def default-content-type "text/plain")
|
|
||||||
|
|
||||||
(defn create-connection [ethereum-rpc-url]
|
|
||||||
(whisper/make-web3 ethereum-rpc-url))
|
|
||||||
|
|
||||||
(defn create-identity [connection]
|
|
||||||
(whisper/new-identity connection))
|
|
||||||
|
|
||||||
(defn init-protocol
|
|
||||||
"Required [handler ethereum-rpc-url]
|
|
||||||
Optional [whisper-identity] - if not passed a new identity is created automatically
|
|
||||||
|
|
||||||
(fn handler [{:keys [event-type...}])
|
|
||||||
|
|
||||||
:event-type can be:
|
|
||||||
|
|
||||||
:new-msg - [from payload]
|
|
||||||
:error - [error-msg details]
|
|
||||||
:msg-acked [msg-id]
|
|
||||||
:delivery-failed [msg-id]
|
|
||||||
:initialized [identity]
|
|
||||||
|
|
||||||
:new-msg, msg-acked should be handled idempotently (may be called multiple times for the same msg-id)
|
|
||||||
"
|
|
||||||
[{:keys [handler ethereum-rpc-url identity]}]
|
|
||||||
(state/set-handler handler)
|
|
||||||
(go
|
|
||||||
(let [connection (create-connection ethereum-rpc-url)
|
|
||||||
identity (or identity
|
|
||||||
(<! (create-identity connection)))]
|
|
||||||
(state/set-connection connection)
|
|
||||||
(state/set-identity identity)
|
|
||||||
(whisper/listen connection)
|
|
||||||
(delivery/start-delivery-loop)
|
|
||||||
(h/invoke-handler :initialized {:identity identity}))))
|
|
||||||
|
|
||||||
(defn send-user-msg [{:keys [to content]}]
|
|
||||||
(let [{:keys [msg-id msg] :as new-msg} (whisper/make-msg {:from (state/my-identity)
|
|
||||||
:to to
|
|
||||||
:payload {:content content
|
|
||||||
:content-type default-content-type
|
|
||||||
:type :user-msg}})]
|
|
||||||
(delivery-state/add-pending-message msg-id msg)
|
|
||||||
(whisper/post-msg (state/connection) msg)
|
|
||||||
new-msg))
|
|
||||||
|
|
||||||
(defn my-identity []
|
|
||||||
(state/my-identity))
|
|
||||||
|
|
||||||
(defn current-connection []
|
|
||||||
(state/connection))
|
|
|
@ -1,5 +0,0 @@
|
||||||
(ns syng-im.protocol.handler
|
|
||||||
(:require [syng-im.protocol.state.state :as state]))
|
|
||||||
|
|
||||||
(defn invoke-handler [event-type params]
|
|
||||||
((state/handler) (assoc params :event-type event-type)))
|
|
|
@ -1,36 +0,0 @@
|
||||||
(ns syng-im.protocol.state.delivery
|
|
||||||
(:require [cljs-time.core :as t]
|
|
||||||
[syng-im.protocol.state.state :refer [state]]
|
|
||||||
[syng-im.utils.logging :as log]))
|
|
||||||
|
|
||||||
(defn inc-retry-count [msg-id]
|
|
||||||
(swap! state (fn [state]
|
|
||||||
(if (get-in state [:pending-messages msg-id])
|
|
||||||
(update-in state [:pending-messages msg-id :retry-count] inc)
|
|
||||||
state))))
|
|
||||||
|
|
||||||
(defn pending? [msg-id]
|
|
||||||
(get-in @state [:pending-messages msg-id]))
|
|
||||||
|
|
||||||
(defn push-msg-to-delivery-queue [state msg-id]
|
|
||||||
(update-in state [:delivery-queue] conj {:timestamp (t/now)
|
|
||||||
:msg-id msg-id}))
|
|
||||||
|
|
||||||
(defn add-pending-message [msg-id msg]
|
|
||||||
(swap! state (fn [state]
|
|
||||||
(-> (assoc-in state [:pending-messages msg-id] {:msg msg
|
|
||||||
:retry-count 0})
|
|
||||||
(push-msg-to-delivery-queue msg-id)))))
|
|
||||||
|
|
||||||
(defn pop-delivery-queue []
|
|
||||||
(swap! state update-in [:delivery-queue] pop))
|
|
||||||
|
|
||||||
(defn push-delivery-queue [msg-id]
|
|
||||||
(swap! state push-msg-to-delivery-queue msg-id))
|
|
||||||
|
|
||||||
(defn remove-pending-message [msg-id]
|
|
||||||
(log/info "Removing message" msg-id "from pending")
|
|
||||||
(swap! state update-in [:pending-messages] dissoc msg-id))
|
|
||||||
|
|
||||||
(defn delivery-queue []
|
|
||||||
(:delivery-queue @state))
|
|
|
@ -1,127 +0,0 @@
|
||||||
(ns syng-im.protocol.whisper
|
|
||||||
(:require [cljs.core.async :refer [chan put! close! <!]]
|
|
||||||
[cljsjs.web3]
|
|
||||||
[cljsjs.chance]
|
|
||||||
[syng-im.utils.logging :as log]
|
|
||||||
[syng-im.protocol.state.state :as state]
|
|
||||||
[syng-im.protocol.state.delivery :as delivery]
|
|
||||||
[syng-im.protocol.handler :as handler]
|
|
||||||
[cljs.reader :refer [read-string]])
|
|
||||||
(:require-macros [cljs.core.async.macros :refer [go]]))
|
|
||||||
|
|
||||||
(def syng-app-topic "SYNG-APP-CHAT-TOPIC")
|
|
||||||
(def syng-msg-ttl 100)
|
|
||||||
|
|
||||||
(defn from-ascii [s]
|
|
||||||
(.fromAscii js/Web3.prototype s))
|
|
||||||
|
|
||||||
(defn to-ascii [s]
|
|
||||||
(.toAscii js/Web3.prototype s))
|
|
||||||
|
|
||||||
(defn whisper [web3]
|
|
||||||
(.-shh web3))
|
|
||||||
|
|
||||||
(defn make-topics [topics]
|
|
||||||
(->> {:topics (mapv from-ascii topics)}
|
|
||||||
(clj->js)))
|
|
||||||
|
|
||||||
(defn make-web3 [rpc-url]
|
|
||||||
(->> (js/Web3.providers.HttpProvider. rpc-url)
|
|
||||||
(js/Web3.)))
|
|
||||||
|
|
||||||
(defn make-callback [{:keys [error-msg result-channel]}]
|
|
||||||
(fn [error result]
|
|
||||||
(if error
|
|
||||||
(do
|
|
||||||
(log/error (str error-msg ":") error)
|
|
||||||
(handler/invoke-handler :error {:error-msg error-msg
|
|
||||||
:details error}))
|
|
||||||
(put! result-channel result))
|
|
||||||
(close! result-channel)))
|
|
||||||
|
|
||||||
(defn new-identity [web3]
|
|
||||||
(let [result-channel (chan)
|
|
||||||
callback (make-callback {:error-msg "Call to newIdentity failed"
|
|
||||||
:result-channel result-channel})]
|
|
||||||
(.newIdentity (.-shh web3) callback)
|
|
||||||
result-channel))
|
|
||||||
|
|
||||||
(defn handle-ack [{:keys [ack-msg-id]}]
|
|
||||||
(log/info "Got ack for message:" ack-msg-id)
|
|
||||||
(delivery/remove-pending-message ack-msg-id)
|
|
||||||
(handler/invoke-handler :msg-acked {:msg-id ack-msg-id}))
|
|
||||||
|
|
||||||
(defn post-msg [web3 msg]
|
|
||||||
(let [js-msg (clj->js msg)]
|
|
||||||
(log/info "Sending whisper message:" js-msg)
|
|
||||||
(-> (whisper web3)
|
|
||||||
(.post js-msg (fn [error result]
|
|
||||||
(when error
|
|
||||||
(let [error-msg "Call to shh.post() failed"]
|
|
||||||
(log/error (str error-msg ":") error)
|
|
||||||
(handler/invoke-handler :error {:error-msg error-msg
|
|
||||||
:details error}))))))))
|
|
||||||
|
|
||||||
(defn make-msg
|
|
||||||
"Returns [msg-id msg], `msg` is formed for Web3.shh.post()"
|
|
||||||
[{:keys [from to ttl topics payload]
|
|
||||||
:or {ttl syng-msg-ttl
|
|
||||||
topics []}}]
|
|
||||||
(let [msg-id (.guid js/chance)]
|
|
||||||
{:msg-id msg-id
|
|
||||||
:msg (cond-> {:ttl ttl
|
|
||||||
:topics (->> (conj topics syng-app-topic)
|
|
||||||
(mapv from-ascii))
|
|
||||||
:payload (->> (merge payload {:msg-id msg-id})
|
|
||||||
(str)
|
|
||||||
(from-ascii))}
|
|
||||||
from (assoc :from from)
|
|
||||||
to (assoc :to to))}))
|
|
||||||
|
|
||||||
(defn send-ack [web3 to msg-id]
|
|
||||||
(log/info "Acking message:" msg-id "To:" to)
|
|
||||||
(let [{:keys [msg]} (make-msg {:from (state/my-identity)
|
|
||||||
:to to
|
|
||||||
:payload {:type :ack
|
|
||||||
:ack-msg-id msg-id}})]
|
|
||||||
(post-msg web3 msg)))
|
|
||||||
|
|
||||||
(defn handle-user-msg [web3 from {:keys [msg-id] :as payload}]
|
|
||||||
(send-ack web3 from msg-id)
|
|
||||||
(handler/invoke-handler :new-msg {:from from
|
|
||||||
:payload payload}))
|
|
||||||
|
|
||||||
(defn handle-arriving-whisper-msg [web3 msg]
|
|
||||||
(log/info "Got whisper message:" msg)
|
|
||||||
(let [{from :from
|
|
||||||
to :to
|
|
||||||
topics :topics ;; always empty (bug in go-ethereum?)
|
|
||||||
payload :payload
|
|
||||||
:as msg} (js->clj msg :keywordize-keys true)]
|
|
||||||
(if (= to (state/my-identity))
|
|
||||||
(let [{msg-type :type
|
|
||||||
msg-id :msg-id
|
|
||||||
:as payload} (->> (to-ascii payload)
|
|
||||||
(read-string))]
|
|
||||||
(case msg-type
|
|
||||||
:ack (handle-ack payload)
|
|
||||||
:user-msg (handle-user-msg web3 from payload)))
|
|
||||||
(log/warn "My identity:" (state/my-identity) "Message To:" to "Message is encrypted for someone else, ignoring"))))
|
|
||||||
|
|
||||||
(defn listen
|
|
||||||
"Returns a filter which can be stopped with (stop-whisper-listener)"
|
|
||||||
[web3]
|
|
||||||
(let [topics [syng-app-topic]
|
|
||||||
shh (whisper web3)
|
|
||||||
filter (.filter shh (make-topics topics) (fn [error msg]
|
|
||||||
(if error
|
|
||||||
(handler/invoke-handler :error {:error-msg error})
|
|
||||||
(handle-arriving-whisper-msg web3 msg))))]
|
|
||||||
(state/add-filter topics filter)))
|
|
||||||
|
|
||||||
(defn stop-listener [filter]
|
|
||||||
(.stopWatching filter))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,15 @@
|
||||||
:min-lein-version "2.5.3"
|
:min-lein-version "2.5.3"
|
||||||
|
|
||||||
:dependencies [[org.clojure/clojure "1.7.0"]
|
:dependencies [[org.clojure/clojure "1.7.0"]
|
||||||
[org.clojure/clojurescript "1.7.170"]
|
[org.clojure/clojurescript "1.7.228"]
|
||||||
[org.clojure/core.async "0.2.374"
|
[org.clojure/core.async "0.2.374"
|
||||||
:exclusions [org.clojure/tools.reader]]
|
:exclusions [org.clojure/tools.reader]]
|
||||||
[com.cemerick/piggieback "0.2.1"]
|
[com.cemerick/piggieback "0.2.1"]
|
||||||
;; cljs deps
|
|
||||||
[cljsjs/chance "0.7.3-0"]
|
[cljsjs/chance "0.7.3-0"]
|
||||||
[cljsjs/web3 "0.15.3-0"]
|
[cljsjs/web3 "0.15.3-0"]
|
||||||
[com.andrewmcveigh/cljs-time "0.4.0"]
|
[com.andrewmcveigh/cljs-time "0.4.0"]
|
||||||
[syng-im/protocol "0.1.0"]]
|
;[syng-im/protocol "0.1.0"]
|
||||||
|
]
|
||||||
|
|
||||||
:plugins [[lein-figwheel "0.5.0-6"]
|
:plugins [[lein-figwheel "0.5.0-6"]
|
||||||
[lein-cljsbuild "1.1.2" :exclusions [[org.clojure/clojure]]]]
|
[lein-cljsbuild "1.1.2" :exclusions [[org.clojure/clojure]]]]
|
||||||
|
@ -26,7 +26,8 @@
|
||||||
|
|
||||||
:cljsbuild {:builds
|
:cljsbuild {:builds
|
||||||
[{:id "dev"
|
[{:id "dev"
|
||||||
:source-paths ["src/cljs"]
|
|
||||||
|
:source-paths ["src/cljs" "protocol/src/cljs"]
|
||||||
|
|
||||||
;; If no code is to be run, set :figwheel true for continued automagical reloading
|
;; If no code is to be run, set :figwheel true for continued automagical reloading
|
||||||
:figwheel {:on-jsload "syng-im.core/on-js-reload"}
|
:figwheel {:on-jsload "syng-im.core/on-js-reload"}
|
||||||
|
@ -40,7 +41,9 @@
|
||||||
;; production. You can build this with:
|
;; production. You can build this with:
|
||||||
;; lein cljsbuild once min
|
;; lein cljsbuild once min
|
||||||
{:id "prod"
|
{:id "prod"
|
||||||
|
|
||||||
:source-paths ["src/cljs"]
|
:source-paths ["src/cljs"]
|
||||||
|
|
||||||
:compiler {:output-to "resources/public/js/compiled/app.js"
|
:compiler {:output-to "resources/public/js/compiled/app.js"
|
||||||
:main syng-im.core
|
:main syng-im.core
|
||||||
:optimizations :advanced
|
:optimizations :advanced
|
||||||
|
|
|
@ -1,48 +1,75 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="css/style.css" rel="stylesheet" type="text/css">
|
<link href="css/style.css" rel="stylesheet" type="text/css">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
input[type=text] {
|
input[type=text] {
|
||||||
width: 530px;
|
width: 530px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
label {
|
|
||||||
width: 80px;
|
label {
|
||||||
display: inline-block;
|
width: 80px;
|
||||||
}
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#group-chat-container {
|
||||||
|
border-top: 20px solid lightblue;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!--<div id="app">-->
|
<!--<div id="app">-->
|
||||||
<!--<h2>Figwheel template</h2>-->
|
<!--<h2>Figwheel template</h2>-->
|
||||||
<!--<p>Checkout your developer console.</p>-->
|
<!--<p>Checkout your developer console.</p>-->
|
||||||
<!--</div>-->
|
<!--</div>-->
|
||||||
|
<div>
|
||||||
|
<label for="rpc-url">RPC URL</label>
|
||||||
|
<input id="rpc-url" type="text" value="http://localhost:4546"/>
|
||||||
|
<button id="connect-button">Connect</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<label for="rpc-url">RPC URL</label>
|
|
||||||
<input id="rpc-url" type="text" value="http://localhost:4546" />
|
|
||||||
<button id="connect-button">Connect</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<label for="chat">Chat:</label>
|
<label for="chat">Chat:</label>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="chat" rows="20" cols="150"></textarea>
|
<textarea id="chat" rows="20" cols="150"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="msg">Message:</label>
|
||||||
|
<input id="msg" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="my-identity">My Identity</label>
|
||||||
|
<input id="my-identity" readonly=readonly type="text"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="to-identity">To Identity</label>
|
||||||
|
<input id="to-identity" type="text"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="group-chat-container">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label for="group-chat">Group Chat:</label>
|
||||||
|
</div>
|
||||||
|
<textarea id="group-chat" rows="20" cols="150"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="msg">Message:</label>
|
<label for="group-msg">Message:</label>
|
||||||
<input id="msg" type="text" />
|
<input id="group-msg" type="text"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="my-identity">My Identity</label>
|
<div>
|
||||||
<input id="my-identity" readonly=readonly type="text" />
|
<label for="to-identities">To Identities:</label>
|
||||||
|
</div>
|
||||||
|
<textarea id="to-identities" rows="5" cols="150"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="to-identity">To Identity</label>
|
<button id="start-group-chat-button">Start Group Chat</button>
|
||||||
<input id="to-identity" type="text" />
|
|
||||||
</div>
|
</div>
|
||||||
<script src="js/compiled/syng_im.js" type="text/javascript"></script>
|
</div>
|
||||||
</body>
|
<script src="js/compiled/syng_im.js" type="text/javascript"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="css/style.css" rel="stylesheet" type="text/css">
|
||||||
|
<style type="text/css">
|
||||||
|
input[type=text] {
|
||||||
|
width: 530px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
width: 80px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--<div id="app">-->
|
||||||
|
<!--<h2>Figwheel template</h2>-->
|
||||||
|
<!--<p>Checkout your developer console.</p>-->
|
||||||
|
<!--</div>-->
|
||||||
|
<div>
|
||||||
|
<label for="rpc-url">RPC URL</label>
|
||||||
|
<input id="rpc-url" type="text" value="http://localhost:4546" />
|
||||||
|
<button id="connect-button">Connect</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label for="chat">Chat:</label>
|
||||||
|
</div>
|
||||||
|
<textarea id="chat" rows="20" cols="150"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="msg">Message:</label>
|
||||||
|
<input id="msg" type="text" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="my-identity">My Identity</label>
|
||||||
|
<input id="my-identity" readonly=readonly type="text" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="to-identity">To Identity</label>
|
||||||
|
<input id="to-identity" type="text" />
|
||||||
|
</div>
|
||||||
|
<!--<script src="js/compiled/syng_im.js" type="text/javascript"></script>-->
|
||||||
|
<script src="js/compiled/app.js" type="text/javascript"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,5 +1,6 @@
|
||||||
(ns syng-im.core
|
(ns syng-im.core
|
||||||
(:require [syng-im.protocol.api :as p]
|
(:require [clojure.string :as s]
|
||||||
|
[syng-im.protocol.api :as p]
|
||||||
[syng-im.utils.logging :as log]
|
[syng-im.utils.logging :as log]
|
||||||
[goog.dom :as g]
|
[goog.dom :as g]
|
||||||
[goog.dom.forms :as f]
|
[goog.dom.forms :as f]
|
||||||
|
@ -7,7 +8,8 @@
|
||||||
[goog.events.EventType]
|
[goog.events.EventType]
|
||||||
[goog.events.KeyCodes]
|
[goog.events.KeyCodes]
|
||||||
[goog.events.KeyHandler]
|
[goog.events.KeyHandler]
|
||||||
[goog.events.KeyHandler.EventType :as key-handler-events])
|
[goog.events.KeyHandler.EventType :as key-handler-events]
|
||||||
|
[syng-im.protocol.state.storage :as st])
|
||||||
(:import [goog.events EventType]
|
(:import [goog.events EventType]
|
||||||
[goog.events KeyCodes]))
|
[goog.events KeyCodes]))
|
||||||
|
|
||||||
|
@ -19,8 +21,20 @@
|
||||||
;; (swap! app-state update-in [:__figwheel_counter] inc)
|
;; (swap! app-state update-in [:__figwheel_counter] inc)
|
||||||
)
|
)
|
||||||
|
|
||||||
(defn add-to-chat [from content]
|
(defrecord MapStore [m]
|
||||||
(let [chat-area (g/getElement "chat")
|
st/Storage
|
||||||
|
(put [{:keys [m]} key value]
|
||||||
|
(swap! m assoc key value))
|
||||||
|
(get [{:keys [m]} key]
|
||||||
|
(get @m key))
|
||||||
|
(contains-key? [{:keys [m]} key]
|
||||||
|
(contains? @m key)))
|
||||||
|
|
||||||
|
(defonce state (atom {:group-id nil
|
||||||
|
:storage (map->MapStore {:m (atom {})})}))
|
||||||
|
|
||||||
|
(defn add-to-chat [element-id from content]
|
||||||
|
(let [chat-area (g/getElement element-id)
|
||||||
chat (f/getValue chat-area)
|
chat (f/getValue chat-area)
|
||||||
chat (str chat (subs from 0 6) ": " content "\n")]
|
chat (str chat (subs from 0 6) ": " content "\n")]
|
||||||
(f/setValue chat-area chat)))
|
(f/setValue chat-area chat)))
|
||||||
|
@ -30,21 +44,26 @@
|
||||||
(f/getValue))]
|
(f/getValue))]
|
||||||
(p/init-protocol
|
(p/init-protocol
|
||||||
{:ethereum-rpc-url rpc-url
|
{:ethereum-rpc-url rpc-url
|
||||||
|
:storage (:storage @state)
|
||||||
:handler (fn [{:keys [event-type] :as event}]
|
:handler (fn [{:keys [event-type] :as event}]
|
||||||
(log/info "Event:" (clj->js event))
|
(log/info "Event:" (clj->js event))
|
||||||
(case event-type
|
(case event-type
|
||||||
:new-msg (let [{:keys [from payload]} event
|
:new-msg (let [{:keys [from payload]} event
|
||||||
{content :content} payload]
|
{content :content} payload]
|
||||||
(add-to-chat from content))
|
(add-to-chat "chat" from content))
|
||||||
:msg-acked (let [{:keys [msg-id]} event]
|
:msg-acked (let [{:keys [msg-id]} event]
|
||||||
(add-to-chat ":" (str "Message " msg-id " was acked")))
|
(add-to-chat "chat" ":" (str "Message " msg-id " was acked")))
|
||||||
:initialized (let [{:keys [identity]} event]
|
:initialized (let [{:keys [identity]} event]
|
||||||
(add-to-chat ":" (str "Initialized, identity is " identity))
|
(add-to-chat "chat" ":" (str "Initialized, identity is " identity))
|
||||||
(-> (g/getElement "my-identity")
|
(-> (g/getElement "my-identity")
|
||||||
(f/setValue identity)))
|
(f/setValue identity)))
|
||||||
:delivery-failed (let [{:keys [msg-id]} event]
|
:delivery-failed (let [{:keys [msg-id]} event]
|
||||||
(add-to-chat ":" (str "Delivery of message " msg-id " failed")))
|
(add-to-chat "chat" ":" (str "Delivery of message " msg-id " failed")))
|
||||||
(add-to-chat ":" (str "Don't know how to handle " event-type))))})
|
:new-group-chat (let [{:keys [from group-id]} event]
|
||||||
|
(add-to-chat "group-chat" ":" (str "Received group chat invitation from " from " for group-id: " group-id)))
|
||||||
|
:group-chat-invite-acked (let [{:keys [from group-id]} event]
|
||||||
|
(add-to-chat "group-chat" ":" (str "Received ACK for group chat invitation from " from " for group-id: " group-id)))
|
||||||
|
(add-to-chat "chat" ":" (str "Don't know how to handle " event-type))))})
|
||||||
(e/listen (-> (g/getElement "msg")
|
(e/listen (-> (g/getElement "msg")
|
||||||
(goog.events.KeyHandler.))
|
(goog.events.KeyHandler.))
|
||||||
key-handler-events/KEY
|
key-handler-events/KEY
|
||||||
|
@ -56,7 +75,15 @@
|
||||||
(f/getValue))]
|
(f/getValue))]
|
||||||
(p/send-user-msg {:to to-identity
|
(p/send-user-msg {:to to-identity
|
||||||
:content msg})
|
:content msg})
|
||||||
(add-to-chat (p/my-identity) msg)))))))
|
(add-to-chat "chat" (p/my-identity) msg)))))))
|
||||||
|
|
||||||
|
(defn start-group-chat []
|
||||||
|
(let [identities (-> (g/getElement "to-identities")
|
||||||
|
(f/getValue)
|
||||||
|
(s/split "\n"))]
|
||||||
|
(add-to-chat "group-chat" ":" (str "Starting group chat with " identities))
|
||||||
|
(let [group-id (p/start-group-chat identities)]
|
||||||
|
(swap! state assoc :group-id group-id))))
|
||||||
|
|
||||||
(let [button (g/getElement "connect-button")]
|
(let [button (g/getElement "connect-button")]
|
||||||
(e/listen button EventType/CLICK
|
(e/listen button EventType/CLICK
|
||||||
|
@ -64,6 +91,12 @@
|
||||||
(g/setProperties button #js {:disabled "disabled"})
|
(g/setProperties button #js {:disabled "disabled"})
|
||||||
(start))))
|
(start))))
|
||||||
|
|
||||||
|
(let [button (g/getElement "start-group-chat-button")]
|
||||||
|
(e/listen button EventType/CLICK
|
||||||
|
(fn [e]
|
||||||
|
(g/setProperties button #js {:disabled "disabled"})
|
||||||
|
(g/setProperties (g/getElement "to-identities") #js {:disabled "disabled"})
|
||||||
|
(start-group-chat))))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue