diff --git a/project.clj b/project.clj index 69b35d795c..ec08249e0b 100644 --- a/project.clj +++ b/project.clj @@ -3,8 +3,8 @@ :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.9.0-alpha7"] - [org.clojure/clojurescript "1.9.76"] + :dependencies [[org.clojure/clojure "1.9.0-alpha11"] + [org.clojure/clojurescript "1.9.227"] [reagent "0.5.1" :exclusions [cljsjs/react]] [re-frame "0.7.0"] [prismatic/schema "1.0.4"] @@ -13,7 +13,11 @@ [natal-shell "0.3.0"] [com.andrewmcveigh/cljs-time "0.4.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"] [lein-figwheel "0.5.0-2"] [lein-voom "0.1.0-20160311_203101-g259fbfc"]] diff --git a/src/status_im/accounts/handlers.cljs b/src/status_im/accounts/handlers.cljs index dafc5d97db..9a6fec2fa8 100644 --- a/src/status_im/accounts/handlers.cljs +++ b/src/status_im/accounts/handlers.cljs @@ -2,7 +2,7 @@ (:require [status-im.models.accounts :as accounts-model] [re-frame.core :refer [register-handler after dispatch dispatch-sync debug]] [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.utils.types :refer [json->clj]] [status-im.persistence.simple-kv-store :as kv] @@ -33,14 +33,17 @@ (storage/put kv/kv-store :password password)) (defn account-created [result password] - (let [data (json->clj result) + (let [data (json->clj result) public-key (:pubkey data) - address (:address data) - mnemonic (:mnemonic data) - account {:public-key public-key - :address address - :name address - :photo-path (identicon public-key)}] + address (:address data) + mnemonic (:mnemonic data) + {:keys [public private]} (protocol/new-keypair!) + account {:public-key public-key + :address address + :name address + :updates-public-key public + :updates-private-key private + :photo-path (identicon public-key)}] (log/debug "account-created") (when (not (str/blank? public-key)) (do @@ -61,15 +64,25 @@ (accounts-model/save-accounts [(get accounts current-account-id)] true)) (defn send-account-update - [{:keys [current-account-id accounts]} _] - (api/send-account-update (get accounts current-account-id))) + [{:keys [current-account-id current-public-key web3 accounts]} _] + (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 :account-update (-> (fn [{:keys [current-account-id accounts] :as db} [_ data]] - (let [data (assoc data :last-updated (time/now-ms)) - account (-> (get accounts current-account-id) - (merge data))] + (let [data (assoc data :last-updated (time/now-ms)) + account (-> (get accounts current-account-id) + (merge data))] (assoc-in db [:accounts current-account-id] account))) ((after save-account-to-realm!)) ((after send-account-update)))) @@ -79,7 +92,7 @@ (u/side-effect! (fn [{:keys [current-account-id accounts]} _] (let [{:keys [last-updated]} (get accounts current-account-id) - now (time/now-ms) + now (time/now-ms) needs-update? (> (- now last-updated) time/week)] (log/info "Need to send account-update: " needs-update?) (when needs-update? diff --git a/src/status_im/chat/handlers.cljs b/src/status_im/chat/handlers.cljs index 325b47d799..34fd3b4e1e 100644 --- a/src/status_im/chat/handlers.cljs +++ b/src/status_im/chat/handlers.cljs @@ -6,6 +6,7 @@ [status-im.components.styles :refer [default-chat-color]] [status-im.chat.suggestions :as suggestions] [status-im.protocol.api :as api] + [status-im.protocol.core :as protocol] [status-im.models.chats :as chats] [status-im.models.messages :as messages] [status-im.models.pending-messages :as pending-messages] @@ -174,7 +175,7 @@ (defn init-console-chat [{:keys [chats] :as db} existing-account?] - (let [chat-id "console" + (let [chat-id "console" new-chat sign-up-service/console-chat] (if (chats chat-id) db @@ -264,11 +265,6 @@ (after #(dispatch [:load-unviewed-messages!])) ((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 [{:keys [current-chat-id] :as db} [_ _ id]] (let [chat-id (or id current-chat-id) @@ -324,9 +320,9 @@ (dispatch [::start-chat! contact-id options navigation-type]))))) (register-handler :add-chat - (-> prepare-chat - ((enrich add-chat)) - ((after save-new-chat!)))) + (-> prepare-chat + ((enrich add-chat)) + ((after save-new-chat!)))) (register-handler :switch-command-suggestions! (u/side-effect! @@ -335,12 +331,8 @@ (dispatch [:set-chat-input-text text]))))) (defn remove-chat - [{:keys [current-chat-id] :as db} _] - (update db :chats dissoc current-chat-id)) - -(defn notify-about-leaving! - [{:keys [current-chat-id]} _] - (api/leave-group-chat current-chat-id)) + [db [_ chat-id]] + (update db :chats dissoc chat-id)) ; todo do we really need this message? (defn leaving-message! @@ -353,27 +345,47 @@ :content-type text-content-type})) (defn delete-messages! - [{:keys [current-chat-id]} _] - (r/write :account - (fn [] - (r/delete :account (r/get-by-field :account :message :chat-id current-chat-id))))) + [{:keys [current-chat-id]} [_ chat-id]] + (let [id (or chat-id current-chat-id)] + (r/write :account + (fn [] + (r/delete :account + (r/get-by-field :account :message :chat-id id)))))) (defn delete-chat! - [{:keys [current-chat-id]} _] + [_ [_ chat-id]] (r/write :account (fn [] :account - (->> (r/get-by-field :account :chat :chat-id current-chat-id) - (r/single) - (r/delete :account))))) + (when-let [chat (->> (r/get-by-field :account :chat :chat-id chat-id) + (r/single))] + (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 ;; todo oreder of operations tbd (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 - ;; todo uncomment - ;((after notify-about-leaving!)) ;((after leaving-message!)) ((after delete-messages!)) + ((after remove-pending-messages!)) ((after delete-chat!)))) (defn edit-mode-handler [mode] @@ -405,13 +417,18 @@ (assoc db :layout-height h))) (register-handler :send-seen! - (after (fn [_ [_ chat-id message-id]] - (dispatch [:message-seen {:message-id message-id - :chat-id chat-id}]))) + (after (fn [_ [_ options]] + (dispatch [:message-seen options]))) (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) - (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 (fn [{:keys [current-chat-id] :as db} [_ url]] diff --git a/src/status_im/chat/handlers/receive_message.cljs b/src/status_im/chat/handlers/receive_message.cljs index e838fa6b33..0a4b606c63 100644 --- a/src/status_im/chat/handlers/receive_message.cljs +++ b/src/status_im/chat/handlers/receive_message.cljs @@ -27,23 +27,35 @@ (defn receive-message [db [_ {:keys [from group-id chat-id message-id] :as message}]] (let [same-message (messages/get-message message-id) - current-identity (get-current-identity db)] - (when-not (or same-message (= from current-identity)) + current-identity (get-current-identity db) + 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)) - chat-id' (or chat-id from) previous-message (messages/get-last-message chat-id') message' (assoc (->> message (cu/check-author-direction previous-message) (check-preview)) :chat-id chat-id')] (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-message message']) (when (= (:content-type message') content-type-command-request) (dispatch [:add-request chat-id' message'])) (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 (u/side-effect! receive-message)) diff --git a/src/status_im/chat/handlers/send_message.cljs b/src/status_im/chat/handlers/send_message.cljs index 55b3867967..db40b1ce8a 100644 --- a/src/status_im/chat/handlers/send_message.cljs +++ b/src/status_im/chat/handlers/send_message.cljs @@ -5,34 +5,15 @@ [status-im.components.status :as status] [status-im.utils.random :as random] [status-im.utils.datetime :as time] - [re-frame.core :refer [enrich after debug dispatch path]] - [status-im.chat.suggestions :as suggestions] - [status-im.models.commands :as commands] + [re-frame.core :refer [enrich after dispatch path]] [status-im.chat.utils :as cu] [status-im.constants :refer [text-content-type content-type-command content-type-command-request default-number-of-messages]] - [status-im.protocol.api :as api] - [status-im.utils.logging :as log])) - -(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))))) + [status-im.utils.datetime :as datetime] + [status-im.protocol.core :as protocol] + [taoensso.timbre :refer-macros [debug]])) (defn prepare-command [identity chat-id {:keys [preview preview-string content command to-message]}] @@ -52,13 +33,13 @@ (register-handler :send-chat-message (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])) text (get-in db [:chats current-chat-id :input-text]) data {:commands staged-commands :message text :chat-id current-chat-id - :identity identity + :identity current-public-key :address current-account-id}] (dispatch [:clear-input current-chat-id]) (cond @@ -88,9 +69,9 @@ (register-handler :prepare-command! (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 - (prepare-command identity chat-id) + (prepare-command current-public-key chat-id) (cu/check-author-direction db chat-id))] (dispatch [::clear-command chat-id (:id command)]) (dispatch [::send-command! (assoc params :command command')]))))) @@ -115,7 +96,7 @@ (register-handler ::save-command! (u/side-effect! - (fn [_ [_ {:keys [command chat-id]}]] + (fn [{:keys [current-public-key]} [_ {:keys [command chat-id]}]] (messages/save-message chat-id (dissoc command :rendered-preview :to-message :has-handler))))) @@ -177,22 +158,41 @@ (register-handler ::send-message! (u/side-effect! - (fn [_ [_ {{:keys [message-type] - :as message} :message - chat-id :chat-id}]] + (fn [{:keys [web3 chats]} [_ {{:keys [message-type] + :as message} :message + chat-id :chat-id}]] (when (and message (cu/not-console? chat-id)) - (if (= message-type :group-user-message) - (api/send-group-user-message message) - (api/send-user-message message)))))) + (let [message' (select-keys message [:from :message-id]) + payload (select-keys message [:timestamp :content :content-type]) + 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! (u/side-effect! - (fn [db [_ {:keys [chat-id command]}]] - (let [{:keys [content]} command] + (fn [{:keys [web3 current-public-key chats] :as db} [_ {:keys [chat-id command]}]] + (let [{:keys [content message-id]} command] (when (cu/not-console? chat-id) - (let [{:keys [group-chat]} (get-in db [:chats chat-id]) - message {:content content - :content-type content-type-command}] + (let [{:keys [public-key private-key]} (chats chat-id) + {:keys [group-chat]} (get-in db [:chats chat-id]) + 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 - (api/send-group-user-message (assoc message :group-id chat-id)) - (api/send-user-message (assoc message :to 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] chat-id))))))))) diff --git a/src/status_im/chat/views/message.cljs b/src/status_im/chat/views/message.cljs index 0a12e1acd5..29fd2421ba 100644 --- a/src/status_im/chat/views/message.cljs +++ b/src/status_im/chat/views/message.cljs @@ -262,7 +262,7 @@ 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) status (subscribe [:get-in [:message-user-statuses message-id my-identity]])] (r/create-class @@ -271,7 +271,9 @@ (when (and (not outgoing) (not= :seen (keyword @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 (fn [{:keys [outgoing timestamp new-day group-chat] :as message}] [message-container message diff --git a/src/status_im/contacts/handlers.cljs b/src/status_im/contacts/handlers.cljs index 9b20c571a6..25c4e85918 100644 --- a/src/status_im/contacts/handlers.cljs +++ b/src/status_im/contacts/handlers.cljs @@ -4,13 +4,13 @@ [status-im.models.contacts :as contacts] [status-im.utils.crypt :refer [encrypt]] [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.phone-number :refer [format-phone-number]] [status-im.utils.handlers :as u] [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 @@ -32,15 +32,30 @@ (contacts/save-contacts [contact])) (defn watch-contact - [_ [_ {:keys [whisper-identity]}]] - (api/watch-user whisper-identity)) + [{:keys [web3]} [_ {:keys [whisper-identity public-key private-key]}]] + (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)) (defn send-contact-request - [{:keys [current-account-id accounts]} [_ contact]] - (let [account (get accounts current-account-id)] - (api/send-contact-request contact account))) + [{:keys [current-public-key web3 current-account-id accounts]} [_ contact]] + (let [{:keys [whisper-identity]} contact + {: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! (-> (fn [db [_ {:keys [whisper-identity] :as contact}]] @@ -107,7 +122,7 @@ (defn request-stored-contacts [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} (fn [{:keys [contacts]}] (let [contacts' (add-identity contacts-by-hash contacts)] @@ -131,7 +146,7 @@ (defn add-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 (map #(update-pending-status contacts %)) (remove #(identities (:whisper-identity %))) @@ -159,8 +174,7 @@ (register-handler ::prepare-contact (-> add-new-contact ((after save-contact)) - ((after send-contact-request)) - ((after watch-contact)))) + ((after send-contact-request)))) (register-handler ::update-pending-contact (-> add-new-contact @@ -168,9 +182,21 @@ (register-handler :add-pending-contact (u/side-effect! - (fn [_ [_ {:keys [whisper-identity] :as contact}]] - (let [contact (assoc contact :pending false)] - (api/send-discovery-keypair whisper-identity) + (fn [{:keys [current-public-key web3 current-account-id accounts]} + [_ {:keys [whisper-identity] :as contact}]] + (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]))))) (defn set-contact-identity-from-qr @@ -181,34 +207,28 @@ (register-handler :contact-update-received (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 [db [_ from {{:keys [public-key last-updated name] :as account} :account}]] - (let [prev-last-updated (get-in db [:contacts public-key :last-updated])] - (if (<= prev-last-updated last-updated) - (let [contact (-> (assoc account :whisper-identity public-key) - (dissoc :public-key))] + (fn [{:keys [chats] :as db} [_ {:keys [from payload]}]] + (let [{:keys [content timestamp]} payload + {:keys [status name profile-image]} (:profile content) + prev-last-updated (get-in db [:contacts from :last-updated])] + (if (<= prev-last-updated timestamp) + (let [contact {:whisper-identity from + :name name + :photo-path profile-image + :status status + :last-updated timestamp}] (dispatch [:update-contact! contact]) - (dispatch [:update-chat! {:chat-id public-key - :name name}]))))))) + (when (chats from) + (dispatch [:update-chat! {:chat-id from + :name name}])))))))) (register-handler :contact-online-received (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])] - (when (< prev-last-online last-online) - (api/resend-pending-messages from) + (when (< prev-last-online timestamp) + (protocol/reset-pending-messages! 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)))) \ No newline at end of file diff --git a/src/status_im/db.cljs b/src/status_im/db.cljs index 3402f69ad4..309a4f5aa9 100644 --- a/src/status_im/db.cljs +++ b/src/status_im/db.cljs @@ -11,6 +11,7 @@ ;; initial state of app-db (def app-db {:identity-password "replace-me-with-user-entered-password" :identity "me" + :current-public-key "me" :accounts {} :current-account-id nil diff --git a/src/status_im/discovery/handlers.cljs b/src/status_im/discovery/handlers.cljs index beb86b5ef6..ecba308a39 100644 --- a/src/status_im/discovery/handlers.cljs +++ b/src/status_im/discovery/handlers.cljs @@ -3,10 +3,12 @@ [status-im.utils.utils :refer [first-index]] [status-im.utils.handlers :refer [register-handler]] [status-im.protocol.api :as api] + [status-im.protocol.core :as protocol] [status-im.navigation.handlers :as nav] [status-im.discovery.model :as discoveries] [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 (fn [db _] @@ -14,8 +16,8 @@ (assoc :discoveries [])))) (defn calculate-priority [{:keys [chats contacts]} from payload] - (let [contact (get contacts from) - chat (get chats from) + (let [contact (get contacts from) + chat (get chats from) seen-online-recently? (< (- (time/now-ms) (get contact :last-online)) time/hour)] (+ (time/now-ms) ;; message is newer => priority is higher @@ -35,23 +37,37 @@ (register-handler :discovery-response-received (u/side-effect! - (fn [db [_ from payload]] - (let [{:keys [message-id name photo-path status hashtags]} payload - discovery {:message-id message-id - :name name - :photo-path photo-path - :status status - :whisper-id from - :tags (map #(hash-map :name %) hashtags) - :last-updated (js/Date.) - :priority (calculate-priority db from payload)}] - (dispatch [:add-discovery discovery]))))) + (fn [{:keys [current-public-key] :as db} [_ {:keys [from payload]}]] + (when-not (= current-public-key from) + (let [{:keys [discovery-id profile hashtags]} payload + {:keys [name profile-image status]} profile + discovery {:message-id discovery-id + :name name + :photo-path profile-image + :status status + :whisper-id from + :tags (map #(hash-map :name %) hashtags) + :last-updated (js/Date.) + :priority (calculate-priority db from payload)}] + (dispatch [:add-discovery discovery])))))) (register-handler :broadcast-status (u/side-effect! - (fn [{:keys [current-account-id accounts]} [_ status hashtags]] - (let [account (get accounts current-account-id)] - (api/broadcast-discover-status account status hashtags))))) + (fn [{:keys [current-public-key web3 current-account-id accounts]} + [_ 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 (fn [db [_ tag]] diff --git a/src/status_im/group_settings/handlers.cljs b/src/status_im/group_settings/handlers.cljs index 1387ed9fca..56bc91ec3e 100644 --- a/src/status_im/group_settings/handlers.cljs +++ b/src/status_im/group_settings/handlers.cljs @@ -3,7 +3,7 @@ [status-im.utils.handlers :refer [register-handler]] [status-im.persistence.realm.core :as r] [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.models.contacts :as contacts] [status-im.models.messages :as messages] @@ -84,16 +84,17 @@ [{:keys [current-chat-id selected-participants] :as db} _] (let [chat (get-in db [:chats current-chat-id])] (r/write :account - (fn [] - (r/create :account - :chat - (update chat :contacts remove-identities selected-participants) - true))))) + (fn [] + (r/create :account + :chat + (update chat :contacts remove-identities selected-participants) + true))))) (defn notify-about-removing! [{:keys [current-chat-id selected-participants]} _] (doseq [participant selected-participants] - (api/group-remove-participant current-chat-id participant))) + ;;todo implement + )) (defn system-message [message-id content] {:from "system" @@ -138,14 +139,33 @@ (chats/chat-add-participants current-chat-id selected-participants)) (defn notify-about-new-members! - [{:keys [current-chat-id selected-participants]} _] - (doseq [identity selected-participants] - (api/group-add-participant current-chat-id identity))) + [{:keys [current-chat-id selected-participants + current-public-key chats web3]} _] + (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 ;; todo order of operations tbd (-> add-memebers ((after add-members-to-realm!)) - ;; todo uncomment - ;((after notify-about-new-members!)) + ((after notify-about-new-members!)) ((enrich deselect-members)))) diff --git a/src/status_im/handlers.cljs b/src/status_im/handlers.cljs index 02b05fee42..1a13a0bcf4 100644 --- a/src/status_im/handlers.cljs +++ b/src/status_im/handlers.cljs @@ -60,22 +60,22 @@ (register-handler :initialize-db (fn [_ _] (realm/reset-account) - (assoc app-db :current-account-id nil - :current-public-key nil))) + (assoc app-db :current-account-id nil))) (register-handler :initialize-account-db (fn [db _] (assoc db + :chats {} + :current-chat-id "console" :signed-up (storage/get kv/kv-store :signed-up) :password (storage/get kv/kv-store :password)))) (register-handler :initialize-account (u/side-effect! (fn [_ [_ address]] - (dispatch [:initialize-protocol address]) (dispatch [:initialize-account-db]) + (dispatch [:initialize-protocol address]) (dispatch [:initialize-chats]) - (dispatch [:initialize-pending-messages]) (dispatch [:load-contacts]) (dispatch [:init-chat]) (dispatch [:init-discoveries]) diff --git a/src/status_im/models/chats.cljs b/src/status_im/models/chats.cljs index 7a542d3b4c..6e0155494f 100644 --- a/src/status_im/models/chats.cljs +++ b/src/status_im/models/chats.cljs @@ -46,21 +46,21 @@ (defn re-join-group-chat [db group-id identities group-name] (r/write :account - (fn [] - (let [new-identities (set identities) - only-old-contacts (->> (chat-contacts group-id) - (r/cljs-list) - (remove (fn [{:keys [identity]}] - (new-identities identity)))) - contacts (->> new-identities - (mapv (fn [ident] - {:identity ident})) - (concat only-old-contacts))] - (r/create :account :chat - {:chat-id group-id - :is-active true - :name group-name - :contacts contacts} true)))) + (fn [] + (let [new-identities (set identities) + only-old-contacts (->> (chat-contacts group-id) + (r/cljs-list) + (remove (fn [{:keys [identity]}] + (new-identities identity)))) + contacts (->> new-identities + (mapv (fn [ident] + {:identity ident})) + (concat only-old-contacts))] + (r/create :account :chat + {:chat-id group-id + :is-active true + :name group-name + :contacts contacts} true)))) db) (defn normalize-contacts @@ -68,7 +68,7 @@ (map #(update % :contacts vals) chats)) (defn chats-list [] - (-> (r/get-all :account :chat) + (-> (r/get-by-field :account :chat :is-active true) (r/sorted :timestamp :desc) r/realm-collection->list normalize-contacts)) @@ -89,54 +89,46 @@ (defn create-chat ([{:keys [last-message-id] :as chat}] (let [chat (assoc chat :last-message-id (or last-message-id ""))] - (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))))) + (r/write :account #(r/create :account :chat chat true))))) (defn chat-add-participants [chat-id identities] (r/write :account - (fn [] - (let [contacts (chat-contacts chat-id)] - (doseq [contact-identity identities] - (if-let [contact-exists (.find contacts (fn [object index collection] + (fn [] + (let [contacts (chat-contacts chat-id) + added-at (timestamp)] + (doseq [contact-identity identities] + (if-let [contact (.find contacts (fn [object index collection] (= contact-identity (aget object "identity"))))] - (aset contact-exists "is-in-chat" true) - (.push contacts (clj->js {:identity contact-identity}))))))) + (doto contact + (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 (dispatch [:initialize-chats])) -;; TODO deprecated? (is there need to remove multiple member at once?) (defn chat-remove-participants [chat-id identities] (r/write :account - (fn [] - (let [query (include-query :identity identities) - chat (r/single (r/get-by-field :account :chat :chat-id chat-id))] - (-> (aget chat "contacts") - (r/filtered query) - (.forEach (fn [object _ _] - (aset object "is-in-chat" false)))))))) + (fn [] + (let [query (include-query :identity identities) + chat (r/single (r/get-by-field :account :chat :chat-id chat-id))] + (-> (aget chat "contacts") + (r/filtered query) + (.forEach (fn [object _ _] + (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 [] - (let [results (r/filtered (r/get-all :account :chat) - "group-chat = true && is-active = true")] - (js->clj (.map results (fn [object _ _] - (aget object "chat-id")))))) + (map + (fn [{:keys [chat-id public-key private-key]}] + {:chat-id chat-id + :keypair {:private private-key + :public public-key}}) + (r/realm-collection->list (groups true)))) (defn set-chat-active [chat-id active?] (r/write :account @@ -144,3 +136,19 @@ (-> (r/get-by-field :account :chat :chat-id chat-id) (r/single) (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))) diff --git a/src/status_im/models/pending_messages.cljs b/src/status_im/models/pending_messages.cljs index 25e08f8e60..874d6537d9 100644 --- a/src/status_im/models/pending_messages.cljs +++ b/src/status_im/models/pending_messages.cljs @@ -8,32 +8,55 @@ [status-im.constants :as c] [status-im.utils.types :refer [clj->json json->clj]] [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 [] - (let [collection (-> (r/get-by-fields :account :pending-message :or [[:status :sending] - [:status :sent] - [:status :failed]]) - (r/sorted :timestamp :desc) - (r/realm-collection->list))] - (->> collection - (map (fn [{:keys [message-id] :as message}] - (let [message (-> message - (update :message json->clj) - (update :identities json->clj))] - [message-id message]))) - (into {})))) +(defn get-pending-messages! [] + (->> (r/get-all :account :pending-message) + r/realm-collection->list + (map (fn [{:keys [message-id] :as message}] + (-> message + (update :topics reader/read-string) + (assoc :id message-id)))))) -(defn upsert-pending-message! - [message] +(defn- get-id + [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 (fn [] - (let [message (-> message - (update :message clj->json) - (update :identities clj->json))] - (r/create :account :pending-message message true))))) + (let [{:keys [from topics payload]} message + id' (get-id id to) + chat-id (or group-id to) + 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 (fn [] (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))))) diff --git a/src/status_im/models/protocol.cljs b/src/status_im/models/protocol.cljs index e85863b20c..9cd9797421 100644 --- a/src/status_im/models/protocol.cljs +++ b/src/status_im/models/protocol.cljs @@ -1,8 +1,6 @@ (ns status-im.models.protocol (:require [cljs.reader :refer [read-string]] [status-im.protocol.state.storage :as s] - [status-im.utils.encryption :refer [password-encrypt - password-decrypt]] [status-im.utils.types :refer [to-edn-string]] [re-frame.db :refer [app-db]] [status-im.db :as db] diff --git a/src/status_im/new_group/handlers.cljs b/src/status_im/new_group/handlers.cljs index a35c964497..ab6611c38c 100644 --- a/src/status_im/new_group/handlers.cljs +++ b/src/status_im/new_group/handlers.cljs @@ -1,11 +1,13 @@ (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]] [status-im.utils.handlers :refer [register-handler]] [status-im.components.styles :refer [default-chat-color]] [status-im.models.chats :as chats] [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 [db [_ id]] @@ -19,11 +21,6 @@ (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 [{:keys [contacts selected-contacts username]}] (->> (select-keys contacts selected-contacts) @@ -33,20 +30,21 @@ (s/join ", "))) (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) chat-name (if-not (s/blank? group-name) group-name - (group-name-from-contacts db))] - (assoc db :new-chat {:chat-id new-group-id - :name chat-name - :color default-chat-color - :group-chat true - :is-active true - :timestamp (.getTime (js/Date.)) - :contacts contacts - :same-author false - :same-direction false}))) + (group-name-from-contacts db)) + {:keys [public private]} (protocol/new-keypair!)] + (assoc db :new-chat {:chat-id (random/id) + :public-key public + :private-key private + :name chat-name + :color default-chat-color + :group-chat true + :is-active true + :timestamp (.getTime (js/Date.)) + :contacts contacts}))) (defn add-chat [{:keys [new-chat] :as db} _] @@ -59,22 +57,45 @@ (chats/create-chat new-chat)) (defn show-chat! - [{:keys [new-group-id]} _] - (dispatch [:navigation-replace :chat new-group-id])) + [{:keys [new-chat]} _] + (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 _] (assoc db :disable-group-creation false)) (register-handler :create-new-group - (-> start-group-chat! - ((enrich prepare-chat)) + (-> prepare-chat ((enrich add-chat)) ((after create-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 _] (assoc db :disable-group-creation true)) @@ -84,18 +105,32 @@ (register-handler :init-group-creation (after dispatch-create-group) - disable-creat-button) + disable-create-button) -; todo rewrite (register-handler :group-chat-invite-received (u/side-effect! - (fn [{:keys [current-public-key] :as db} - [action from group-id identities group-name]] - (if (chats/chat-exists? group-id) - (chats/re-join-group-chat db group-id identities group-name) - (let [contacts (keep (fn [ident] - (when (not= ident current-public-key) - {:identity ident})) identities)] - (dispatch [:add-chat group-id {:name group-name - :group-chat true - :contacts contacts}])))))) + (fn [{:keys [current-public-key web3] :as db} + [_ {{:keys [group-id group-name contacts keypair timestamp] :as payload} :payload}]] + (let [{:keys [private public]} keypair] + (let [removed-at (chats/removed-at group-id) + is-active (chats/is-active? group-id) + contacts' (keep (fn [ident] + (when (not= ident current-public-key) + {:identity ident})) contacts) + chat {:name group-name + :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])})))))))) diff --git a/src/status_im/persistence/realm/schemas.cljs b/src/status_im/persistence/realm/schemas.cljs index 91dbe0e0e1..47f084b18a 100644 --- a/src/status_im/persistence/realm/schemas.cljs +++ b/src/status_im/persistence/realm/schemas.cljs @@ -3,14 +3,18 @@ (def base {:schema [{:name :account :primaryKey :address - :properties {:address "string" - :public-key "string" - :name {:type "string" :optional true} - :phone {:type "string" :optional true} - :email {:type "string" :optional true} - :status {:type "string" :optional true} - :photo-path "string" - :last-updated {:type "int" :default 0}}} + :properties {:address "string" + :public-key "string" + :updates-public-key {:type :string + :optional true} + :updates-private-key {:type :string + :optional true} + :name {:type "string" :optional true} + :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 :primaryKey :key :properties {:key "string" @@ -25,7 +29,11 @@ :photo-path {:type "string" :optional true} :last-updated {: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 :properties {:message-id :string :chat-id :string @@ -85,19 +93,21 @@ :user-statuses {:type :list :objectType "user-status"}}} {:name :pending-message - :primaryKey :message-id - :properties {:message-id "string" - :chat-id {:type "string" - :optional true} - :message "string" - :timestamp "int" - :status "string" - :retry-count "int" - :send-once "bool" - :identities {:type "string" - :optional true} - :internal? {:type "bool" - :optional true}}} + :primaryKey :id + :properties {:id :string + :message-id :string + :chat-id {:type :string + :optional true} + :ack? :bool + :requires-ack? :bool + :from :string + :to {:type :string + :optional true} + :payload :string + :type :string + :topics :string + :attempts :int + :was-sent? :bool}} {:name :chat-contact :properties {:identity "string" :is-in-chat {:type "bool" @@ -118,7 +128,13 @@ :optional true} :dapp-hash {:type :int :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 :primaryKey :chat-id :properties {:chat-id "string" diff --git a/src/status_im/profile/handlers.cljs b/src/status_im/profile/handlers.cljs index 485942515d..e54d6eeff4 100644 --- a/src/status_im/profile/handlers.cljs +++ b/src/status_im/profile/handlers.cljs @@ -4,13 +4,9 @@ [status-im.components.react :refer [show-image-picker]] [status-im.utils.image-processing :refer [img->base64]] [status-im.i18n :refer [label]] - [status-im.utils.handlers :as u] + [status-im.utils.handlers :as u :refer [get-hashtags]] [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] (when identity (dispatch [:navigate-to :chat identity]))) @@ -59,4 +55,4 @@ 0 (dispatch [:show-profile-photo-capture]) 1 (dispatch [:open-image-picker]) :default)) - :cancel-text (label :t/image-source-cancel)})))) \ No newline at end of file + :cancel-text (label :t/image-source-cancel)})))) diff --git a/src/status_im/profile/screen.cljs b/src/status_im/profile/screen.cljs index e013ec3386..33412d7575 100644 --- a/src/status_im/profile/screen.cljs +++ b/src/status_im/profile/screen.cljs @@ -16,8 +16,8 @@ my-profile-icon]] [status-im.components.status-bar :refer [status-bar]] [status-im.profile.styles :as st] - [status-im.profile.handlers :refer [get-hashtags - message-user + [status-im.utils.handlers :refer [get-hashtags]] + [status-im.profile.handlers :refer [message-user update-profile]] [status-im.components.qr-code :refer [qr-code]] [status-im.utils.phone-number :refer [format-phone-number diff --git a/src/status_im/protocol/ack.cljs b/src/status_im/protocol/ack.cljs new file mode 100644 index 0000000000..8d8a38b4a5 --- /dev/null +++ b/src/status_im/protocol/ack.cljs @@ -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))) diff --git a/src/status_im/protocol/chat.cljs b/src/status_im/protocol/chat.cljs new file mode 100644 index 0000000000..fd3b6c28e7 --- /dev/null +++ b/src/status_im/protocol/chat.cljs @@ -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))))) diff --git a/src/status_im/protocol/core.cljs b/src/status_im/protocol/core.cljs new file mode 100644 index 0000000000..05ff4c9d04 --- /dev/null +++ b/src/status_im/protocol/core.cljs @@ -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)) diff --git a/src/status_im/protocol/discoveries.cljs b/src/status_im/protocol/discoveries.cljs new file mode 100644 index 0000000000..9a377d385b --- /dev/null +++ b/src/status_im/protocol/discoveries.cljs @@ -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)))))) diff --git a/src/status_im/protocol/encryption.cljs b/src/status_im/protocol/encryption.cljs new file mode 100644 index 0000000000..e21e174623 --- /dev/null +++ b/src/status_im/protocol/encryption.cljs @@ -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)) + diff --git a/src/status_im/protocol/group.cljs b/src/status_im/protocol/group.cljs new file mode 100644 index 0000000000..0d875a022c --- /dev/null +++ b/src/status_im/protocol/group.cljs @@ -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}))) diff --git a/src/status_im/protocol/handlers.cljs b/src/status_im/protocol/handlers.cljs index f7fa056b50..ebb7fde5e0 100644 --- a/src/status_im/protocol/handlers.cljs +++ b/src/status_im/protocol/handlers.cljs @@ -3,26 +3,75 @@ (ns status-im.protocol.handlers (:require [status-im.utils.handlers :as u] [status-im.utils.logging :as log] - [status-im.protocol.api :as api] [re-frame.core :refer [dispatch after]] [status-im.utils.handlers :refer [register-handler]] [status-im.models.contacts :as contacts] [status-im.models.messages :as messages] [status-im.models.pending-messages :as pending-messages] [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 set-initialized]] + [status-im.protocol.core :as protocol] [status-im.constants :refer [text-content-type]] [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 + (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! - (fn [db [_ current-account-id]] - (let [current-account (get-in db [:accounts current-account-id])] - (init-protocol current-account (make-handler db)))))) + (fn [_ [_ type {:keys [payload] :as message}]] + (debug :incoming-message type) + (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 (fn [db [_ identity]] @@ -43,9 +92,9 @@ :content (str (or contact-name from) " " (label :t/received-invitation)) :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)) - invitee-name (if (= identity (api/my-identity)) + invitee-name (if (= identity current-identity) (label :t/You) (:name (contacts/contact-by-identity identity)))] (messages/save-message chat-id {:from "system" @@ -94,42 +143,94 @@ (register-handler :participant-left-group (u/side-effect! - (fn [_ [action from group-id message-id]] - (log/debug action message-id from group-id) - (when-not (= (api/my-identity) from) - (participant-left-group-message group-id from message-id))))) + (fn [{:keys [current-public-key]} + [_ {:keys [from] + {:keys [group-id message-id timestamp]} :payload}]] + (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 (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) - (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] - (fn [_ [_ {:keys [message-id whisper-identity]}]] + (fn [_ [_ + {:keys [from] + {:keys [message-id group-id]} :payload}]] (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 - [:user-statuses whisper-identity] + [:user-statuses from] (fn [{old-status :status}] {:id (random/id) - :whisper-identity whisper-identity + :whisper-identity from :status (if (= (keyword old-status) :seen) old-status status)})) (assoc message :message-status status))] (messages/update-message! message))))) + (defn update-message-status [status] - (fn [db [_ {:keys [message-id whisper-identity]}]] - (let [db-key (if whisper-identity - [:message-user-statuses message-id whisper-identity] - [:message-statuses message-id]) - current-status (get-in db db-key)] - (if-not (= :seen current-status) - (assoc-in db db-key {:whisper-identity whisper-identity - :status status}) - db)))) + (fn [db + [_ {:keys [from] + {:keys [message-id group-id]} :payload}]] + (if (chats/is-active? (or group-id from)) + (let [group? (boolean group-id) + status-path (if (and group? (not= status :sent)) + [:message-user-statuses message-id from] + [:message-statuses message-id]) + 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 (after (save-message-status! :failed)) @@ -139,10 +240,6 @@ (after (save-message-status! :sent)) (update-message-status :sent)) -(register-handler :message-delivered - (after (save-message-status! :delivered)) - (update-message-status :delivered)) - (register-handler :message-seen [(after (save-message-status! :seen)) (after (fn [_ [_ {:keys [chat-id]}]] @@ -150,16 +247,39 @@ (update-message-status :seen)) (register-handler :pending-message-upsert - (u/side-effect! - (fn [_ [_ pending-message]] - (pending-messages/upsert-pending-message! pending-message)))) + (after + (fn [_ [_ {:keys [type id] :as 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 (u/side-effect! - (fn [_ [_ message-id]] - (pending-messages/remove-pending-message! message-id)))) + (fn [_ [_ message]] + (pending-messages/remove-pending-message! message)))) -(register-handler :send-transaction! +(register-handler :contact-request-received (u/side-effect! - (fn [_ [_ amount message]] - (println :send-transacion! amount message)))) + (fn [_ [_ {:keys [from payload]}]] + (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]])))))) diff --git a/src/status_im/protocol/listeners.cljs b/src/status_im/protocol/listeners.cljs new file mode 100644 index 0000000000..a09b6ddbc9 --- /dev/null +++ b/src/status_im/protocol/listeners.cljs @@ -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))))))) diff --git a/src/status_im/protocol/message.cljs b/src/status_im/protocol/message.cljs new file mode 100644 index 0000000000..43eedee4cb --- /dev/null +++ b/src/status_im/protocol/message.cljs @@ -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? %))) diff --git a/src/status_im/protocol/protocol_handler.cljs b/src/status_im/protocol/protocol_handler.cljs index f330850218..319b0c719b 100644 --- a/src/status_im/protocol/protocol_handler.cljs +++ b/src/status_im/protocol/protocol_handler.cljs @@ -5,64 +5,3 @@ [status-im.models.protocol :refer [stored-identity]] [status-im.persistence.simple-kv-store :as kv] [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)))}) diff --git a/src/status_im/protocol/validation.clj b/src/status_im/protocol/validation.clj new file mode 100644 index 0000000000..c9d472fd10 --- /dev/null +++ b/src/status_im/protocol/validation.clj @@ -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?#)) diff --git a/src/status_im/protocol/validation.cljs b/src/status_im/protocol/validation.cljs new file mode 100644 index 0000000000..44ba173656 --- /dev/null +++ b/src/status_im/protocol/validation.cljs @@ -0,0 +1,2 @@ +(ns status-im.protocol.validation + (:require-macros [status-im.protocol.validation :as macros])) diff --git a/src/status_im/protocol/web3/delivery.cljs b/src/status_im/protocol/web3/delivery.cljs new file mode 100644 index 0000000000..1cf3fd1fd5 --- /dev/null +++ b/src/status_im/protocol/web3/delivery.cljs @@ -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 [ 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 (pending-message to)] + (swap! messages + (fn [messages] + (when (get-in messages key) + (update-in messages key assoc + :last-attempt 0 + :attempts 0)))))) diff --git a/src/status_im/protocol/web3/filtering.cljs b/src/status_im/protocol/web3/filtering.cljs new file mode 100644 index 0000000000..d4f3bfa49c --- /dev/null +++ b/src/status_im/protocol/web3/filtering.cljs @@ -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)))) diff --git a/src/status_im/protocol/web3/transport.cljs b/src/status_im/protocol/web3/transport.cljs new file mode 100644 index 0000000000..ff00d4fcf1 --- /dev/null +++ b/src/status_im/protocol/web3/transport.cljs @@ -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)) diff --git a/src/status_im/protocol/web3/utils.cljs b/src/status_im/protocol/web3/utils.cljs new file mode 100644 index 0000000000..a044146d22 --- /dev/null +++ b/src/status_im/protocol/web3/utils.cljs @@ -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))) diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index 982be6717f..73f9c7abb6 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -25,6 +25,7 @@ ;messages :status-sending "Sending" + :status-pending "Sending" :status-sent "Sent" :status-seen-by-everyone "Seen by everyone" :status-seen "Seen" diff --git a/src/status_im/utils/handlers.cljs b/src/status_im/utils/handlers.cljs index f5d5006d1a..db49a6609c 100644 --- a/src/status_im/utils/handlers.cljs +++ b/src/status_im/utils/handlers.cljs @@ -1,6 +1,7 @@ (ns status-im.utils.handlers (: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! "Middleware for handlers that will not affect db." @@ -24,3 +25,10 @@ ([name handler] (register-handler name nil handler)) ([name 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 []))) + #{}))