wip group chat

This commit is contained in:
michaelr 2016-03-09 17:04:39 +02:00
parent 47af4de5b3
commit 1fd85efc7d
24 changed files with 631 additions and 286 deletions

11
protocol/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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