protocol rework

This commit is contained in:
Roman Volosovskyi 2016-09-04 18:39:05 +03:00
parent 21963e47de
commit 27ab637e94
36 changed files with 1479 additions and 412 deletions

View File

@ -3,8 +3,8 @@
:url "http://example.com/FIXME" :url "http://example.com/FIXME"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"} :url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.9.0-alpha7"] :dependencies [[org.clojure/clojure "1.9.0-alpha11"]
[org.clojure/clojurescript "1.9.76"] [org.clojure/clojurescript "1.9.227"]
[reagent "0.5.1" :exclusions [cljsjs/react]] [reagent "0.5.1" :exclusions [cljsjs/react]]
[re-frame "0.7.0"] [re-frame "0.7.0"]
[prismatic/schema "1.0.4"] [prismatic/schema "1.0.4"]
@ -13,7 +13,11 @@
[natal-shell "0.3.0"] [natal-shell "0.3.0"]
[com.andrewmcveigh/cljs-time "0.4.0"] [com.andrewmcveigh/cljs-time "0.4.0"]
[tailrecursion/cljs-priority-map "1.2.0"] [tailrecursion/cljs-priority-map "1.2.0"]
[cljsjs/web3 "0.16.0-0"]] [cljsjs/web3 "0.16.0-0"]
[com.taoensso/timbre "4.7.4"]
[org.clojure/test.check "0.9.0"]
[cljsjs/chance "0.7.3-0"]
[cljsjs/eccjs "0.3.1-0"]]
:plugins [[lein-cljsbuild "1.1.1"] :plugins [[lein-cljsbuild "1.1.1"]
[lein-figwheel "0.5.0-2"] [lein-figwheel "0.5.0-2"]
[lein-voom "0.1.0-20160311_203101-g259fbfc"]] [lein-voom "0.1.0-20160311_203101-g259fbfc"]]

View File

@ -2,7 +2,7 @@
(:require [status-im.models.accounts :as accounts-model] (:require [status-im.models.accounts :as accounts-model]
[re-frame.core :refer [register-handler after dispatch dispatch-sync debug]] [re-frame.core :refer [register-handler after dispatch dispatch-sync debug]]
[status-im.utils.logging :as log] [status-im.utils.logging :as log]
[status-im.protocol.api :as api] [status-im.protocol.core :as protocol]
[status-im.components.status :as status] [status-im.components.status :as status]
[status-im.utils.types :refer [json->clj]] [status-im.utils.types :refer [json->clj]]
[status-im.persistence.simple-kv-store :as kv] [status-im.persistence.simple-kv-store :as kv]
@ -33,14 +33,17 @@
(storage/put kv/kv-store :password password)) (storage/put kv/kv-store :password password))
(defn account-created [result password] (defn account-created [result password]
(let [data (json->clj result) (let [data (json->clj result)
public-key (:pubkey data) public-key (:pubkey data)
address (:address data) address (:address data)
mnemonic (:mnemonic data) mnemonic (:mnemonic data)
account {:public-key public-key {:keys [public private]} (protocol/new-keypair!)
:address address account {:public-key public-key
:name address :address address
:photo-path (identicon public-key)}] :name address
:updates-public-key public
:updates-private-key private
:photo-path (identicon public-key)}]
(log/debug "account-created") (log/debug "account-created")
(when (not (str/blank? public-key)) (when (not (str/blank? public-key))
(do (do
@ -61,15 +64,25 @@
(accounts-model/save-accounts [(get accounts current-account-id)] true)) (accounts-model/save-accounts [(get accounts current-account-id)] true))
(defn send-account-update (defn send-account-update
[{:keys [current-account-id accounts]} _] [{:keys [current-account-id current-public-key web3 accounts]} _]
(api/send-account-update (get accounts current-account-id))) (let [{:keys [name photo-path status]} (get accounts current-account-id)
{:keys [updates-public-key updates-private-key]} (accounts current-account-id)]
(protocol/broadcats-profile!
{:web3 web3
:message {:from current-public-key
:message-id (random/id)
:keypair {:public updates-public-key
:private updates-private-key}
:payload {:profile {:name name
:status status
:profile-image photo-path}}}})))
(register-handler (register-handler
:account-update :account-update
(-> (fn [{:keys [current-account-id accounts] :as db} [_ data]] (-> (fn [{:keys [current-account-id accounts] :as db} [_ data]]
(let [data (assoc data :last-updated (time/now-ms)) (let [data (assoc data :last-updated (time/now-ms))
account (-> (get accounts current-account-id) account (-> (get accounts current-account-id)
(merge data))] (merge data))]
(assoc-in db [:accounts current-account-id] account))) (assoc-in db [:accounts current-account-id] account)))
((after save-account-to-realm!)) ((after save-account-to-realm!))
((after send-account-update)))) ((after send-account-update))))
@ -79,7 +92,7 @@
(u/side-effect! (u/side-effect!
(fn [{:keys [current-account-id accounts]} _] (fn [{:keys [current-account-id accounts]} _]
(let [{:keys [last-updated]} (get accounts current-account-id) (let [{:keys [last-updated]} (get accounts current-account-id)
now (time/now-ms) now (time/now-ms)
needs-update? (> (- now last-updated) time/week)] needs-update? (> (- now last-updated) time/week)]
(log/info "Need to send account-update: " needs-update?) (log/info "Need to send account-update: " needs-update?)
(when needs-update? (when needs-update?

View File

@ -6,6 +6,7 @@
[status-im.components.styles :refer [default-chat-color]] [status-im.components.styles :refer [default-chat-color]]
[status-im.chat.suggestions :as suggestions] [status-im.chat.suggestions :as suggestions]
[status-im.protocol.api :as api] [status-im.protocol.api :as api]
[status-im.protocol.core :as protocol]
[status-im.models.chats :as chats] [status-im.models.chats :as chats]
[status-im.models.messages :as messages] [status-im.models.messages :as messages]
[status-im.models.pending-messages :as pending-messages] [status-im.models.pending-messages :as pending-messages]
@ -174,7 +175,7 @@
(defn init-console-chat (defn init-console-chat
[{:keys [chats] :as db} existing-account?] [{:keys [chats] :as db} existing-account?]
(let [chat-id "console" (let [chat-id "console"
new-chat sign-up-service/console-chat] new-chat sign-up-service/console-chat]
(if (chats chat-id) (if (chats chat-id)
db db
@ -264,11 +265,6 @@
(after #(dispatch [:load-unviewed-messages!])) (after #(dispatch [:load-unviewed-messages!]))
((enrich initialize-chats) load-chats!)) ((enrich initialize-chats) load-chats!))
(register-handler :initialize-pending-messages
(u/side-effect!
(fn [_ _]
(api/init-pending-messages (pending-messages/get-pending-messages)))))
(defmethod nav/preload-data! :chat (defmethod nav/preload-data! :chat
[{:keys [current-chat-id] :as db} [_ _ id]] [{:keys [current-chat-id] :as db} [_ _ id]]
(let [chat-id (or id current-chat-id) (let [chat-id (or id current-chat-id)
@ -324,9 +320,9 @@
(dispatch [::start-chat! contact-id options navigation-type]))))) (dispatch [::start-chat! contact-id options navigation-type])))))
(register-handler :add-chat (register-handler :add-chat
(-> prepare-chat (-> prepare-chat
((enrich add-chat)) ((enrich add-chat))
((after save-new-chat!)))) ((after save-new-chat!))))
(register-handler :switch-command-suggestions! (register-handler :switch-command-suggestions!
(u/side-effect! (u/side-effect!
@ -335,12 +331,8 @@
(dispatch [:set-chat-input-text text]))))) (dispatch [:set-chat-input-text text])))))
(defn remove-chat (defn remove-chat
[{:keys [current-chat-id] :as db} _] [db [_ chat-id]]
(update db :chats dissoc current-chat-id)) (update db :chats dissoc chat-id))
(defn notify-about-leaving!
[{:keys [current-chat-id]} _]
(api/leave-group-chat current-chat-id))
; todo do we really need this message? ; todo do we really need this message?
(defn leaving-message! (defn leaving-message!
@ -353,27 +345,47 @@
:content-type text-content-type})) :content-type text-content-type}))
(defn delete-messages! (defn delete-messages!
[{:keys [current-chat-id]} _] [{:keys [current-chat-id]} [_ chat-id]]
(r/write :account (let [id (or chat-id current-chat-id)]
(fn [] (r/write :account
(r/delete :account (r/get-by-field :account :message :chat-id current-chat-id))))) (fn []
(r/delete :account
(r/get-by-field :account :message :chat-id id))))))
(defn delete-chat! (defn delete-chat!
[{:keys [current-chat-id]} _] [_ [_ chat-id]]
(r/write :account (r/write :account
(fn [] :account (fn [] :account
(->> (r/get-by-field :account :chat :chat-id current-chat-id) (when-let [chat (->> (r/get-by-field :account :chat :chat-id chat-id)
(r/single) (r/single))]
(r/delete :account))))) (doto chat
(aset "is-active" false)
(aset "removed-at" (.getTime (js/Date.))))))))
(defn remove-pending-messages!
[_ [_ chat-id]]
(pending-messages/remove-all-by-chat chat-id))
(register-handler :leave-group-chat (register-handler :leave-group-chat
;; todo oreder of operations tbd ;; todo oreder of operations tbd
(after (fn [_ _] (dispatch [:navigation-replace :chat-list]))) (after (fn [_ _] (dispatch [:navigation-replace :chat-list])))
(u/side-effect!
(fn [{:keys [web3 current-chat-id chats current-public-key]} _]
(let [{:keys [public-key private-key]} (chats current-chat-id)]
(protocol/leave-group-chat!
{:web3 web3
:group-id current-chat-id
:keypair {:public public-key
:private private-key}
:message {:from current-public-key
:message-id (random/id)}}))
(dispatch [::remove-chat current-chat-id]))))
(register-handler ::remove-chat
(-> remove-chat (-> remove-chat
;; todo uncomment
;((after notify-about-leaving!))
;((after leaving-message!)) ;((after leaving-message!))
((after delete-messages!)) ((after delete-messages!))
((after remove-pending-messages!))
((after delete-chat!)))) ((after delete-chat!))))
(defn edit-mode-handler [mode] (defn edit-mode-handler [mode]
@ -405,13 +417,18 @@
(assoc db :layout-height h))) (assoc db :layout-height h)))
(register-handler :send-seen! (register-handler :send-seen!
(after (fn [_ [_ chat-id message-id]] (after (fn [_ [_ options]]
(dispatch [:message-seen {:message-id message-id (dispatch [:message-seen options])))
:chat-id chat-id}])))
(u/side-effect! (u/side-effect!
(fn [_ [_ chat-id message-id]] (fn [{:keys [web3 current-public-key chats]}
[_ {:keys [from chat-id message-id]}]]
(when-not (console? chat-id) (when-not (console? chat-id)
(api/send-seen chat-id message-id))))) (let [{:keys [group-chat]} (chats chat-id)]
(protocol/send-seen! {:web3 web3
:message {:from current-public-key
:to from
:group-id (when group-chat chat-id)
:message-id message-id}}))))))
(register-handler :set-web-view-url (register-handler :set-web-view-url
(fn [{:keys [current-chat-id] :as db} [_ url]] (fn [{:keys [current-chat-id] :as db} [_ url]]

View File

@ -27,23 +27,35 @@
(defn receive-message (defn receive-message
[db [_ {:keys [from group-id chat-id message-id] :as message}]] [db [_ {:keys [from group-id chat-id message-id] :as message}]]
(let [same-message (messages/get-message message-id) (let [same-message (messages/get-message message-id)
current-identity (get-current-identity db)] current-identity (get-current-identity db)
(when-not (or same-message (= from current-identity)) chat-id' (or group-id chat-id from)
exists? (c/chat-exists? chat-id')
active? (c/is-active? chat-id')]
(when (and (not same-message)
(not= from current-identity)
(or (not exists?) active?))
(let [group-chat? (not (nil? group-id)) (let [group-chat? (not (nil? group-id))
chat-id' (or chat-id from)
previous-message (messages/get-last-message chat-id') previous-message (messages/get-last-message chat-id')
message' (assoc (->> message message' (assoc (->> message
(cu/check-author-direction previous-message) (cu/check-author-direction previous-message)
(check-preview)) (check-preview))
:chat-id chat-id')] :chat-id chat-id')]
(store-message message') (store-message message')
(when-not (c/chat-exists? chat-id') (when-not exists?
(dispatch [:add-chat chat-id' (when group-chat? {:group-chat true})])) (dispatch [:add-chat chat-id' (when group-chat? {:group-chat true})]))
(dispatch [::add-message message']) (dispatch [::add-message message'])
(when (= (:content-type message') content-type-command-request) (when (= (:content-type message') content-type-command-request)
(dispatch [:add-request chat-id' message'])) (dispatch [:add-request chat-id' message']))
(dispatch [:add-unviewed-message chat-id' message-id]))))) (dispatch [:add-unviewed-message chat-id' message-id])))))
(register-handler :received-protocol-message!
(u/side-effect!
(fn [_ [_ {:keys [from to payload]}]]
(dispatch [:received-message (merge payload
{:from from
:to to
:chat-id from})]))))
(register-handler :received-message (register-handler :received-message
(u/side-effect! receive-message)) (u/side-effect! receive-message))

View File

@ -5,34 +5,15 @@
[status-im.components.status :as status] [status-im.components.status :as status]
[status-im.utils.random :as random] [status-im.utils.random :as random]
[status-im.utils.datetime :as time] [status-im.utils.datetime :as time]
[re-frame.core :refer [enrich after debug dispatch path]] [re-frame.core :refer [enrich after dispatch path]]
[status-im.chat.suggestions :as suggestions]
[status-im.models.commands :as commands]
[status-im.chat.utils :as cu] [status-im.chat.utils :as cu]
[status-im.constants :refer [text-content-type [status-im.constants :refer [text-content-type
content-type-command content-type-command
content-type-command-request content-type-command-request
default-number-of-messages]] default-number-of-messages]]
[status-im.protocol.api :as api] [status-im.utils.datetime :as datetime]
[status-im.utils.logging :as log])) [status-im.protocol.core :as protocol]
[taoensso.timbre :refer-macros [debug]]))
(defn prepare-message
[{:keys [identity current-chat-id] :as db} _]
(let [text (get-in db [:chats current-chat-id :input-text])
[command] (suggestions/check-suggestion db (str text " "))
message (cu/check-author-direction
db current-chat-id
{:message-id (random/id)
:chat-id current-chat-id
:content text
:to current-chat-id
:from identity
:content-type text-content-type
:outgoing true
:timestamp (time/now-ms)})]
(if command
(commands/set-command-input db :commands command)
(assoc db :new-message (when-not (s/blank? text) message)))))
(defn prepare-command (defn prepare-command
[identity chat-id {:keys [preview preview-string content command to-message]}] [identity chat-id {:keys [preview preview-string content command to-message]}]
@ -52,13 +33,13 @@
(register-handler :send-chat-message (register-handler :send-chat-message
(u/side-effect! (u/side-effect!
(fn [{:keys [current-chat-id identity current-account-id] :as db}] (fn [{:keys [current-chat-id current-public-key current-account-id] :as db}]
(let [staged-commands (vals (get-in db [:chats current-chat-id :staged-commands])) (let [staged-commands (vals (get-in db [:chats current-chat-id :staged-commands]))
text (get-in db [:chats current-chat-id :input-text]) text (get-in db [:chats current-chat-id :input-text])
data {:commands staged-commands data {:commands staged-commands
:message text :message text
:chat-id current-chat-id :chat-id current-chat-id
:identity identity :identity current-public-key
:address current-account-id}] :address current-account-id}]
(dispatch [:clear-input current-chat-id]) (dispatch [:clear-input current-chat-id])
(cond (cond
@ -88,9 +69,9 @@
(register-handler :prepare-command! (register-handler :prepare-command!
(u/side-effect! (u/side-effect!
(fn [db [_ {:keys [chat-id command identity] :as params}]] (fn [{:keys [current-public-key] :as db} [_ {:keys [chat-id command] :as params}]]
(let [command' (->> command (let [command' (->> command
(prepare-command identity chat-id) (prepare-command current-public-key chat-id)
(cu/check-author-direction db chat-id))] (cu/check-author-direction db chat-id))]
(dispatch [::clear-command chat-id (:id command)]) (dispatch [::clear-command chat-id (:id command)])
(dispatch [::send-command! (assoc params :command command')]))))) (dispatch [::send-command! (assoc params :command command')])))))
@ -115,7 +96,7 @@
(register-handler ::save-command! (register-handler ::save-command!
(u/side-effect! (u/side-effect!
(fn [_ [_ {:keys [command chat-id]}]] (fn [{:keys [current-public-key]} [_ {:keys [command chat-id]}]]
(messages/save-message (messages/save-message
chat-id chat-id
(dissoc command :rendered-preview :to-message :has-handler))))) (dissoc command :rendered-preview :to-message :has-handler)))))
@ -177,22 +158,41 @@
(register-handler ::send-message! (register-handler ::send-message!
(u/side-effect! (u/side-effect!
(fn [_ [_ {{:keys [message-type] (fn [{:keys [web3 chats]} [_ {{:keys [message-type]
:as message} :message :as message} :message
chat-id :chat-id}]] chat-id :chat-id}]]
(when (and message (cu/not-console? chat-id)) (when (and message (cu/not-console? chat-id))
(if (= message-type :group-user-message) (let [message' (select-keys message [:from :message-id])
(api/send-group-user-message message) payload (select-keys message [:timestamp :content :content-type])
(api/send-user-message message)))))) options {:web3 web3
:message (assoc message' :payload payload)}]
(if (= message-type :group-user-message)
(let [{:keys [public-key private-key]} (chats chat-id)]
(protocol/send-group-message! (assoc options
:group-id chat-id
:keypair {:public public-key
:private private-key})))
(protocol/send-message! (assoc-in options
[:message :to] (:to message)))))))))
(register-handler ::send-command-protocol! (register-handler ::send-command-protocol!
(u/side-effect! (u/side-effect!
(fn [db [_ {:keys [chat-id command]}]] (fn [{:keys [web3 current-public-key chats] :as db} [_ {:keys [chat-id command]}]]
(let [{:keys [content]} command] (let [{:keys [content message-id]} command]
(when (cu/not-console? chat-id) (when (cu/not-console? chat-id)
(let [{:keys [group-chat]} (get-in db [:chats chat-id]) (let [{:keys [public-key private-key]} (chats chat-id)
message {:content content {:keys [group-chat]} (get-in db [:chats chat-id])
:content-type content-type-command}] payload {:content content
:content-type content-type-command
:timestamp (datetime/now-ms)}
options {:web3 web3
:message {:from current-public-key
:message-id message-id
:payload payload}}]
(if group-chat (if group-chat
(api/send-group-user-message (assoc message :group-id chat-id)) (protocol/send-group-message! (assoc options
(api/send-user-message (assoc message :to chat-id))))))))) :group-id chat-id
:keypair {:public public-key
:private private-key}))
(protocol/send-message! (assoc-in options
[:message :to] chat-id)))))))))

View File

@ -262,7 +262,7 @@
children)])})) children)])}))
(into [view] children))) (into [view] children)))
(defn chat-message [{:keys [outgoing message-id chat-id user-statuses]}] (defn chat-message [{:keys [outgoing message-id chat-id user-statuses from]}]
(let [my-identity (api/my-identity) (let [my-identity (api/my-identity)
status (subscribe [:get-in [:message-user-statuses message-id my-identity]])] status (subscribe [:get-in [:message-user-statuses message-id my-identity]])]
(r/create-class (r/create-class
@ -271,7 +271,9 @@
(when (and (not outgoing) (when (and (not outgoing)
(not= :seen (keyword @status)) (not= :seen (keyword @status))
(not= :seen (keyword (get-in user-statuses [my-identity :status])))) (not= :seen (keyword (get-in user-statuses [my-identity :status]))))
(dispatch [:send-seen! chat-id message-id]))) (dispatch [:send-seen! {:chat-id chat-id
:from from
:message-id message-id}])))
:reagent-render :reagent-render
(fn [{:keys [outgoing timestamp new-day group-chat] :as message}] (fn [{:keys [outgoing timestamp new-day group-chat] :as message}]
[message-container message [message-container message

View File

@ -4,13 +4,13 @@
[status-im.models.contacts :as contacts] [status-im.models.contacts :as contacts]
[status-im.utils.crypt :refer [encrypt]] [status-im.utils.crypt :refer [encrypt]]
[clojure.string :as s] [clojure.string :as s]
[status-im.protocol.api :as api] [status-im.protocol.core :as protocol]
[status-im.utils.utils :refer [http-post]] [status-im.utils.utils :refer [http-post]]
[status-im.utils.phone-number :refer [format-phone-number]] [status-im.utils.phone-number :refer [format-phone-number]]
[status-im.utils.handlers :as u] [status-im.utils.handlers :as u]
[status-im.utils.utils :refer [require]] [status-im.utils.utils :refer [require]]
[status-im.utils.logging :as log] [status-im.navigation.handlers :as nav]
[status-im.navigation.handlers :as nav])) [status-im.utils.random :as random]))
(defmethod nav/preload-data! :group-contacts (defmethod nav/preload-data! :group-contacts
@ -32,15 +32,30 @@
(contacts/save-contacts [contact])) (contacts/save-contacts [contact]))
(defn watch-contact (defn watch-contact
[_ [_ {:keys [whisper-identity]}]] [{:keys [web3]} [_ {:keys [whisper-identity public-key private-key]}]]
(api/watch-user whisper-identity)) (when (and public-key private-key)
(protocol/watch-user! {:web3 web3
:identity whisper-identity
:keypair {:public public-key
:private private-key}
:callback #(dispatch [:incoming-message %1 %2])})))
(register-handler :watch-contact (u/side-effect! watch-contact)) (register-handler :watch-contact (u/side-effect! watch-contact))
(defn send-contact-request (defn send-contact-request
[{:keys [current-account-id accounts]} [_ contact]] [{:keys [current-public-key web3 current-account-id accounts]} [_ contact]]
(let [account (get accounts current-account-id)] (let [{:keys [whisper-identity]} contact
(api/send-contact-request contact account))) {:keys [name photo-path updates-public-key updates-private-key]} (get accounts current-account-id)]
(protocol/contact-request!
{:web3 web3
:message {:from current-public-key
:to whisper-identity
:message-id (random/id)
:payload {:contact {:name name
:profile-image photo-path
:address current-account-id}
:keypair {:public updates-public-key
:private updates-private-key}}}})))
(register-handler :update-contact! (register-handler :update-contact!
(-> (fn [db [_ {:keys [whisper-identity] :as contact}]] (-> (fn [db [_ {:keys [whisper-identity] :as contact}]]
@ -107,7 +122,7 @@
(defn request-stored-contacts [contacts] (defn request-stored-contacts [contacts]
(let [contacts-by-hash (get-contacts-by-hash contacts) (let [contacts-by-hash (get-contacts-by-hash contacts)
data (or (keys contacts-by-hash) ())] data (or (keys contacts-by-hash) ())]
(http-post "get-contacts" {:phone-number-hashes data} (http-post "get-contacts" {:phone-number-hashes data}
(fn [{:keys [contacts]}] (fn [{:keys [contacts]}]
(let [contacts' (add-identity contacts-by-hash contacts)] (let [contacts' (add-identity contacts-by-hash contacts)]
@ -131,7 +146,7 @@
(defn add-new-contacts (defn add-new-contacts
[{:keys [contacts] :as db} [_ new-contacts]] [{:keys [contacts] :as db} [_ new-contacts]]
(let [identities (set (map :whisper-identity contacts)) (let [identities (set (map :whisper-identity contacts))
new-contacts' (->> new-contacts new-contacts' (->> new-contacts
(map #(update-pending-status contacts %)) (map #(update-pending-status contacts %))
(remove #(identities (:whisper-identity %))) (remove #(identities (:whisper-identity %)))
@ -159,8 +174,7 @@
(register-handler ::prepare-contact (register-handler ::prepare-contact
(-> add-new-contact (-> add-new-contact
((after save-contact)) ((after save-contact))
((after send-contact-request)) ((after send-contact-request))))
((after watch-contact))))
(register-handler ::update-pending-contact (register-handler ::update-pending-contact
(-> add-new-contact (-> add-new-contact
@ -168,9 +182,21 @@
(register-handler :add-pending-contact (register-handler :add-pending-contact
(u/side-effect! (u/side-effect!
(fn [_ [_ {:keys [whisper-identity] :as contact}]] (fn [{:keys [current-public-key web3 current-account-id accounts]}
(let [contact (assoc contact :pending false)] [_ {:keys [whisper-identity] :as contact}]]
(api/send-discovery-keypair whisper-identity) (let [contact (assoc contact :pending false)
{:keys [name photo-path updates-public-key updates-private-key]}
(accounts current-account-id)]
(protocol/contact-request!
{:web3 web3
:message {:from current-public-key
:to whisper-identity
:message-id (random/id)
:payload {:contact {:name name
:profile-image photo-path
:address current-account-id}
:keypair {:public updates-public-key
:private updates-private-key}}}})
(dispatch [::update-pending-contact contact]))))) (dispatch [::update-pending-contact contact])))))
(defn set-contact-identity-from-qr (defn set-contact-identity-from-qr
@ -181,34 +207,28 @@
(register-handler :contact-update-received (register-handler :contact-update-received
(u/side-effect! (u/side-effect!
;; TODO: security issue: we should use `from` instead of `public-key` here, but for testing it is much easier to use `public-key` (fn [{:keys [chats] :as db} [_ {:keys [from payload]}]]
(fn [db [_ from {{:keys [public-key last-updated name] :as account} :account}]] (let [{:keys [content timestamp]} payload
(let [prev-last-updated (get-in db [:contacts public-key :last-updated])] {:keys [status name profile-image]} (:profile content)
(if (<= prev-last-updated last-updated) prev-last-updated (get-in db [:contacts from :last-updated])]
(let [contact (-> (assoc account :whisper-identity public-key) (if (<= prev-last-updated timestamp)
(dissoc :public-key))] (let [contact {:whisper-identity from
:name name
:photo-path profile-image
:status status
:last-updated timestamp}]
(dispatch [:update-contact! contact]) (dispatch [:update-contact! contact])
(dispatch [:update-chat! {:chat-id public-key (when (chats from)
:name name}]))))))) (dispatch [:update-chat! {:chat-id from
:name name}]))))))))
(register-handler :contact-online-received (register-handler :contact-online-received
(u/side-effect! (u/side-effect!
(fn [db [_ from {last-online :at :as payload}]] (fn [db [_ {:keys [from]
{:keys [timestamp]} :payload}]]
(let [prev-last-online (get-in db [:contacts from :last-online])] (let [prev-last-online (get-in db [:contacts from :last-online])]
(when (< prev-last-online last-online) (when (< prev-last-online timestamp)
(api/resend-pending-messages from) (protocol/reset-pending-messages! from)
(dispatch [:update-contact! {:whisper-identity from (dispatch [:update-contact! {:whisper-identity from
:last-online last-online}])))))) :last-online timestamp}]))))))
(register-handler :contact-request-received
(u/side-effect!
(fn [_ [_ {:keys [contact from]}]]
(let [contact (assoc contact :whisper-identity from
:pending true)]
(dispatch [:add-contacts [contact]])))))
(register-handler :contact-keypair-received
(u/side-effect!
(fn [_ [_ from]]
(api/stop-watching-user from)
(api/watch-user from))))

View File

@ -11,6 +11,7 @@
;; initial state of app-db ;; initial state of app-db
(def app-db {:identity-password "replace-me-with-user-entered-password" (def app-db {:identity-password "replace-me-with-user-entered-password"
:identity "me" :identity "me"
:current-public-key "me"
:accounts {} :accounts {}
:current-account-id nil :current-account-id nil

View File

@ -3,10 +3,12 @@
[status-im.utils.utils :refer [first-index]] [status-im.utils.utils :refer [first-index]]
[status-im.utils.handlers :refer [register-handler]] [status-im.utils.handlers :refer [register-handler]]
[status-im.protocol.api :as api] [status-im.protocol.api :as api]
[status-im.protocol.core :as protocol]
[status-im.navigation.handlers :as nav] [status-im.navigation.handlers :as nav]
[status-im.discovery.model :as discoveries] [status-im.discovery.model :as discoveries]
[status-im.utils.handlers :as u] [status-im.utils.handlers :as u]
[status-im.utils.datetime :as time])) [status-im.utils.datetime :as time]
[status-im.utils.random :as random]))
(register-handler :init-discoveries (register-handler :init-discoveries
(fn [db _] (fn [db _]
@ -14,8 +16,8 @@
(assoc :discoveries [])))) (assoc :discoveries []))))
(defn calculate-priority [{:keys [chats contacts]} from payload] (defn calculate-priority [{:keys [chats contacts]} from payload]
(let [contact (get contacts from) (let [contact (get contacts from)
chat (get chats from) chat (get chats from)
seen-online-recently? (< (- (time/now-ms) (get contact :last-online)) seen-online-recently? (< (- (time/now-ms) (get contact :last-online))
time/hour)] time/hour)]
(+ (time/now-ms) ;; message is newer => priority is higher (+ (time/now-ms) ;; message is newer => priority is higher
@ -35,23 +37,37 @@
(register-handler :discovery-response-received (register-handler :discovery-response-received
(u/side-effect! (u/side-effect!
(fn [db [_ from payload]] (fn [{:keys [current-public-key] :as db} [_ {:keys [from payload]}]]
(let [{:keys [message-id name photo-path status hashtags]} payload (when-not (= current-public-key from)
discovery {:message-id message-id (let [{:keys [discovery-id profile hashtags]} payload
:name name {:keys [name profile-image status]} profile
:photo-path photo-path discovery {:message-id discovery-id
:status status :name name
:whisper-id from :photo-path profile-image
:tags (map #(hash-map :name %) hashtags) :status status
:last-updated (js/Date.) :whisper-id from
:priority (calculate-priority db from payload)}] :tags (map #(hash-map :name %) hashtags)
(dispatch [:add-discovery discovery]))))) :last-updated (js/Date.)
:priority (calculate-priority db from payload)}]
(dispatch [:add-discovery discovery]))))))
(register-handler :broadcast-status (register-handler :broadcast-status
(u/side-effect! (u/side-effect!
(fn [{:keys [current-account-id accounts]} [_ status hashtags]] (fn [{:keys [current-public-key web3 current-account-id accounts]}
(let [account (get accounts current-account-id)] [_ status hashtags]]
(api/broadcast-discover-status account status hashtags))))) (let [{:keys [name photo-path]} (get accounts current-account-id)]
(protocol/broadcats-discoveries!
{:web3 web3
:hashtags (set hashtags)
:message {:from current-public-key
:message-id (random/id)
:payload {:status status
:profile {:name name
:status status
:profile-image photo-path}}}})
(protocol/watch-hashtags! {:web3 web3
:hashtags (set hashtags)
:callback #(dispatch [:incoming-message %1 %2])})))))
(register-handler :show-discovery-tag (register-handler :show-discovery-tag
(fn [db [_ tag]] (fn [db [_ tag]]

View File

@ -3,7 +3,7 @@
[status-im.utils.handlers :refer [register-handler]] [status-im.utils.handlers :refer [register-handler]]
[status-im.persistence.realm.core :as r] [status-im.persistence.realm.core :as r]
[status-im.chat.handlers :refer [delete-messages!]] [status-im.chat.handlers :refer [delete-messages!]]
[status-im.protocol.api :as api] [status-im.protocol.core :as protocol]
[status-im.utils.random :as random] [status-im.utils.random :as random]
[status-im.models.contacts :as contacts] [status-im.models.contacts :as contacts]
[status-im.models.messages :as messages] [status-im.models.messages :as messages]
@ -84,16 +84,17 @@
[{:keys [current-chat-id selected-participants] :as db} _] [{:keys [current-chat-id selected-participants] :as db} _]
(let [chat (get-in db [:chats current-chat-id])] (let [chat (get-in db [:chats current-chat-id])]
(r/write :account (r/write :account
(fn [] (fn []
(r/create :account (r/create :account
:chat :chat
(update chat :contacts remove-identities selected-participants) (update chat :contacts remove-identities selected-participants)
true))))) true)))))
(defn notify-about-removing! (defn notify-about-removing!
[{:keys [current-chat-id selected-participants]} _] [{:keys [current-chat-id selected-participants]} _]
(doseq [participant selected-participants] (doseq [participant selected-participants]
(api/group-remove-participant current-chat-id participant))) ;;todo implement
))
(defn system-message [message-id content] (defn system-message [message-id content]
{:from "system" {:from "system"
@ -138,14 +139,33 @@
(chats/chat-add-participants current-chat-id selected-participants)) (chats/chat-add-participants current-chat-id selected-participants))
(defn notify-about-new-members! (defn notify-about-new-members!
[{:keys [current-chat-id selected-participants]} _] [{:keys [current-chat-id selected-participants
(doseq [identity selected-participants] current-public-key chats web3]} _]
(api/group-add-participant current-chat-id identity))) (let [{:keys [public-key private-key name contacts]} (chats current-chat-id)
identities (map :identity contacts)
keypair {:public public-key
:private private-key}]
(protocol/invite-to-group!
{:web3 web3
:group {:id current-chat-id
:name name
:contacts (conj identities current-public-key)
:admin current-public-key
:keypair keypair}
:identities selected-participants
:message {:from current-public-key
:message-id (random/id)}})
(doseq [identity selected-participants]
(protocol/add-to-group! {:web3 web3
:group-id current-chat-id
:identity identity
:keypair keypair
:message {:from current-public-key
:message-id (random/id)}}))))
(register-handler :add-new-participants (register-handler :add-new-participants
;; todo order of operations tbd ;; todo order of operations tbd
(-> add-memebers (-> add-memebers
((after add-members-to-realm!)) ((after add-members-to-realm!))
;; todo uncomment ((after notify-about-new-members!))
;((after notify-about-new-members!))
((enrich deselect-members)))) ((enrich deselect-members))))

View File

@ -60,22 +60,22 @@
(register-handler :initialize-db (register-handler :initialize-db
(fn [_ _] (fn [_ _]
(realm/reset-account) (realm/reset-account)
(assoc app-db :current-account-id nil (assoc app-db :current-account-id nil)))
:current-public-key nil)))
(register-handler :initialize-account-db (register-handler :initialize-account-db
(fn [db _] (fn [db _]
(assoc db (assoc db
:chats {}
:current-chat-id "console"
:signed-up (storage/get kv/kv-store :signed-up) :signed-up (storage/get kv/kv-store :signed-up)
:password (storage/get kv/kv-store :password)))) :password (storage/get kv/kv-store :password))))
(register-handler :initialize-account (register-handler :initialize-account
(u/side-effect! (u/side-effect!
(fn [_ [_ address]] (fn [_ [_ address]]
(dispatch [:initialize-protocol address])
(dispatch [:initialize-account-db]) (dispatch [:initialize-account-db])
(dispatch [:initialize-protocol address])
(dispatch [:initialize-chats]) (dispatch [:initialize-chats])
(dispatch [:initialize-pending-messages])
(dispatch [:load-contacts]) (dispatch [:load-contacts])
(dispatch [:init-chat]) (dispatch [:init-chat])
(dispatch [:init-discoveries]) (dispatch [:init-discoveries])

View File

@ -46,21 +46,21 @@
(defn re-join-group-chat [db group-id identities group-name] (defn re-join-group-chat [db group-id identities group-name]
(r/write :account (r/write :account
(fn [] (fn []
(let [new-identities (set identities) (let [new-identities (set identities)
only-old-contacts (->> (chat-contacts group-id) only-old-contacts (->> (chat-contacts group-id)
(r/cljs-list) (r/cljs-list)
(remove (fn [{:keys [identity]}] (remove (fn [{:keys [identity]}]
(new-identities identity)))) (new-identities identity))))
contacts (->> new-identities contacts (->> new-identities
(mapv (fn [ident] (mapv (fn [ident]
{:identity ident})) {:identity ident}))
(concat only-old-contacts))] (concat only-old-contacts))]
(r/create :account :chat (r/create :account :chat
{:chat-id group-id {:chat-id group-id
:is-active true :is-active true
:name group-name :name group-name
:contacts contacts} true)))) :contacts contacts} true))))
db) db)
(defn normalize-contacts (defn normalize-contacts
@ -68,7 +68,7 @@
(map #(update % :contacts vals) chats)) (map #(update % :contacts vals) chats))
(defn chats-list [] (defn chats-list []
(-> (r/get-all :account :chat) (-> (r/get-by-field :account :chat :is-active true)
(r/sorted :timestamp :desc) (r/sorted :timestamp :desc)
r/realm-collection->list r/realm-collection->list
normalize-contacts)) normalize-contacts))
@ -89,54 +89,46 @@
(defn create-chat (defn create-chat
([{:keys [last-message-id] :as chat}] ([{:keys [last-message-id] :as chat}]
(let [chat (assoc chat :last-message-id (or last-message-id ""))] (let [chat (assoc chat :last-message-id (or last-message-id ""))]
(r/write :account #(r/create :account :chat chat true)))) (r/write :account #(r/create :account :chat chat true)))))
([db chat-id identities group-chat? chat-name]
(when-not (chat-exists? chat-id)
(let [chat-name (or chat-name
(get-chat-name chat-id identities))
_ (log/debug "creating chat" chat-name)]
(r/write :account
(fn []
(let [contacts (mapv (fn [ident]
{:identity ident}) identities)]
(r/create :account :chat
{:chat-id chat-id
:is-active true
:name chat-name
:group-chat group-chat?
:timestamp (timestamp)
:contacts contacts
:last-message-id ""}))))
(add-status-message chat-id)))))
(defn chat-add-participants [chat-id identities] (defn chat-add-participants [chat-id identities]
(r/write :account (r/write :account
(fn [] (fn []
(let [contacts (chat-contacts chat-id)] (let [contacts (chat-contacts chat-id)
(doseq [contact-identity identities] added-at (timestamp)]
(if-let [contact-exists (.find contacts (fn [object index collection] (doseq [contact-identity identities]
(if-let [contact (.find contacts (fn [object index collection]
(= contact-identity (aget object "identity"))))] (= contact-identity (aget object "identity"))))]
(aset contact-exists "is-in-chat" true) (doto contact
(.push contacts (clj->js {:identity contact-identity}))))))) (aset "is-in-chat" true)
(aset "added-at" added-at))
(.push contacts (clj->js {:identity contact-identity
:added-at added-at})))))))
;; TODO temp. Update chat in db atom ;; TODO temp. Update chat in db atom
(dispatch [:initialize-chats])) (dispatch [:initialize-chats]))
;; TODO deprecated? (is there need to remove multiple member at once?)
(defn chat-remove-participants [chat-id identities] (defn chat-remove-participants [chat-id identities]
(r/write :account (r/write :account
(fn [] (fn []
(let [query (include-query :identity identities) (let [query (include-query :identity identities)
chat (r/single (r/get-by-field :account :chat :chat-id chat-id))] chat (r/single (r/get-by-field :account :chat :chat-id chat-id))]
(-> (aget chat "contacts") (-> (aget chat "contacts")
(r/filtered query) (r/filtered query)
(.forEach (fn [object _ _] (.forEach (fn [object _ _]
(aset object "is-in-chat" false)))))))) (r/delete :account object))))))))
(defn- groups [active?]
(r/filtered (r/get-all :account :chat)
(str "group-chat = true && is-active = "
(if active? "true" "false"))))
(defn active-group-chats [] (defn active-group-chats []
(let [results (r/filtered (r/get-all :account :chat) (map
"group-chat = true && is-active = true")] (fn [{:keys [chat-id public-key private-key]}]
(js->clj (.map results (fn [object _ _] {:chat-id chat-id
(aget object "chat-id")))))) :keypair {:private private-key
:public public-key}})
(r/realm-collection->list (groups true))))
(defn set-chat-active [chat-id active?] (defn set-chat-active [chat-id active?]
(r/write :account (r/write :account
@ -144,3 +136,19 @@
(-> (r/get-by-field :account :chat :chat-id chat-id) (-> (r/get-by-field :account :chat :chat-id chat-id)
(r/single) (r/single)
(aset "is-active" active?))))) (aset "is-active" active?)))))
(defn get-property [chat-id property]
(when-let [chat (r/single (r/get-by-field :account :chat :chat-id chat-id))]
(aget chat (name property))))
(defn is-active? [chat-id]
(get-property chat-id :is-active))
(defn removed-at [chat-id]
(get-property chat-id :removed-at))
(defn contact [chat-id id]
(let [contacts (r/cljs-list (chat-contacts chat-id))]
(some (fn [{:keys [identity]}]
(= id identity))
contacts)))

View File

@ -8,32 +8,55 @@
[status-im.constants :as c] [status-im.constants :as c]
[status-im.utils.types :refer [clj->json json->clj]] [status-im.utils.types :refer [clj->json json->clj]]
[status-im.commands.utils :refer [generate-hiccup]] [status-im.commands.utils :refer [generate-hiccup]]
[status-im.utils.logging :as log])) [status-im.utils.logging :as log]
[cljs.reader :as reader]
[clojure.string :as str]))
(defn get-pending-messages [] (defn get-pending-messages! []
(let [collection (-> (r/get-by-fields :account :pending-message :or [[:status :sending] (->> (r/get-all :account :pending-message)
[:status :sent] r/realm-collection->list
[:status :failed]]) (map (fn [{:keys [message-id] :as message}]
(r/sorted :timestamp :desc) (-> message
(r/realm-collection->list))] (update :topics reader/read-string)
(->> collection (assoc :id message-id))))))
(map (fn [{:keys [message-id] :as message}]
(let [message (-> message
(update :message json->clj)
(update :identities json->clj))]
[message-id message])))
(into {}))))
(defn upsert-pending-message! (defn- get-id
[message] [message-id to]
(let [to' (if (and to (str/starts-with? "0x" to))
(subs to 2)
to)
to'' (when to' (subs to' 0 7))
id' (if to''
(str message-id "-" (subs to'' 0 7))
message-id)]
id'))
(defn add-pending-message!
[{:keys [id to group-id message] :as pending-message}]
(r/write :account (r/write :account
(fn [] (fn []
(let [message (-> message (let [{:keys [from topics payload]} message
(update :message clj->json) id' (get-id id to)
(update :identities clj->json))] chat-id (or group-id to)
(r/create :account :pending-message message true))))) message' (-> pending-message
(assoc :id id'
:from from
:message-id id
:chat-id chat-id
:payload payload
:topics (prn-str topics))
(dissoc :message))]
(r/create :account :pending-message message' true)))))
(defn remove-pending-message! [message-id] (defn remove-pending-message!
[{{:keys [message-id]} :payload}]
(r/write :account (r/write :account
(fn [] (fn []
(r/delete :account (r/get-by-field :account :pending-message :message-id message-id))))) (r/delete :account (r/get-by-field :account :pending-message :message-id message-id)))))
(defn remove-all-by-chat [chat-id]
(r/write
:account
(fn []
(r/delete :account
(r/get-by-field :account :pending-message :chat-id chat-id)))))

View File

@ -1,8 +1,6 @@
(ns status-im.models.protocol (ns status-im.models.protocol
(:require [cljs.reader :refer [read-string]] (:require [cljs.reader :refer [read-string]]
[status-im.protocol.state.storage :as s] [status-im.protocol.state.storage :as s]
[status-im.utils.encryption :refer [password-encrypt
password-decrypt]]
[status-im.utils.types :refer [to-edn-string]] [status-im.utils.types :refer [to-edn-string]]
[re-frame.db :refer [app-db]] [re-frame.db :refer [app-db]]
[status-im.db :as db] [status-im.db :as db]

View File

@ -1,11 +1,13 @@
(ns status-im.new-group.handlers (ns status-im.new-group.handlers
(:require [status-im.protocol.api :as api] (:require [status-im.protocol.core :as protocol]
[re-frame.core :refer [after dispatch debug enrich]] [re-frame.core :refer [after dispatch debug enrich]]
[status-im.utils.handlers :refer [register-handler]] [status-im.utils.handlers :refer [register-handler]]
[status-im.components.styles :refer [default-chat-color]] [status-im.components.styles :refer [default-chat-color]]
[status-im.models.chats :as chats] [status-im.models.chats :as chats]
[clojure.string :as s] [clojure.string :as s]
[status-im.utils.handlers :as u])) [status-im.utils.handlers :as u]
[status-im.utils.random :as random]
[taoensso.timbre :refer-macros [debug]]))
(defn deselect-contact (defn deselect-contact
[db [_ id]] [db [_ id]]
@ -19,11 +21,6 @@
(register-handler :select-contact select-contact) (register-handler :select-contact select-contact)
(defn start-group-chat!
[{:keys [selected-contacts] :as db} [_ group-name]]
(let [group-id (api/start-group-chat selected-contacts group-name)]
(assoc db :new-group-id group-id)))
(defn group-name-from-contacts (defn group-name-from-contacts
[{:keys [contacts selected-contacts username]}] [{:keys [contacts selected-contacts username]}]
(->> (select-keys contacts selected-contacts) (->> (select-keys contacts selected-contacts)
@ -33,20 +30,21 @@
(s/join ", "))) (s/join ", ")))
(defn prepare-chat (defn prepare-chat
[{:keys [selected-contacts new-group-id] :as db} [_ group-name]] [{:keys [selected-contacts] :as db} [_ group-name]]
(let [contacts (mapv #(hash-map :identity %) selected-contacts) (let [contacts (mapv #(hash-map :identity %) selected-contacts)
chat-name (if-not (s/blank? group-name) chat-name (if-not (s/blank? group-name)
group-name group-name
(group-name-from-contacts db))] (group-name-from-contacts db))
(assoc db :new-chat {:chat-id new-group-id {:keys [public private]} (protocol/new-keypair!)]
:name chat-name (assoc db :new-chat {:chat-id (random/id)
:color default-chat-color :public-key public
:group-chat true :private-key private
:is-active true :name chat-name
:timestamp (.getTime (js/Date.)) :color default-chat-color
:contacts contacts :group-chat true
:same-author false :is-active true
:same-direction false}))) :timestamp (.getTime (js/Date.))
:contacts contacts})))
(defn add-chat (defn add-chat
[{:keys [new-chat] :as db} _] [{:keys [new-chat] :as db} _]
@ -59,22 +57,45 @@
(chats/create-chat new-chat)) (chats/create-chat new-chat))
(defn show-chat! (defn show-chat!
[{:keys [new-group-id]} _] [{:keys [new-chat]} _]
(dispatch [:navigation-replace :chat new-group-id])) (dispatch [:navigation-replace :chat (:chat-id new-chat)]))
(defn enable-creat-buttion (defn start-listen-group!
[{:keys [new-chat web3 current-public-key]}]
(let [{:keys [chat-id public-key private-key contacts name]} new-chat
identities (mapv :identity contacts)]
(protocol/invite-to-group!
{:web3 web3
:group {:id chat-id
:name name
:contacts (conj identities current-public-key)
:admin current-public-key
:keypair {:public public-key
:private private-key}}
:identities identities
:message {:from current-public-key
:message-id (random/id)}})
(protocol/start-watching-group!
{:web3 web3
:group-id chat-id
:identity current-public-key
:keypair {:public public-key
:private private-key}
:callback #(dispatch [:incoming-message %1 %2])})))
(defn enable-create-button
[db _] [db _]
(assoc db :disable-group-creation false)) (assoc db :disable-group-creation false))
(register-handler :create-new-group (register-handler :create-new-group
(-> start-group-chat! (-> prepare-chat
((enrich prepare-chat))
((enrich add-chat)) ((enrich add-chat))
((after create-chat!)) ((after create-chat!))
((after show-chat!)) ((after show-chat!))
((enrich enable-creat-buttion)))) ((after start-listen-group!))
((enrich enable-create-button))))
(defn disable-creat-button (defn disable-create-button
[db _] [db _]
(assoc db :disable-group-creation true)) (assoc db :disable-group-creation true))
@ -84,18 +105,32 @@
(register-handler :init-group-creation (register-handler :init-group-creation
(after dispatch-create-group) (after dispatch-create-group)
disable-creat-button) disable-create-button)
; todo rewrite
(register-handler :group-chat-invite-received (register-handler :group-chat-invite-received
(u/side-effect! (u/side-effect!
(fn [{:keys [current-public-key] :as db} (fn [{:keys [current-public-key web3] :as db}
[action from group-id identities group-name]] [_ {{:keys [group-id group-name contacts keypair timestamp] :as payload} :payload}]]
(if (chats/chat-exists? group-id) (let [{:keys [private public]} keypair]
(chats/re-join-group-chat db group-id identities group-name) (let [removed-at (chats/removed-at group-id)
(let [contacts (keep (fn [ident] is-active (chats/is-active? group-id)
(when (not= ident current-public-key) contacts' (keep (fn [ident]
{:identity ident})) identities)] (when (not= ident current-public-key)
(dispatch [:add-chat group-id {:name group-name {:identity ident})) contacts)
:group-chat true chat {:name group-name
:contacts contacts}])))))) :group-chat true
:public-key public
:private-key private
:contacts contacts'}]
(when (or (not (chats/chat-exists? group-id))
is-active
(> timestamp removed-at))
(dispatch [:add-chat group-id (assoc chat :is-active true
:timestamp timestamp)])
(when-not is-active
(protocol/start-watching-group!
{:web3 web3
:group-id group-id
:identity current-public-key
:keypair keypair
:callback #(dispatch [:incoming-message %1 %2])}))))))))

View File

@ -3,14 +3,18 @@
(def base {:schema [{:name :account (def base {:schema [{:name :account
:primaryKey :address :primaryKey :address
:properties {:address "string" :properties {:address "string"
:public-key "string" :public-key "string"
:name {:type "string" :optional true} :updates-public-key {:type :string
:phone {:type "string" :optional true} :optional true}
:email {:type "string" :optional true} :updates-private-key {:type :string
:status {:type "string" :optional true} :optional true}
:photo-path "string" :name {:type "string" :optional true}
:last-updated {:type "int" :default 0}}} :phone {:type "string" :optional true}
:email {:type "string" :optional true}
:status {:type "string" :optional true}
:photo-path "string"
:last-updated {:type "int" :default 0}}}
{:name :kv-store {:name :kv-store
:primaryKey :key :primaryKey :key
:properties {:key "string" :properties {:key "string"
@ -25,7 +29,11 @@
:photo-path {:type "string" :optional true} :photo-path {:type "string" :optional true}
:last-updated {:type "int" :default 0} :last-updated {:type "int" :default 0}
:last-online {:type "int" :default 0} :last-online {:type "int" :default 0}
:pending {:type "bool" :default false}}} :pending {:type "bool" :default false}
:public-key {:type :string
:optional true}
:private-key {:type :string
:optional true}}}
{:name :request {:name :request
:properties {:message-id :string :properties {:message-id :string
:chat-id :string :chat-id :string
@ -85,19 +93,21 @@
:user-statuses {:type :list :user-statuses {:type :list
:objectType "user-status"}}} :objectType "user-status"}}}
{:name :pending-message {:name :pending-message
:primaryKey :message-id :primaryKey :id
:properties {:message-id "string" :properties {:id :string
:chat-id {:type "string" :message-id :string
:optional true} :chat-id {:type :string
:message "string" :optional true}
:timestamp "int" :ack? :bool
:status "string" :requires-ack? :bool
:retry-count "int" :from :string
:send-once "bool" :to {:type :string
:identities {:type "string" :optional true}
:optional true} :payload :string
:internal? {:type "bool" :type :string
:optional true}}} :topics :string
:attempts :int
:was-sent? :bool}}
{:name :chat-contact {:name :chat-contact
:properties {:identity "string" :properties {:identity "string"
:is-in-chat {:type "bool" :is-in-chat {:type "bool"
@ -118,7 +128,13 @@
:optional true} :optional true}
:dapp-hash {:type :int :dapp-hash {:type :int
:optional true} :optional true}
:last-message-id "string"}} :removed-at {:type :int
:optional true}
:last-message-id "string"
:public-key {:type :string
:optional true}
:private-key {:type :string
:optional true}}}
{:name :command {:name :command
:primaryKey :chat-id :primaryKey :chat-id
:properties {:chat-id "string" :properties {:chat-id "string"

View File

@ -4,13 +4,9 @@
[status-im.components.react :refer [show-image-picker]] [status-im.components.react :refer [show-image-picker]]
[status-im.utils.image-processing :refer [img->base64]] [status-im.utils.image-processing :refer [img->base64]]
[status-im.i18n :refer [label]] [status-im.i18n :refer [label]]
[status-im.utils.handlers :as u] [status-im.utils.handlers :as u :refer [get-hashtags]]
[clojure.string :as str])) [clojure.string :as str]))
(defn get-hashtags [status]
(let [hashtags (map #(str/lower-case (subs % 1)) (re-seq #"#[^ !?,;:.]+" status))]
(or hashtags [])))
(defn message-user [identity] (defn message-user [identity]
(when identity (when identity
(dispatch [:navigate-to :chat identity]))) (dispatch [:navigate-to :chat identity])))
@ -59,4 +55,4 @@
0 (dispatch [:show-profile-photo-capture]) 0 (dispatch [:show-profile-photo-capture])
1 (dispatch [:open-image-picker]) 1 (dispatch [:open-image-picker])
:default)) :default))
:cancel-text (label :t/image-source-cancel)})))) :cancel-text (label :t/image-source-cancel)}))))

View File

@ -16,8 +16,8 @@
my-profile-icon]] my-profile-icon]]
[status-im.components.status-bar :refer [status-bar]] [status-im.components.status-bar :refer [status-bar]]
[status-im.profile.styles :as st] [status-im.profile.styles :as st]
[status-im.profile.handlers :refer [get-hashtags [status-im.utils.handlers :refer [get-hashtags]]
message-user [status-im.profile.handlers :refer [message-user
update-profile]] update-profile]]
[status-im.components.qr-code :refer [qr-code]] [status-im.components.qr-code :refer [qr-code]]
[status-im.utils.phone-number :refer [format-phone-number [status-im.utils.phone-number :refer [format-phone-number

View File

@ -0,0 +1,22 @@
(ns status-im.protocol.ack
(:require [status-im.protocol.web3.delivery :as d]
[status-im.protocol.web3.filtering :as f]))
(defn check-ack!
[web3
from
{:keys [type requires-ack? message-id ack? group-id]}
identity]
(when (and requires-ack? (not ack?))
(let [message {:from identity
:to from
:message-id message-id
:topics [f/status-topic]
:type type
:ack? true
:payload {:type type
:ack? true
:group-id group-id}}]
(d/add-pending-message! web3 message)))
(when ack?
(d/remove-pending-message! web3 message-id from)))

View File

@ -0,0 +1,43 @@
(ns status-im.protocol.chat
(:require [cljs.spec :as s]
[status-im.protocol.message :as m]
[status-im.protocol.web3.filtering :as f]
[status-im.protocol.web3.delivery :as d]
[taoensso.timbre :refer-macros [debug]]
[status-im.protocol.validation :refer-macros [valid?]]))
(def message-defaults
{:topics [f/status-topic]})
(s/def ::timestamp int?)
(s/def ::user-message
(s/merge
:protocol/message
(s/keys :req-un [:message/to :chat-message/payload])))
(defn send!
[{:keys [web3 message]}]
{:pre [(valid? ::user-message message)]}
(let [message' (merge message-defaults
(assoc message
:type :message
:requires-ack? true))]
(debug :send-user-message message')
(d/add-pending-message! web3 message')))
(s/def ::seen-message
(s/merge :protocol/message (s/keys :req-un [:message/to])))
(defn send-seen!
[{:keys [web3 message]}]
{:pre [(valid? ::seen-message message)]}
(debug :send-seen message)
(d/add-pending-message!
web3
(merge message-defaults
(-> message
(assoc
:type :seen
:requires-ack? false)
(assoc-in [:payload :group-id] (:group-id message))
(dissoc :group-id)))))

View File

@ -0,0 +1,105 @@
(ns status-im.protocol.core
(:require status-im.protocol.message
[status-im.protocol.web3.utils :as u]
[status-im.protocol.web3.filtering :as f]
[status-im.protocol.web3.delivery :as d]
[taoensso.timbre :refer-macros [debug]]
[status-im.protocol.validation :refer-macros [valid?]]
[status-im.protocol.web3.utils :as u]
[status-im.protocol.chat :as chat]
[status-im.protocol.group :as group]
[status-im.protocol.listeners :as l]
[status-im.protocol.encryption :as e]
[status-im.protocol.discoveries :as discoveries]
[cljs.spec :as s]
[status-im.utils.random :as random]))
;; user
(def send-message! chat/send!)
(def send-seen! chat/send-seen!)
(def reset-pending-messages! d/reset-pending-messages!)
;; group
(def start-watching-group! group/start-watching-group!)
(def stop-watching-group! group/stop-watching-group!)
(def send-group-message! group/send!)
(def invite-to-group! group/invite!)
(def update-group! group/update-group!)
(def remove-from-group! group/remove-identity!)
(def add-to-group! group/add-identity!)
(def leave-group-chat! group/leave!)
;; encryption
;; todo move somewhere, encryption functions shouldn't be there
(def new-keypair! e/new-keypair!)
;; discoveries
(def watch-user! discoveries/watch-user!)
(def contact-request! discoveries/contact-request!)
(def watch-hashtags! discoveries/watch-hashtags!)
(def broadcats-profile! discoveries/broadcats-profile!)
(def broadcats-discoveries! discoveries/broadcats-discoveries!)
;; initialization
(s/def ::rpc-url string?)
(s/def ::identity string?)
(s/def :message/chat-id string?)
(s/def ::group (s/keys :req-un [:message/chat-id :message/keypair]))
(s/def ::groups (s/* ::group))
(s/def ::callback fn?)
(s/def ::contact (s/keys :req-un [::identity :message/keypair]))
(s/def ::contacts (s/* ::contact))
(s/def ::profile-keypair :message/keypair)
(s/def ::options
(s/merge
(s/keys :req-un [::rpc-url ::identity ::groups ::profile-keypair
::callback :discoveries/hashtags ::contacts])
::d/delivery-options))
(def stop-watching-all! f/remove-all-filters!)
(defn init-whisper!
[{:keys [rpc-url identity groups callback
hashtags contacts profile-keypair pending-messages]
:as options}]
{:pre [(valid? ::options options)]}
(debug :init-whisper)
(stop-watching-all!)
(let [web3 (u/make-web3 rpc-url)
listener-options {:web3 web3
:identity identity}]
;; start listening to user's inbox
(f/add-filter!
web3
{:to identity
:topics [f/status-topic]}
(l/message-listener (assoc listener-options :callback callback)))
;; start listening to groups
(doseq [{:keys [chat-id keypair]} groups]
(f/add-filter!
web3
{:topics [chat-id]}
(l/message-listener (assoc listener-options :callback callback
:keypair keypair))))
;; start listening to discoveries
(watch-hashtags! {:web3 web3
:hashtags hashtags
:callback callback})
;; start listening to profiles
(doseq [{:keys [identity keypair]} contacts]
(watch-user! {:web3 web3
:identity identity
:keypair keypair
:callback callback}))
(d/set-pending-mesage-callback! callback)
(let [online-message #(discoveries/send-online!
{:web3 web3
:message {:from identity
:message-id (random/id)
:keypair profile-keypair}})]
(d/run-delivery-loop!
web3
(assoc options :online-message online-message)))
(doseq [pending-message pending-messages]
(d/add-prepeared-pending-message! web3 pending-message))
web3))

View File

@ -0,0 +1,148 @@
(ns status-im.protocol.discoveries
(:require
[taoensso.timbre :refer-macros [debug]]
[status-im.protocol.web3.utils :as u]
[status-im.protocol.web3.delivery :as d]
[status-im.protocol.web3.filtering :as f]
[status-im.protocol.listeners :as l]
[cljs.spec :as s]
[status-im.protocol.validation :refer-macros [valid?]]
[status-im.utils.random :as random]))
(def discovery-topic "status-discovery")
(def discovery-topic-prefix "status-discovery-")
(def discovery-hashtag-prefix "status-hashtag-")
(defn- add-hashtag-prefix [hashtag]
(str discovery-hashtag-prefix hashtag))
(defn- make-discovery-topic [identity]
(str discovery-topic-prefix identity))
(s/def :send-online/message
(s/merge :protocol/message
(s/keys :req-un [:message/keypair])))
(s/def :send-online/options
(s/keys :req-un [:options/web3 :send-online/message]))
(defn send-online!
[{:keys [web3 message] :as options}]
{:pre [(valid? :send-online/options options)]}
(debug :send-online)
(let [message' (merge
message
{:requires-ack? false
:type :online
:payload {:timestamp (u/timestamp)}
:topics [(make-discovery-topic (:from message))]})]
(d/add-pending-message! web3 message')))
(s/def ::identity :message/from)
(s/def :watch-user/options
(s/keys :req-un [:options/web3 :message/keypair ::identity ::callback]))
(defn watch-user!
[{:keys [web3 identity] :as options}]
{:pre [(valid? :watch-user/options options)]}
(f/add-filter!
web3
{:from identity
:topics [(make-discovery-topic identity)]}
(l/message-listener (dissoc options :identity))))
(s/def :contact-request/contact map?)
(s/def :contact-request/payload
(s/merge :message/payload
(s/keys :req-un [:contact-request/contact :message/keypair])))
(s/def :contact-request/message
(s/merge :protocol/message
(s/keys :req-un [:message/to :contact-request/payload])))
(defn contact-request!
[{:keys [web3 message]}]
{:pre [(valid? :contact-request/message message)]}
(d/add-pending-message!
web3
(assoc message :type :contact-request
:requires-ack? true
:topics [f/status-topic])))
(defonce watched-hashtag-topics (atom nil))
(defn- hashtags->topics
"Create topics from hashtags."
[hashtags]
(->> hashtags
(map (fn [tag]
[tag [(add-hashtag-prefix tag) discovery-topic]]))
(into {})))
(s/def :discoveries/hashtags (s/every string? :kind-of set?))
(defn stop-watching-hashtags!
[web3]
(doseq [topics @watched-hashtag-topics]
(f/remove-filter! web3 topics)))
(s/def ::callback fn?)
(s/def :watch-hashtags/options
(s/keys :req-un [:options/web3 :discoveries/hashtags ::callback]))
(defn watch-hashtags!
[{:keys [web3 hashtags] :as options}]
{:pre [(valid? :watch-hashtags/options options)]}
(debug :watch-hashtags hashtags)
(stop-watching-hashtags! web3)
(let [hashtag-topics (vals (hashtags->topics hashtags))]
(reset! watched-hashtag-topics hashtag-topics)
(doseq [topics hashtag-topics]
(f/add-filter! web3 {:topics topics} (l/message-listener options)))))
(s/def ::status (s/nilable string?))
(s/def ::profile (s/keys :req-un [::status]))
(s/def :profile/payload
(s/merge :message/payload (s/keys :req-un [::profile])))
(s/def :profile/message
(s/merge :protocol/message (s/keys :req-un [:message/keypair
:profile/payload])))
(s/def :broadcast-profile/options
(s/keys :req-un [:profile/message :options/web3]))
(defn broadcats-profile!
[{:keys [web3 message] :as options}]
{:pre [(valid? :broadcast-profile/options options)]}
(debug :broadcasting-status)
(d/add-pending-message!
web3
(-> message
(assoc :type :profile
:topics [(make-discovery-topic (:from message))])
(assoc-in [:payload :timestamp] (u/timestamp))
(assoc-in [:payload :content :profile]
(get-in message [:payload :profile]))
(update :payload dissoc :profile))))
(s/def :status/payload
(s/merge :message/payload (s/keys :req-un [::status])))
(s/def :status/message
(s/merge :protocol/message (s/keys :req-un [:status/payload])))
(s/def :broadcast-hasthags/options
(s/keys :req-un [:discoveries/hashtags :status/message :options/web3]))
(defn broadcats-discoveries!
[{:keys [web3 hashtags message] :as options}]
{:pre [(valid? :broadcast-hasthags/options options)]}
(debug :broadcasting-status)
(let [discovery-id (random/id)]
(doseq [[tag hashtag-topics] (hashtags->topics hashtags)]
(d/add-pending-message!
web3
(-> message
(assoc :type :discovery
:topics hashtag-topics)
(assoc-in [:payload :tag] tag)
(assoc-in [:payload :hashtags] (vec hashtags))
(assoc-in [:payload :discovery-id] discovery-id)
(update :message-id str tag))))))

View File

@ -0,0 +1,21 @@
(ns status-im.protocol.encryption
(:require [cljsjs.chance]
[cljsjs.eccjs]))
(def default-curve 384)
(defn new-keypair!
"Returns {:private \"private key\" :public \"public key\""
[]
(let [{:keys [enc dec]}
(-> (.generate js/ecc (.-ENC_DEC js/ecc) default-curve)
(js->clj :keywordize-keys true))]
{:private dec
:public enc}))
(defn encrypt [public-key content]
(.encrypt js/ecc public-key content))
(defn decrypt [private-key content]
(.decrypt js/ecc private-key content))

View File

@ -0,0 +1,117 @@
(ns status-im.protocol.group
(:require
[status-im.protocol.message :as m]
[status-im.protocol.web3.delivery :as d]
[status-im.protocol.web3.utils :as u]
[cljs.spec :as s]
[taoensso.timbre :refer-macros [debug]]
[status-im.protocol.validation :refer-macros [valid?]]
[status-im.protocol.web3.filtering :as f]
[status-im.protocol.listeners :as l]))
(defn prepare-mesage
[{:keys [message group-id keypair new-keypair type]}]
(let [message' (-> message
(update :payload assoc
:group-id group-id
:type type
:timestamp (u/timestamp))
(assoc :topics [group-id]
:requires-ack? true
:keypair keypair
:type type))]
(if new-keypair
(assoc message' :new-keypair keypair)
message')))
(defn- send-group-message!
[{:keys [web3] :as opts} type]
(let [message (-> opts
(assoc :type type)
(prepare-mesage))]
(debug :send-group-message message)
(d/add-pending-message! web3 message)))
(s/def ::group-message
(s/merge :protocol/message (s/keys :req-un [:chat-message/payload])))
(defn send!
[{:keys [keypair message] :as options}]
{:pre [(valid? :message/keypair keypair)
(valid? ::group-message message)]}
(send-group-message! options :group-message))
(defn leave!
[options]
(send-group-message! options :leave-group))
(defn add-identity!
[{:keys [identity] :as options}]
{:pre [(valid? :message/to identity)]}
(let [options' (assoc-in options
[:message :payload :identity]
identity)]
(send-group-message! options' :add-group-identity)))
(defn remove-identity!
[{:keys [identity] :as options}]
{:pre [(valid? :message/to identity)]}
(let [options' (assoc-in options
[:message :payload :identity]
identity)]
(send-group-message! options' :remove-group-identity)))
(s/def :group/admin :message/from)
(s/def ::identities (s/* string?))
(s/def :group/name string?)
(s/def :group/id string?)
(s/def :group/contacts (s/* string?))
(s/def ::group
(s/keys :req-un [:group/name :group/id :group/contacts :message/keypair]))
(s/def :invite/options
(s/keys :req-un [:options/web3 :protocol/message ::group ::identities]))
(defn- notify-about-group!
[type {:keys [web3 message identities group]
:as options}]
{:pre [(valid? :invite/options options)]}
(let [{:keys [id admin name keypair contacts]} group
message' (-> message
(assoc :topics [f/status-topic]
:requires-ack? true
:type type)
(update :payload assoc
:timestamp (u/timestamp)
:group-id id
:group-admin admin
:group-name name
:keypair keypair
:contacts contacts
:type type))]
(doseq [identity identities]
(d/add-pending-message! web3 (assoc message' :to identity)))))
(defn invite!
[options]
(notify-about-group! :group-invitation options))
;; todo notify users about keypair change when someone leaves group (from admin)
(defn update-group!
[options]
(notify-about-group! :update-group options))
(defn stop-watching-group!
[{:keys [web3 group-id]}]
{:pre [(valid? :message/chat-id group-id)]}
(f/remove-filter! web3 [group-id]))
(defn start-watching-group!
[{:keys [web3 group-id keypair callback identity]}]
(f/add-filter!
web3
{:topics [group-id]}
(l/message-listener {:web3 web3
:identity identity
:callback callback
:keypair keypair})))

View File

@ -3,26 +3,75 @@
(ns status-im.protocol.handlers (ns status-im.protocol.handlers
(:require [status-im.utils.handlers :as u] (:require [status-im.utils.handlers :as u]
[status-im.utils.logging :as log] [status-im.utils.logging :as log]
[status-im.protocol.api :as api]
[re-frame.core :refer [dispatch after]] [re-frame.core :refer [dispatch after]]
[status-im.utils.handlers :refer [register-handler]] [status-im.utils.handlers :refer [register-handler]]
[status-im.models.contacts :as contacts] [status-im.models.contacts :as contacts]
[status-im.models.messages :as messages] [status-im.models.messages :as messages]
[status-im.models.pending-messages :as pending-messages] [status-im.models.pending-messages :as pending-messages]
[status-im.models.chats :as chats] [status-im.models.chats :as chats]
[status-im.protocol.api :refer [init-protocol]]
[status-im.protocol.protocol-handler :refer [make-handler]]
[status-im.models.protocol :refer [update-identity [status-im.models.protocol :refer [update-identity
set-initialized]] set-initialized]]
[status-im.protocol.core :as protocol]
[status-im.constants :refer [text-content-type]] [status-im.constants :refer [text-content-type]]
[status-im.i18n :refer [label]] [status-im.i18n :refer [label]]
[status-im.utils.random :as random])) [status-im.utils.random :as random]
[taoensso.timbre :refer-macros [debug]]))
(register-handler :initialize-protocol (register-handler :initialize-protocol
(fn [db [_ current-account-id]]
(let [{:keys [public-key status updates-public-key
updates-private-key]}
(get-in db [:accounts current-account-id])]
(let [groups (chats/active-group-chats)
w3 (protocol/init-whisper!
{:rpc-url "http://localhost:8545"
:identity public-key
:groups groups
:callback #(dispatch [:incoming-message %1 %2])
:ack-not-received-s-interval 17
:default-ttl 15
:send-online-s-interval 180
:ttl {}
:max-attempts-number 3
:delivery-loop-ms-interval 500
:profile-keypair {:public updates-public-key
:private updates-private-key}
:hashtags (u/get-hashtags status)
:pending-messages (pending-messages/get-pending-messages!)
:contacts (keep (fn [{:keys [whisper-identity
public-key
private-key]}]
(when (and public-key private-key)
{:identity whisper-identity
:keypair {:public public-key
:private private-key}}))
(contacts/get-contacts))})]
(assoc db :web3 w3)))))
(register-handler :incoming-message
(u/side-effect! (u/side-effect!
(fn [db [_ current-account-id]] (fn [_ [_ type {:keys [payload] :as message}]]
(let [current-account (get-in db [:accounts current-account-id])] (debug :incoming-message type)
(init-protocol current-account (make-handler db)))))) (case type
:message (dispatch [:received-protocol-message! message])
:group-message (dispatch [:received-protocol-message! message])
:ack (when (#{:message :group-message} (:type payload))
(dispatch [:message-delivered message]))
:seen (dispatch [:message-seen message])
:group-invitation (dispatch [:group-chat-invite-received message])
:leave-group (dispatch [:participant-left-group message])
:contact-request (dispatch [:contact-request-received message])
:discovery (dispatch [:discovery-response-received message])
:profile (dispatch [:contact-update-received message])
:online (dispatch [:contact-online-received message])
:pending (dispatch [:pending-message-upsert message])
:sent (let [{:keys [to id group-id]} message
message' {:from to
:payload {:message-id id
:group-id group-id}}]
(dispatch [:message-sent message']))
(debug "Unknown message type" type)))))
(register-handler :protocol-initialized (register-handler :protocol-initialized
(fn [db [_ identity]] (fn [db [_ identity]]
@ -43,9 +92,9 @@
:content (str (or contact-name from) " " (label :t/received-invitation)) :content (str (or contact-name from) " " (label :t/received-invitation))
:content-type text-content-type}))) :content-type text-content-type})))
(defn participant-invited-to-group-message [chat-id identity from message-id] (defn participant-invited-to-group-message [chat-id current-identity identity from message-id]
(let [inviter-name (:name (contacts/contact-by-identity from)) (let [inviter-name (:name (contacts/contact-by-identity from))
invitee-name (if (= identity (api/my-identity)) invitee-name (if (= identity current-identity)
(label :t/You) (label :t/You)
(:name (contacts/contact-by-identity identity)))] (:name (contacts/contact-by-identity identity)))]
(messages/save-message chat-id {:from "system" (messages/save-message chat-id {:from "system"
@ -94,42 +143,94 @@
(register-handler :participant-left-group (register-handler :participant-left-group
(u/side-effect! (u/side-effect!
(fn [_ [action from group-id message-id]] (fn [{:keys [current-public-key]}
(log/debug action message-id from group-id) [_ {:keys [from]
(when-not (= (api/my-identity) from) {:keys [group-id message-id timestamp]} :payload}]]
(participant-left-group-message group-id from message-id))))) (when (and (not= current-public-key from)
(chats/is-active? group-id)
(> timestamp (chats/get-property group-id :timestamp)))
(participant-left-group-message group-id from message-id)
(dispatch [::remove-identity-from-chat group-id from])
(dispatch [::remove-identity-from-chat! group-id from])))))
(register-handler ::remove-identity-from-chat
(fn [db [_ chat-id id]]
(update-in db [:chats chat-id :contacts]
#(remove (fn [{:keys [identity]}]
(= identity id)) %))))
(register-handler ::remove-identity-from-chat!
(u/side-effect!
(fn [_ [_ group-id identity]]
(chats/chat-remove-participants group-id [identity]))))
(register-handler :participant-invited-to-group (register-handler :participant-invited-to-group
(u/side-effect! (u/side-effect!
(fn [_ [action from group-id identity message-id]] (fn [{:keys [current-public-key]} [action from group-id identity message-id]]
(log/debug action message-id from group-id identity) (log/debug action message-id from group-id identity)
(participant-invited-to-group-message group-id identity from message-id)))) (participant-invited-to-group-message group-id current-public-key identity from message-id)
;; todo uncomment
#_(dispatch [:add-contact-to-group! group-id identity]))))
(register-handler :add-contact-to-group!
(u/side-effect!
(fn [_ [_ group-id identity]]
(when-not (chats/contact group-id identity)
(dispatch [::add-contact group-id identity])
(dispatch [::store-contact! group-id identity])))))
(register-handler ::add-contact
(fn [db [_ group-id identity]]
(update-in db [:chats group-id :contacts] conj {:identity identity})))
(register-handler ::store-contact!
(u/side-effect!
(fn [_ [_ group-id identity]]
(chats/chat-add-participants group-id [identity]))))
(defn save-message-status! [status] (defn save-message-status! [status]
(fn [_ [_ {:keys [message-id whisper-identity]}]] (fn [_ [_
{:keys [from]
{:keys [message-id group-id]} :payload}]]
(when-let [message (messages/get-message message-id)] (when-let [message (messages/get-message message-id)]
(let [message (if whisper-identity (let [group? (boolean group-id)
message (if (and group? (not= status :sent))
(update-in message (update-in message
[:user-statuses whisper-identity] [:user-statuses from]
(fn [{old-status :status}] (fn [{old-status :status}]
{:id (random/id) {:id (random/id)
:whisper-identity whisper-identity :whisper-identity from
:status (if (= (keyword old-status) :seen) :status (if (= (keyword old-status) :seen)
old-status old-status
status)})) status)}))
(assoc message :message-status status))] (assoc message :message-status status))]
(messages/update-message! message))))) (messages/update-message! message)))))
(defn update-message-status [status] (defn update-message-status [status]
(fn [db [_ {:keys [message-id whisper-identity]}]] (fn [db
(let [db-key (if whisper-identity [_ {:keys [from]
[:message-user-statuses message-id whisper-identity] {:keys [message-id group-id]} :payload}]]
[:message-statuses message-id]) (if (chats/is-active? (or group-id from))
current-status (get-in db db-key)] (let [group? (boolean group-id)
(if-not (= :seen current-status) status-path (if (and group? (not= status :sent))
(assoc-in db db-key {:whisper-identity whisper-identity [:message-user-statuses message-id from]
:status status}) [:message-statuses message-id])
db)))) current-status (get-in db status-path)]
(if-not (= :seen current-status)
(assoc-in db status-path {:whisper-identity from
:status status})
db))
db)))
(defn remove-pending-message
[_ [_ message]]
(dispatch [:pending-message-remove message]))
(register-handler :message-delivered
[(after (save-message-status! :delivered))
(after remove-pending-message)]
(update-message-status :delivered))
(register-handler :message-failed (register-handler :message-failed
(after (save-message-status! :failed)) (after (save-message-status! :failed))
@ -139,10 +240,6 @@
(after (save-message-status! :sent)) (after (save-message-status! :sent))
(update-message-status :sent)) (update-message-status :sent))
(register-handler :message-delivered
(after (save-message-status! :delivered))
(update-message-status :delivered))
(register-handler :message-seen (register-handler :message-seen
[(after (save-message-status! :seen)) [(after (save-message-status! :seen))
(after (fn [_ [_ {:keys [chat-id]}]] (after (fn [_ [_ {:keys [chat-id]}]]
@ -150,16 +247,39 @@
(update-message-status :seen)) (update-message-status :seen))
(register-handler :pending-message-upsert (register-handler :pending-message-upsert
(u/side-effect! (after
(fn [_ [_ pending-message]] (fn [_ [_ {:keys [type id] :as pending-message}]]
(pending-messages/upsert-pending-message! pending-message)))) (pending-messages/add-pending-message! pending-message)
(when (#{:message :group-message} type)
(messages/update-message! {:message-id id
:delivery-status :pending}))))
(fn [db [_ {:keys [type id to groupd-id]}]]
(if (#{:message :group-message} type)
(let [chat-id (or groupd-id to)
current-status (get-in db [:message-status chat-id id])]
(if-not (= :seen current-status)
(assoc-in db [:message-status chat-id id] :pending)
db))
db)))
(register-handler :pending-message-remove (register-handler :pending-message-remove
(u/side-effect! (u/side-effect!
(fn [_ [_ message-id]] (fn [_ [_ message]]
(pending-messages/remove-pending-message! message-id)))) (pending-messages/remove-pending-message! message))))
(register-handler :send-transaction! (register-handler :contact-request-received
(u/side-effect! (u/side-effect!
(fn [_ [_ amount message]] (fn [_ [_ {:keys [from payload]}]]
(println :send-transacion! amount message)))) (when from
(let [{{:keys [name profile-image address]} :contact
{:keys [public private]} :keypair} payload
contact {:whisper-identity from
:public-key public
:private-key private
:address address
:photo-path profile-image
:name name
:pending true}]
(dispatch [:watch-contact contact])
(dispatch [:add-contacts [contact]]))))))

View File

@ -0,0 +1,39 @@
(ns status-im.protocol.listeners
(:require [cljs.reader :as r]
[status-im.protocol.ack :as ack]
[status-im.protocol.web3.utils :as u]
[status-im.protocol.encryption :as e]
[taoensso.timbre :refer-macros [debug]]))
(defn- parse-payload [payload]
(debug :parse-payload)
(r/read-string (u/to-utf8 payload)))
(defn- parse-content [key {:keys [content]} was-encrypted?]
(debug :parse-content
"Key exitsts:" (not (nil? key))
"Content exists:" (not (nil? content)))
(if (and (not was-encrypted?) key content)
(r/read-string (e/decrypt key content))
content))
(defn message-listener
[{:keys [web3 identity callback keypair]}]
(fn [error js-message]
;; todo handle error
(when error
(debug :listener-error error))
(when-not error
(debug :message-received)
(let [{:keys [from payload to] :as message}
(js->clj js-message :keywordize-keys true)]
(when-not (= identity from)
(let [{:keys [type ack?] :as payload'}
(parse-payload payload)
content (parse-content (:private keypair) payload' (not= "0x0" to))
payload'' (assoc payload' :content content)
message' (assoc message :payload payload'')]
(callback (if ack? :ack type) message')
(ack/check-ack! web3 from payload'' identity)))))))

View File

@ -0,0 +1,47 @@
(ns status-im.protocol.message
(:require [cljs.spec :as s]))
(s/def :message/ttl (s/and int? pos?))
(s/def :message/from string?)
(s/def :message/to string?)
(s/def :message/message-id string?)
(s/def :message/requires-ack? boolean?)
(s/def :keypair/private string?)
(s/def :keypair/public string?)
(s/def :message/keypair (s/keys :req-un [:keypair/private
:keypair/public]))
(s/def :message/topics (s/* string?))
(s/def :payload/content (s/or :string-message string?
:command map?))
(s/def :payload/content-type string?)
(s/def :payload/timestamp (s/and int? pos?))
(s/def :payload/new-keypair :message/keypair)
(s/def :group-message/type
#{:group-message :group-invitation :add-group-identity
:remove-group-identity :leave-group :update-group})
(s/def :discovery-message/type #{:online :status :discovery :contact-request})
(s/def :message/type
(s/or :group :group-message/type
:discovery :discovery-message/type
:user #{:message}))
(s/def :message/payload
(s/keys :opt-un [:message/type
:payload/content
:payload/content-type
:payload/new-keypair
:payload/timestamp]))
(s/def :protocol/message
(s/keys :req-un [:message/from :message/message-id]
:opt-un [:message/to :message/topics :message/requires-ack?
:message/keypair :message/ttl :message/payload]))
(s/def :chat-message/payload
(s/keys :req-un [:payload/content :payload/content-type :payload/timestamp]))
(s/def :options/web3 #(not (nil? %)))

View File

@ -5,64 +5,3 @@
[status-im.models.protocol :refer [stored-identity]] [status-im.models.protocol :refer [stored-identity]]
[status-im.persistence.simple-kv-store :as kv] [status-im.persistence.simple-kv-store :as kv]
[status-im.models.chats :refer [active-group-chats]])) [status-im.models.chats :refer [active-group-chats]]))
(defn make-handler [db]
{:ethereum-rpc-url ethereum-rpc-url
:identity (stored-identity db)
;; :active-group-ids is never used in protocol
:active-group-ids (active-group-chats)
:storage kv/kv-store
:handler (fn [{:keys [event-type] :as event}]
(case event-type
:initialized (let [{:keys [identity]} event]
(dispatch [:protocol-initialized identity]))
:message-received (let [{:keys [from to payload]} event]
(dispatch [:received-message (assoc payload :chat-id from
:from from
:to to)]))
:contact-request (let [{:keys [from payload]} event]
(dispatch [:contact-request-received (assoc payload :from from)]))
:message-delivered (let [{:keys [from message-id]} event]
(dispatch [:message-delivered {:whisper-identity from
:message-id message-id}]))
:message-seen (let [{:keys [from message-id]} event]
(dispatch [:message-seen {:whisper-identity from
:message-id message-id}]))
:message-failed (let [{:keys [chat-id message-id]} event]
(dispatch [:message-failed {:chat-id chat-id
:message-id message-id}]))
:message-sent (let [{:keys [chat-id message-id] :as data} event]
(dispatch [:message-sent {:chat-id chat-id
:message-id message-id}]))
:user-discovery-keypair (let [{:keys [from]} event]
(dispatch [:contact-keypair-received from]))
:pending-message-upsert (let [{message :message} event]
(dispatch [:pending-message-upsert message]))
:pending-message-remove (let [{:keys [message-id]} event]
(dispatch [:pending-message-remove message-id]))
:new-group-chat (let [{:keys [from group-id identities group-name]} event]
(dispatch [:group-chat-invite-received from group-id identities group-name]))
:new-group-message (let [{from :from
group-id :group-id
payload :payload} event]
(dispatch [:received-message (assoc payload
:chat-id group-id
:from from)]))
:group-chat-invite-acked (let [{:keys [from group-id ack-message-id]} event]
(dispatch [:group-chat-invite-acked from group-id ack-message-id]))
:group-new-participant (let [{:keys [group-id identity from message-id]} event]
(dispatch [:participant-invited-to-group from group-id identity message-id]))
:group-removed-participant (let [{:keys [group-id identity from message-id]} event]
(dispatch [:participant-removed-from-group from group-id identity message-id]))
:removed-from-group (let [{:keys [group-id from message-id]} event]
(dispatch [:you-removed-from-group from group-id message-id]))
:participant-left-group (let [{:keys [group-id from message-id]} event]
(dispatch [:participant-left-group from group-id message-id]))
:discover-response (let [{:keys [from payload]} event]
(dispatch [:discovery-response-received from payload]))
:contact-update (let [{:keys [from payload]} event]
(dispatch [:contact-update-received from payload]))
:contact-online (let [{:keys [from payload]} event]
(dispatch [:contact-online-received from payload]))
(log/info "Don't know how to handle" event-type)))})

View File

@ -0,0 +1,12 @@
(ns status-im.protocol.validation)
(defn- fline [and-form] (:line (meta and-form)))
(defmacro valid? [spec x]
`(let [v?# (cljs.spec/valid? ~spec ~x)]
(when-not v?#
(let [explanation# (cljs.spec/explain-str ~spec ~x)]
(taoensso.timbre/log! :error :p
[explanation#]
~{:?line (fline &form)})))
v?#))

View File

@ -0,0 +1,2 @@
(ns status-im.protocol.validation
(:require-macros [status-im.protocol.validation :as macros]))

View File

@ -0,0 +1,193 @@
(ns status-im.protocol.web3.delivery
(:require-macros [cljs.core.async.macros :refer [go-loop go]])
(:require [cljs.core.async :refer [<! timeout]]
[status-im.protocol.web3.transport :as t]
[status-im.protocol.web3.utils :as u]
[status-im.protocol.encryption :as e]
[cljs.spec :as s]
[taoensso.timbre :refer-macros [debug] :as timbre]
[status-im.protocol.validation :refer-macros [valid?]]
[clojure.set :as set]))
(defonce loop-state (atom nil))
(defonce messages (atom {}))
(defn prepare-message
[{:keys [payload keypair to] :as message}]
(debug :prepare-message!)
(let [{:keys [public]} keypair
content (:content payload)
content' (if (and (not to) public content)
(e/encrypt public (prn-str content))
content)
payload' (-> message
(select-keys [:message-id :requires-ack? :type])
(merge payload)
(assoc :content content')
prn-str
u/from-utf8)]
(-> message (select-keys [:from :to :topics :ttl])
(assoc :payload payload'))))
(s/def :shh/pending-message
(s/keys :req-un [:message/from :shh/payload :message/topics]
:opt-un [:message/ttl :message/to]))
(defonce pending-mesage-callback (atom nil))
(defonce recipient->pending-message (atom {}))
(defn set-pending-mesage-callback!
[callback]
(reset! pending-mesage-callback callback))
(defn add-pending-message!
[web3 {:keys [type message-id requires-ack? to ack?] :as message}]
{:pre [(valid? :protocol/message message)]}
(go
(debug :add-pending-message!)
;; encryption can take some time, better to run asynchronously
(let [message' (prepare-message message)]
(when (valid? :shh/pending-message message')
(let [group-id (get-in message [:payload :group-id])
pending-message {:id message-id
:ack? (boolean ack?)
:message message'
:to to
:type type
:group-id group-id
:requires-ack? (boolean requires-ack?)
:attempts 0
:was-sent? false}]
(when (and @pending-mesage-callback requires-ack?)
(@pending-mesage-callback :pending pending-message))
(swap! messages assoc-in [web3 message-id to] pending-message)
(when to
(swap! recipient->pending-message
update to set/union #{[web3 message-id to]})))))))
(s/def :delivery/pending-message
(s/keys :req-un [:message/to :message/from :shh/payload
:message/requires-ack? :payload/ack? ::id :message/topics
::attempts ::was-sent?]))
(defn add-prepeared-pending-message!
[web3 {:keys [id to] :as pending-message}]
{:pre [(valid? :delivery/pending-message pending-message)]}
(debug :add-prepeared-pending-message!)
(let [message (select-keys pending-message [:from :to :topics :payload])
pending-message' (assoc pending-message :message message)]
(swap! messages assoc-in [web3 id to] pending-message')
(when to
(swap! recipient->pending-message
update to set/union #{[web3 id to]}))))
(defn remove-pending-message! [web3 id to]
(swap! messages update-in [web3 id] dissoc to)
(when to
(swap! recipient->pending-message
update to set/difference #{[web3 id to]})))
(defn message-was-sent! [web3 id to]
(let [messages' (swap! messages update-in [web3 id to]
(fn [message]
(assoc message :was-sent? true
:attemps 1)))]
(when @pending-mesage-callback
(@pending-mesage-callback :sent (get-in messages' [web3 id to])))))
(defn attempt-was-made! [web3 id to]
(debug :attempt-was-made id)
(swap! messages update-in [web3 id to]
(fn [{:keys [attempts] :as data}]
(assoc data :attempts (inc attempts)
:last-attempt (u/timestamp)))))
(defn delivery-callback
[web3 {:keys [id requires-ack? to]}]
(fn [error _]
(when error (timbre/error :shh-post-error error))
(when-not error
(debug :delivery-callback)
(message-was-sent! web3 id to)
(when-not requires-ack?
(remove-pending-message! web3 id to)))))
(s/def ::pos-int (s/and pos? int?))
(s/def ::delivery-loop-ms-interval ::pos-int)
(s/def ::ack-not-received-s-interval ::pos-int)
(s/def ::max-attempts-number ::pos-int)
(s/def ::default-ttl ::pos-int)
(s/def ::send-online-s-interval ::pos-int)
(s/def ::online-message fn?)
(s/def ::delivery-options
(s/keys :req-un [::delivery-loop-ms-interval ::ack-not-received-s-interval
::max-attempts-number ::default-ttl ::send-online-s-interval]
:opt-un [::online-message]))
(defn should-be-retransmitted?
"Checks if messages should be transmitted again."
[{:keys [ack-not-received-s-interval max-attempts-number]}
{:keys [was-sent? attempts last-attempt]}]
(if-not was-sent?
;; message was not sent succesfully via web3.shh, but maybe
;; better to do this only when we receive error from shh.post
;; todo add some notification about network issues
(<= attempts (* 5 max-attempts-number))
(and
;; if message was not send lees then max-attempts-number times
;; continue attempts
(<= attempts max-attempts-number)
;; check retransmition interval
(<= (+ last-attempt (* 1000 ack-not-received-s-interval)) (u/timestamp)))))
(defn- check-ttl
[message message-type ttl-config default-ttl]
(update message :ttl #(or % ((keyword message-type) ttl-config) default-ttl)))
(defn run-delivery-loop!
[web3 {:keys [delivery-loop-ms-interval default-ttl ttl-config
send-online-s-interval online-message]
:as options}]
{:pre [(valid? ::delivery-options options)]}
(debug :run-delivery-loop!)
(let [previous-stop-flag @loop-state
stop? (atom false)]
;; stop previous delivery loop if it exists
(when previous-stop-flag
(reset! previous-stop-flag true))
;; reset stop flag for a new loop
(reset! loop-state stop?)
;; go go!!!
(debug :init-loop)
(go-loop [_ nil]
(doseq [[_ messages] (@messages web3)]
(doseq [[_ {:keys [id message to type] :as data}] messages]
;; check each message asynchronously
(go
(when (should-be-retransmitted? options data)
(try
(let [message' (check-ttl message type ttl-config default-ttl)
callback (delivery-callback web3 data)]
(t/post-message! web3 message' callback))
(catch :default err
(timbre/error :post-message-error err))
(finally
(attempt-was-made! web3 id to)))))))
(when-not @stop?
(recur (<! (timeout delivery-loop-ms-interval)))))
(go-loop [_ nil]
(when-not @stop?
(online-message)
(recur (<! (timeout (* 1000 send-online-s-interval))))))))
(defn reset-pending-messages! [to]
(doseq [key (@recipient->pending-message to)]
(swap! messages
(fn [messages]
(when (get-in messages key)
(update-in messages key assoc
:last-attempt 0
:attempts 0))))))

View File

@ -0,0 +1,29 @@
(ns status-im.protocol.web3.filtering
(:require [status-im.protocol.web3.utils :as u]
[cljs.spec :as s]
[taoensso.timbre :refer-macros [debug]]))
(def status-topic "status-dapp-topic")
(defonce filters (atom {}))
(s/def ::options (s/keys :opt-un [:message/to :message/topics]))
(defn remove-filter! [web3 options]
(when-let [filter (get-in @filters [web3 options])]
(.stopWatching filter)
(debug :stop-watching options)
(swap! filters update web3 dissoc options)))
(defn add-filter!
[web3 options callback]
(remove-filter! web3 options)
(debug :add-filter options)
(let [filter (.filter (u/shh web3)
(clj->js options)
callback)]
(swap! filters assoc-in [web3 options] filter)))
(defn remove-all-filters! []
(doseq [[web3 filters] @filters]
(doseq [options (keys filters)]
(remove-filter! web3 options))))

View File

@ -0,0 +1,17 @@
(ns status-im.protocol.web3.transport
(:require [status-im.protocol.web3.utils :as u]
[cljs.spec :as s]
[status-im.protocol.validation :refer-macros [valid?]]
[taoensso.timbre :refer-macros [debug]]))
(s/def :shh/payload string?)
(s/def :shh/message
(s/keys
:req-un [:shh/payload :message/ttl :message/from :message/topics]
:opt-un [:message/to]))
(defn post-message!
[web3 message callback]
{:pre [(valid? :shh/message message)]}
(debug :post-message web3 message)
(.post (u/shh web3) (clj->js message) callback))

View File

@ -0,0 +1,23 @@
(ns status-im.protocol.web3.utils
(:require cljsjs.web3
[cljs-time.core :refer [now]]
[cljs-time.coerce :refer [to-long]]))
(def web3 js/Web3)
(def status-app-topic "status-app")
(defn from-utf8 [s]
(.fromUtf8 web3.prototype s))
(defn to-utf8 [s]
(.toUtf8 web3.prototype s))
(defn shh [web3]
(.-shh web3))
(defn make-web3 [rpc-url]
(->> (web3.providers.HttpProvider. rpc-url)
(web3.)))
(defn timestamp []
(to-long (now)))

View File

@ -25,6 +25,7 @@
;messages ;messages
:status-sending "Sending" :status-sending "Sending"
:status-pending "Sending"
:status-sent "Sent" :status-sent "Sent"
:status-seen-by-everyone "Seen by everyone" :status-seen-by-everyone "Seen by everyone"
:status-seen "Seen" :status-seen "Seen"

View File

@ -1,6 +1,7 @@
(ns status-im.utils.handlers (ns status-im.utils.handlers
(:require [re-frame.core :refer [after dispatch debug] :as re-core] (:require [re-frame.core :refer [after dispatch debug] :as re-core]
[re-frame.utils :refer [log]])) [re-frame.utils :refer [log]]
[clojure.string :as str]))
(defn side-effect! (defn side-effect!
"Middleware for handlers that will not affect db." "Middleware for handlers that will not affect db."
@ -24,3 +25,10 @@
([name handler] (register-handler name nil handler)) ([name handler] (register-handler name nil handler))
([name middleware handler] ([name middleware handler]
(re-core/register-handler name [debug-handlers-names middleware] handler))) (re-core/register-handler name [debug-handlers-names middleware] handler)))
(defn get-hashtags [status]
(if status
(let [hashtags (map #(str/lower-case (subs % 1))
(re-seq #"#[^ !?,;:.]+" status))]
(set (or hashtags [])))
#{}))