From 7960fdef85eb569662e96c68b5bb29ce6a59764d Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Fri, 25 Jan 2019 14:07:33 +0100 Subject: [PATCH] Publish contact updates periodically Currently it's very easy for contact details to get out of sync, the simplest example is: A & B are contacts. A changes name. B receives the updated name. B re-install the app. Until A changes name again, B will not see their name, picture and won't be able to send push notifications. This PR changes the behavior to publish account informations to contacts every 24 hrs, to add some redundancy in this cases. It also publishes a contact code every 12hrs. Signed-off-by: Andrea Maria Piana --- src/status_im/accounts/create/core.cljs | 7 +- src/status_im/accounts/update/core.cljs | 39 +++++++- src/status_im/accounts/update/publisher.cljs | 43 +++++++++ src/status_im/chat/models.cljs | 17 +++- src/status_im/contact/core.cljs | 38 ++++---- src/status_im/contact_code/core.cljs | 94 +++++++++++++++++++ .../realm/schemas/base/account.cljs | 5 +- .../data_store/realm/schemas/base/core.cljs | 10 +- src/status_im/events.cljs | 22 +++++ src/status_im/group_chats/core.cljs | 23 ++++- src/status_im/models/transactions.cljs | 58 +----------- src/status_im/pairing/core.cljs | 25 +++-- src/status_im/transport/core.cljs | 22 +++-- src/status_im/transport/filters.cljs | 5 +- src/status_im/transport/impl/send.cljs | 26 +---- src/status_im/transport/shh.cljs | 48 +++++----- src/status_im/utils/async.cljs | 50 ++++++++++ src/status_im/utils/fx.cljs | 2 +- src/status_im/utils/publisher.cljs | 42 +++++++++ test/cljs/status_im/test/chat/models.cljs | 2 +- .../status_im/test/contact_code/core.cljs | 78 +++++++++++++++ test/cljs/status_im/test/models/contact.cljs | 2 +- test/cljs/status_im/test/pairing/core.cljs | 79 ++++++++-------- test/cljs/status_im/test/runner.cljs | 2 + 24 files changed, 547 insertions(+), 192 deletions(-) create mode 100644 src/status_im/accounts/update/publisher.cljs create mode 100644 src/status_im/contact_code/core.cljs create mode 100644 src/status_im/utils/publisher.cljs create mode 100644 test/cljs/status_im/test/contact_code/core.cljs diff --git a/src/status_im/accounts/create/core.cljs b/src/status_im/accounts/create/core.cljs index c22e1baa5f..4f6301d1ad 100644 --- a/src/status_im/accounts/create/core.cljs +++ b/src/status_im/accounts/create/core.cljs @@ -102,12 +102,15 @@ [{db :db} input-key text] {:db (update db :accounts/create merge {input-key text :error nil})}) -(defn account-set-name [{{:accounts/keys [create] :as db} :db :as cofx}] +(defn account-set-name [{{:accounts/keys [create] :as db} :db now :now :as cofx}] (fx/merge cofx {:db (assoc db :accounts/create {:show-welcome? true}) :notifications/request-notifications-permissions nil :dispatch [:navigate-to :home]} - (accounts.update/account-update {:name (:name create)} {}))) + ;; We set last updated as we are actually changing a field, + ;; unlike on recovery where the name is not set + (accounts.update/account-update {:last-updated now + :name (:name create)} {}))) (fx/defn next-step [{:keys [db] :as cofx} step password password-confirm] diff --git a/src/status_im/accounts/update/core.cljs b/src/status_im/accounts/update/core.cljs index 02da9befdd..3a278a444b 100644 --- a/src/status_im/accounts/update/core.cljs +++ b/src/status_im/accounts/update/core.cljs @@ -1,9 +1,46 @@ (ns status-im.accounts.update.core (:require [status-im.data-store.accounts :as accounts-store] [status-im.transport.message.protocol :as protocol] + [status-im.data-store.transport :as transport-store] [status-im.transport.message.contact :as message.contact] [status-im.utils.fx :as fx])) +(fx/defn account-update-message [{:keys [db]}] + (let [account (:account/account db) + fcm-token (get-in db [:notifications :fcm-token]) + {:keys [name photo-path address]} account] + (message.contact/ContactUpdate. name photo-path address fcm-token))) + +(fx/defn send-contact-update-fx + [{:keys [db] :as cofx} chat-id payload] + (when-let [chat (get-in cofx [:db :transport/chats chat-id])] + (let [updated-chat (assoc chat :resend? "contact-update") + tx [(transport-store/save-transport-tx {:chat-id chat-id + :chat updated-chat})] + success-event [:transport/contact-message-sent chat-id]] + (fx/merge cofx + {:db (assoc-in db + [:transport/chats chat-id :resend?] + "contact-update") + :data-store/tx tx} + (protocol/send-with-pubkey {:chat-id chat-id + :payload payload + :success-event success-event}))))) + +(fx/defn contact-public-keys [{:keys [db]}] + (reduce (fn [acc [_ {:keys [public-key dapp? pending?]}]] + (if (and (not dapp?) + (not pending?)) + (conj acc public-key) + acc)) + #{} + (:contacts/contacts db))) + +(fx/defn send-contact-update [cofx payload] + (let [public-keys (contact-public-keys cofx)] + ;;NOTE: chats with contacts use public-key as chat-id + (map #(send-contact-update-fx % payload) public-keys))) + (fx/defn account-update "Takes effects (containing :db) + new account fields, adds all effects necessary for account update. Optionally, one can specify a success-event to be dispatched after fields are persisted." @@ -18,7 +55,7 @@ (if (or (:name new-account-fields) (:photo-path new-account-fields)) (fx/merge cofx fx - #(protocol/send (message.contact/ContactUpdate. name photo-path address fcm-token) nil %)) + #(protocol/send (account-update-message %) nil %)) fx))) (fx/defn clean-seed-phrase diff --git a/src/status_im/accounts/update/publisher.cljs b/src/status_im/accounts/update/publisher.cljs new file mode 100644 index 0000000000..6415bb6f6b --- /dev/null +++ b/src/status_im/accounts/update/publisher.cljs @@ -0,0 +1,43 @@ +(ns status-im.accounts.update.publisher + (:require [status-im.constants :as constants] + [status-im.accounts.update.core :as accounts] + [status-im.pairing.core :as pairing] + [status-im.data-store.accounts :as accounts-store] + [status-im.transport.shh :as shh] + [status-im.utils.fx :as fx])) + +;; Publish updates every 48 hours +(def publish-updates-interval (* 48 60 60 1000)) + +(defn publish-update! [{:keys [db now web3]}] + (let [my-public-key (get-in db [:account/account :public-key]) + peers-count (:peers-count db) + last-updated (get-in + db + [:account/account :last-updated])] + (when (and (pos? peers-count) + (pos? last-updated) + (< publish-updates-interval + (- now last-updated))) + (let [public-keys (accounts/contact-public-keys {:db db}) + payload (accounts/account-update-message {:db db}) + sync-message (pairing/sync-installation-account-message {:db db})] + (doseq [pk public-keys] + (shh/send-direct-message! + web3 + {:pubKey pk + :sig my-public-key + :chat constants/contact-discovery + :payload payload} + [:accounts.update.callback/published] + [:accounts.update.callback/failed-to-publish] + 1)) + (shh/send-direct-message! + web3 + {:pubKey my-public-key + :sig my-public-key + :chat constants/contact-discovery + :payload sync-message} + [:accounts.update.callback/published] + [:accounts.update.callback/failed-to-publish] + 1))))) diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index 9fcc5c1cc1..68738d1834 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -4,8 +4,10 @@ [status-im.data-store.chats :as chats-store] [status-im.data-store.messages :as messages-store] [status-im.data-store.user-statuses :as user-statuses-store] + [status-im.contact-code.core :as contact-code] [status-im.i18n :as i18n] [status-im.transport.chat.core :as transport.chat] + [status-im.transport.utils :as transport.utils] [status-im.transport.message.protocol :as protocol] [status-im.transport.message.public-chat :as public-chat] [status-im.ui.components.colors :as colors] @@ -28,6 +30,9 @@ ([cofx chat-id] (multi-user-chat? (get-chat cofx chat-id)))) +(def one-to-one-chat? + (complement multi-user-chat?)) + (defn public-chat? ([chat] (:public? chat)) @@ -142,6 +147,8 @@ (transport.chat/unsubscribe-from-chat % chat-id)) (deactivate-chat chat-id) (clear-history chat-id) + #(when (one-to-one-chat? % chat-id) + (contact-code/stop-listening % chat-id)) (navigation/navigate-to-cofx :home {}))) (fx/defn send-messages-seen @@ -217,10 +224,12 @@ (fx/defn preload-chat-data "Takes chat-id and coeffects map, returns effects necessary when navigating to chat" [{:keys [db] :as cofx} chat-id] - (fx/merge cofx - {:db (-> (assoc db :current-chat-id chat-id) - (set-chat-ui-props {:validation-messages nil}))} - (mark-messages-seen chat-id))) + (let [chat (get-in db [:chats chat-id])] + (fx/merge cofx + {:db (-> (assoc db :current-chat-id chat-id) + (set-chat-ui-props {:validation-messages nil}))} + (contact-code/listen-to-chat chat-id) + (mark-messages-seen chat-id)))) (fx/defn navigate-to-chat "Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data" diff --git a/src/status_im/contact/core.cljs b/src/status_im/contact/core.cljs index be6c84849b..0df399dc0f 100644 --- a/src/status_im/contact/core.cljs +++ b/src/status_im/contact/core.cljs @@ -7,8 +7,11 @@ [status-im.data-store.messages :as data-store.messages] [status-im.data-store.chats :as data-store.chats] [status-im.i18n :as i18n] + [status-im.transport.utils :as transport.utils] [status-im.transport.message.contact :as message.contact] + [status-im.transport.message.public-chat :as transport.public-chat] [status-im.transport.message.protocol :as protocol] + [status-im.contact-code.core :as contact-code] [status-im.ui.screens.add-new.new-chat.db :as new-chat.db] [status-im.ui.screens.navigation :as navigation] [status-im.utils.fx :as fx] @@ -47,16 +50,15 @@ :address address :fcm-token fcm-token})) -(fx/defn add-new-contact [{:keys [db]} {:keys [public-key] :as contact}] - (let [new-contact (assoc contact - :pending? false - :hide-contact? false - :public-key public-key)] - {:db (-> db - (update-in [:contacts/contacts public-key] - merge new-contact) - (assoc-in [:contacts/new-identity] "")) - :data-store/tx [(contacts-store/save-contact-tx new-contact)]})) +(fx/defn upsert-contact [{:keys [db] :as cofx} + {:keys [pending? + public-key] :as contact}] + (fx/merge cofx + {:db (-> db + (update-in [:contacts/contacts public-key] merge contact)) + :data-store/tx [(contacts-store/save-contact-tx contact)]} + #(when-not pending? + (contact-code/listen-to-chat % public-key)))) (fx/defn send-contact-request [{:keys [db] :as cofx} {:keys [public-key pending? dapp?] :as contact}] @@ -65,11 +67,16 @@ (protocol/send (message.contact/map->ContactRequestConfirmed (own-info db)) public-key cofx) (protocol/send (message.contact/map->ContactRequest (own-info db)) public-key cofx)))) -(fx/defn add-contact [{:keys [db] :as cofx} public-key] +(fx/defn add-contact + "Add a contact and set pending to false" + [{:keys [db] :as cofx} public-key] (when (not= (get-in db [:account/account :public-key]) public-key) - (let [contact (build-contact cofx public-key)] + (let [contact (-> (build-contact cofx public-key) + (assoc :pending? false + :hide-contact? false))] (fx/merge cofx - (add-new-contact contact) + {:db (assoc-in db [:contacts/new-identity] "")} + (upsert-contact contact) (send-contact-request contact))))) (fx/defn add-contacts-filter [{:keys [db]} public-key action] @@ -244,10 +251,7 @@ (when-not (= contact-props (select-keys contact [:public-key :address :photo-path :name :fcm-token :pending?])) - {:db (update-in db [:contacts/contacts public-key] - merge contact-props) - :data-store/tx [(contacts-store/save-contact-tx - contact-props)]}))))) + (upsert-contact cofx contact-props)))))) (def receive-contact-request handle-contact-update) (def receive-contact-request-confirmation handle-contact-update) diff --git a/src/status_im/contact_code/core.cljs b/src/status_im/contact_code/core.cljs new file mode 100644 index 0000000000..2634ee1793 --- /dev/null +++ b/src/status_im/contact_code/core.cljs @@ -0,0 +1,94 @@ +(ns status-im.contact-code.core + "This namespace is used to listen for and publish contact-codes. We want to listen + to contact codes once we engage in the conversation with someone, or once someone is + in our contacts." + (:require + [taoensso.timbre :as log] + [status-im.utils.fx :as fx] + [status-im.transport.shh :as shh] + [status-im.transport.message.public-chat :as transport.public-chat] + [status-im.data-store.accounts :as data-store.accounts] + [status-im.transport.chat.core :as transport.chat] + [status-im.accounts.db :as accounts.db])) + +(defn topic [pk] + (str pk "-contact-code")) + +(fx/defn listen [cofx chat-id] + (transport.public-chat/join-public-chat + cofx + (topic chat-id))) + +(fx/defn listen-to-chat + "For a one-to-one chat, listen to the pk of the user, for a group chat + listen for any member" + [cofx chat-id] + (let [{:keys [members + public? + is-active + group-chat]} (get-in cofx [:db :chats chat-id])] + (when is-active + (cond + (and group-chat + (not public?)) + (apply fx/merge cofx + (map listen members)) + (not public?) + (listen cofx chat-id))))) + +(fx/defn stop-listening + "We can stop listening to contact codes when we don't have any active chat + with the user (one-to-one or group-chat), and it is not in our contacts" + [{:keys [db] :as cofx} their-public-key] + (let [my-public-key (accounts.db/current-public-key cofx) + active-group-chats (filter (fn [{:keys [is-active members members-joined]}] + (and is-active + (contains? members-joined my-public-key) + (contains? members their-public-key))) + (vals (:chats db)))] + (when (and (not= false (get-in db [:contacts/contacts their-public-key :pending?])) + (not= my-public-key their-public-key) + (not (get-in db [:chats their-public-key :is-active])) + (empty? active-group-chats)) + + (fx/merge + cofx + (transport.chat/unsubscribe-from-chat (topic their-public-key)))))) + +;; Publish contact code every 12hrs +(def publish-contact-code-interval (* 12 60 60 1000)) + +(fx/defn init [cofx] + (log/debug "initializing contact-code") + (let [current-public-key (accounts.db/current-public-key cofx)] + (listen cofx current-public-key))) + +(defn publish! [{:keys [web3 now] :as cofx}] + (let [current-public-key (accounts.db/current-public-key cofx) + chat-id (topic current-public-key) + peers-count (:peers-count @re-frame.db/app-db) + last-published (get-in + @re-frame.db/app-db + [:account/account :last-published-contact-code])] + (when (and (pos? peers-count) + (< publish-contact-code-interval + (- now last-published))) + + (let [message {:chat chat-id + :sig current-public-key + :payload ""}] + (shh/send-public-message! + web3 + message + [:contact-code.callback/contact-code-published] + :contact-code.callback/contact-code-publishing-failed))))) + +(fx/defn published [{:keys [now db] :as cofx}] + (let [new-account (assoc (:account/account db) + :last-published-contact-code + now)] + {:db (assoc db :account/account new-account) + :data-store/base-tx [(data-store.accounts/save-account-tx new-account)]})) + +(fx/defn publishing-failed [cofx] + (log/warn "failed to publish contact-code")) diff --git a/src/status_im/data_store/realm/schemas/base/account.cljs b/src/status_im/data_store/realm/schemas/base/account.cljs index 008b465d83..3a34ce04cb 100644 --- a/src/status_im/data_store/realm/schemas/base/account.cljs +++ b/src/status_im/data_store/realm/schemas/base/account.cljs @@ -229,4 +229,7 @@ (def v19 (update v18 :properties merge {:stickers {:type "string[]" :optional true} :recent-stickers - {:type "string[]" :optional true}})) \ No newline at end of file + {:type "string[]" :optional true}})) +(def v20 (assoc-in v19 + [:properties :last-published-contact-code] + {:type :int :default 0})) diff --git a/src/status_im/data_store/realm/schemas/base/core.cljs b/src/status_im/data_store/realm/schemas/base/core.cljs index e0bb0d8251..b670bf341d 100644 --- a/src/status_im/data_store/realm/schemas/base/core.cljs +++ b/src/status_im/data_store/realm/schemas/base/core.cljs @@ -93,6 +93,11 @@ (def v24 v23) +(def v25 [network/v1 + bootnode/v4 + extension/v12 + account/v20]) + ;; put schemas ordered by version (def schemas [{:schema v1 :schemaVersion 1 @@ -165,4 +170,7 @@ :migration (constantly nil)} {:schema v24 :schemaVersion 24 - :migration migrations/v24}]) + :migration migrations/v24} + {:schema v25 + :schemaVersion 25 + :migration (constantly nil)}]) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index ae354b4c09..cc27f0af97 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -31,6 +31,7 @@ [status-im.network.core :as network] [status-im.notifications.core :as notifications] [status-im.pairing.core :as pairing] + [status-im.contact-code.core :as contact-code] [status-im.privacy-policy.core :as privacy-policy] [status-im.protocol.core :as protocol] [status-im.qr-scanner.core :as qr-scanner] @@ -154,6 +155,17 @@ (fn [cofx _] (accounts.update/account-update cofx {:mainnet-warning-shown? true} {}))) +(handlers/register-handler-fx + :accounts.update.callback/published + (fn [{:keys [now] :as cofx} _] + (accounts.update/account-update cofx {:last-updated now} {}))) + +(handlers/register-handler-fx + :accounts.update.callback/failed-to-publish + (fn [{:keys [now] :as cofx} [_ message]] + (log/warn "failed to publish account update" message) + (accounts.update/account-update cofx {:last-updated now} {}))) + (handlers/register-handler-fx :accounts.ui/dev-mode-switched (fn [cofx [_ dev-mode?]] @@ -1583,6 +1595,16 @@ (fn [cofx [_ initial-props]] {:db (assoc (:db cofx) :initial-props initial-props)})) +(handlers/register-handler-fx + :contact-code.callback/contact-code-published + (fn [cofx arg] + (contact-code/published cofx))) + +(handlers/register-handler-fx + :contact-code.callback/contact-code-publishing-failed + (fn [cofx _] + (contact-code/publishing-failed cofx))) + (handlers/register-handler-fx :pairing.ui/enable-installation-pressed (fn [cofx [_ installation-id]] diff --git a/src/status_im/group_chats/core.cljs b/src/status_im/group_chats/core.cljs index 3dd0f69f59..210e8427fd 100644 --- a/src/status_im/group_chats/core.cljs +++ b/src/status_im/group_chats/core.cljs @@ -10,6 +10,7 @@ [status-im.utils.clocks :as utils.clocks] [status-im.chat.models.message :as models.message] [status-im.contact.core :as models.contact] + [status-im.contact-code.core :as contact-code] [status-im.native-module.core :as native-module] [status-im.transport.utils :as transport.utils] [status-im.transport.db :as transport.db] @@ -483,14 +484,30 @@ from))) last)) -(fx/defn set-up-topic [cofx chat-id previous-chat] +(fx/defn set-up-topic + "Listen/Tear down the shared topic/contact-codes. Stop listening for members who + have left the chat" + [cofx chat-id previous-chat] (let [my-public-key (accounts.db/current-public-key cofx) new-chat (get-in cofx [:db :chats chat-id])] ;; If we left the chat, teardown, otherwise upsert (if (and (joined? my-public-key previous-chat) (not (joined? my-public-key new-chat))) - (transport.chat/unsubscribe-from-chat cofx chat-id) - (transport.public-chat/join-group-chat cofx chat-id)))) + (apply fx/merge + cofx + (conj + (map #(contact-code/stop-listening %) + (:members new-chat)) + (transport.chat/unsubscribe-from-chat chat-id))) + (apply fx/merge + cofx + (concat + (map #(contact-code/listen-to-chat %) + (:members new-chat)) + (map #(contact-code/stop-listening %) + (clojure.set/difference (:members previous-chat) + (:members new-chat))) + [(transport.public-chat/join-group-chat chat-id)]))))) (fx/defn handle-membership-update "Upsert chat and receive message if valid" diff --git a/src/status_im/models/transactions.cljs b/src/status_im/models/transactions.cljs index 539d47fad4..9965e76a90 100644 --- a/src/status_im/models/transactions.cljs +++ b/src/status_im/models/transactions.cljs @@ -239,56 +239,6 @@ account-address success-fn))) -;; --------------------------------------------------------------------------- -;; Periodic background job -;; --------------------------------------------------------------------------- - -(defn- async-periodic-run! - ([async-periodic-chan] - (async-periodic-run! async-periodic-chan true)) - ([async-periodic-chan worker-fn] - (async/put! async-periodic-chan worker-fn))) - -(defn- async-periodic-stop! [async-periodic-chan] - (async/close! async-periodic-chan)) - -(defn- async-periodic-exec - "Periodically execute an function. - - Takes a work-fn of one argument `finished-fn -> any` this function - is passed a finished-fn that must be called to signal that the work - being performed in the work-fn is finished. - - Returns a go channel that represents a way to control the looping process. - - Stop the polling loop with `async-periodic-stop!` - - The work-fn can be forced to run immediately with `async-periodic-run!` - - Or you can queue up another fn `finished-fn -> any` to execute on - the queue with `async-periodic-run!`." - [work-fn interval-ms timeout-ms] - {:pre [(fn? work-fn) (integer? interval-ms) (integer? timeout-ms)]} - (let [do-now-chan (async/chan (async/sliding-buffer 1)) - try-it (fn [exec-fn catch-fn] (try (exec-fn) (catch :default e (catch-fn e))))] - (go-loop [] - (let [timeout (async-util/timeout interval-ms) - finished-chan (async/promise-chan) - [v ch] (async/alts! [do-now-chan timeout]) - worker (if (and (= ch do-now-chan) (fn? v)) - v work-fn)] - (when-not (and (= ch do-now-chan) (nil? v)) - ;; don't let try catch be parsed by go-block - (try-it #(worker (fn [] (async/put! finished-chan true))) - (fn [e] - (log/error "failed to run transaction sync" e) - ;; if an error occurs in work-fn log it and consider it done - (async/put! finished-chan true))) - ;; sanity timeout for work-fn - (async/alts! [finished-chan (async-util/timeout timeout-ms)]) - (recur)))) - do-now-chan)) - ;; ----------------------------------------------------------------------------- ;; Helpers functions that help determine if a background sync should execute ;; ----------------------------------------------------------------------------- @@ -396,7 +346,7 @@ (when (and (not= network-status :offline) (= app-state "active") (not= :custom chain)) - (async-periodic-run! + (async-util/async-periodic-run! @polling-executor (partial transactions-query-helper web3 all-tokens account-address chain)))))) @@ -421,9 +371,9 @@ (defn- start-sync! [{:keys [:account/account network web3] :as options}] (let [account-address (:address account)] (when @polling-executor - (async-periodic-stop! @polling-executor)) + (async-util/async-periodic-stop! @polling-executor)) (reset! polling-executor - (async-periodic-exec + (async-util/async-periodic-exec (partial #'background-sync web3 account-address) sync-interval-ms sync-timeout-ms))) @@ -442,7 +392,7 @@ (re-frame/reg-fx ::stop-sync-transactions #(when @polling-executor - (async-periodic-stop! @polling-executor))) + (async-util/async-periodic-stop! @polling-executor))) (fx/defn stop-sync [_] {::stop-sync-transactions nil}) diff --git a/src/status_im/pairing/core.cljs b/src/status_im/pairing/core.cljs index 91012c272a..870422b23f 100644 --- a/src/status_im/pairing/core.cljs +++ b/src/status_im/pairing/core.cljs @@ -7,11 +7,15 @@ [status-im.utils.config :as config] [status-im.utils.platform :as utils.platform] [status-im.chat.models :as models.chat] + [status-im.transport.message.public-chat :as transport.public-chat] [status-im.accounts.db :as accounts.db] [status-im.transport.message.protocol :as protocol] + [status-im.transport.utils :as transport.utils] [status-im.data-store.installations :as data-store.installations] [status-im.native-module.core :as native-module] [status-im.utils.identicon :as identicon] + [status-im.contact.core :as contact] + [status-im.contact-code.core :as contact-code] [status-im.data-store.contacts :as data-store.contacts] [status-im.data-store.accounts :as data-store.accounts] [status-im.transport.message.pairing :as transport.pairing])) @@ -227,16 +231,17 @@ (defn handle-sync-installation [{:keys [db] :as cofx} {:keys [contacts account chat]} sender] (when (= sender (accounts.db/current-public-key cofx)) - (let [new-contacts (merge-contacts (:contacts/contacts db) (ensure-photo-path contacts)) - new-account (merge-account (:account/account db) account)] - (fx/merge cofx - {:db (assoc db - :contacts/contacts new-contacts - :account/account new-account) - :data-store/base-tx [(data-store.accounts/save-account-tx new-account)] - :data-store/tx [(data-store.contacts/save-contacts-tx (vals new-contacts))]} - #(when (:public? chat) - (models.chat/start-public-chat % (:chat-id chat) {:dont-navigate? true})))))) + (let [new-contacts (vals (merge-contacts (:contacts/contacts db) (ensure-photo-path contacts))) + new-account (merge-account (:account/account db) account) + contacts-fx (mapv contact/upsert-contact new-contacts)] + (apply fx/merge + cofx + (concat + [{:db (assoc db :account/account new-account) + :data-store/base-tx [(data-store.accounts/save-account-tx new-account)]} + #(when (:public? chat) + (models.chat/start-public-chat % (:chat-id chat) {:dont-navigate? true}))] + contacts-fx))))) (defn handle-pair-installation [{:keys [db] :as cofx} {:keys [name installation-id device-type]} timestamp sender] (when (and (= sender (accounts.db/current-public-key cofx)) diff --git a/src/status_im/transport/core.cljs b/src/status_im/transport/core.cljs index 2c1b792a49..3a93199ba1 100644 --- a/src/status_im/transport/core.cljs +++ b/src/status_im/transport/core.cljs @@ -5,6 +5,8 @@ [status-im.mailserver.core :as mailserver] [status-im.transport.message.core :as message] [status-im.transport.partitioned-topic :as transport.topic] + [status-im.contact-code.core :as contact-code] + [status-im.utils.publisher :as publisher] [status-im.utils.fx :as fx] [status-im.utils.handlers :as handlers] [taoensso.timbre :as log] @@ -54,6 +56,8 @@ (assoc chat :chat-id chat-id))) (:transport/chats db)) :on-success #(re-frame/dispatch [::sym-keys-added %])}} + (publisher/start-fx) + (contact-code/init) (mailserver/connect-to-mailserver) (message/resend-contact-messages []))))) @@ -100,11 +104,15 @@ It is necessary to remove the filters because status-go there isn't currently a logout feature in status-go to clean-up after logout. When logging out of account A and logging in account B, account B would receive account A messages without this." - [{:keys [db]} callback] + [{:keys [db] :as cofx} callback] (let [{:transport/keys [filters]} db] - {:shh/remove-filters {:filters (mapcat (fn [[chat-id chat-filters]] - (map (fn [filter] - [chat-id filter]) - chat-filters)) - filters) - :callback callback}})) + (fx/merge + cofx + + {:shh/remove-filters {:filters (mapcat (fn [[chat-id chat-filters]] + (map (fn [filter] + [chat-id filter]) + chat-filters)) + filters) + :callback callback}} + (publisher/stop-fx)))) diff --git a/src/status_im/transport/filters.cljs b/src/status_im/transport/filters.cljs index e88c3366c3..d54f86da40 100644 --- a/src/status_im/transport/filters.cljs +++ b/src/status_im/transport/filters.cljs @@ -108,8 +108,9 @@ (re-frame/reg-fx :shh/remove-filter - (fn [{:keys [filter] :as params}] - (when filter (remove-filter! params)))) + (fn [filters] + (doseq [{:keys [filter] :as params} filters] + (when filter (remove-filter! params))))) (re-frame/reg-fx :shh/remove-filters diff --git a/src/status_im/transport/impl/send.cljs b/src/status_im/transport/impl/send.cljs index 6e694c55b2..2ff6613cfe 100644 --- a/src/status_im/transport/impl/send.cljs +++ b/src/status_im/transport/impl/send.cljs @@ -2,6 +2,7 @@ (:require [status-im.group-chats.core :as group-chats] [status-im.utils.fx :as fx] [status-im.pairing.core :as pairing] + [status-im.accounts.update.core :as accounts.update] [status-im.data-store.transport :as transport-store] [status-im.transport.db :as transport.db] [status-im.transport.message.pairing :as transport.pairing] @@ -67,33 +68,10 @@ :success-event success-event}) (pairing/send-installation-message-fx sync-message))))) -(fx/defn send-contact-update - [{:keys [db] :as cofx} chat-id payload] - (when-let [chat (get-in cofx [:db :transport/chats chat-id])] - (let [updated-chat (assoc chat :resend? "contact-update") - tx [(transport-store/save-transport-tx {:chat-id chat-id - :chat updated-chat})] - success-event [:transport/contact-message-sent chat-id]] - (fx/merge cofx - {:db (assoc-in db - [:transport/chats chat-id :resend?] - "contact-update") - :data-store/tx tx} - (protocol/send-with-pubkey {:chat-id chat-id - :payload payload - :success-event success-event}))))) (extend-type transport.contact/ContactUpdate protocol/StatusMessage (send [this _ {:keys [db] :as cofx}] - (let [contact-public-keys (reduce (fn [acc [_ {:keys [public-key dapp? pending?]}]] - (if (and (not dapp?) - (not pending?)) - (conj acc public-key) - acc)) - #{} - (:contacts/contacts db)) - ;;NOTE: chats with contacts use public-key as chat-id - send-contact-update-fxs (map #(send-contact-update % this) contact-public-keys) + (let [send-contact-update-fxs (accounts.update/send-contact-update cofx this) sync-message (pairing/sync-installation-account-message cofx) fxs (conj send-contact-update-fxs (pairing/send-installation-message-fx sync-message))] diff --git a/src/status_im/transport/shh.cljs b/src/status_im/transport/shh.cljs index 2b7f0900a5..7b1f9d0694 100644 --- a/src/status_im/transport/shh.cljs +++ b/src/status_im/transport/shh.cljs @@ -69,23 +69,24 @@ (log/debug :shh/post-success)) (re-frame/dispatch [error-event err resp])))) +(defn send-direct-message! [web3 direct-message success-event error-event count] + (.. web3 + -shh + (sendDirectMessage + (clj->js (update direct-message :payload (comp transport.utils/from-utf8 + transit/serialize))) + (handle-response success-event error-event count)))) + (re-frame/reg-fx :shh/send-direct-message (fn [post-calls] (doseq [{:keys [web3 payload src dst success-event error-event] :or {error-event :transport/send-status-message-error}} post-calls] - (let [chat (transport.topic/public-key->discovery-topic dst) - direct-message (clj->js {:pubKey dst - :sig src - :chat chat - :payload (-> payload - transit/serialize - transport.utils/from-utf8)})] - (.. web3 - -shh - (sendDirectMessage - direct-message - (handle-response success-event error-event 1))))))) + (let [direct-message {:pubKey dst + :sig src + :chat (transport.topic/public-key->discovery-topic dst) + :payload payload}] + (send-direct-message! web3 direct-message success-event error-event 1))))) (re-frame/reg-fx :shh/send-pairing-message @@ -123,21 +124,24 @@ message (handle-response success-event error-event (count dsts))))))))) +(defn send-public-message! [web3 message success-event error-event] + (.. web3 + -shh + (sendPublicMessage + (clj->js message) + (handle-response success-event error-event 1)))) + (re-frame/reg-fx :shh/send-public-message (fn [post-calls] (doseq [{:keys [web3 payload src chat success-event error-event] :or {error-event :transport/send-status-message-error}} post-calls] - (let [message (clj->js {:chat chat - :sig src - :payload (-> payload - transit/serialize - transport.utils/from-utf8)})] - (.. web3 - -shh - (sendPublicMessage - message - (handle-response success-event error-event 1))))))) + (let [message {:chat chat + :sig src + :payload (-> payload + transit/serialize + transport.utils/from-utf8)}] + (send-public-message! web3 message success-event error-event))))) (re-frame/reg-fx :shh/post diff --git a/src/status_im/utils/async.cljs b/src/status_im/utils/async.cljs index c46845ddb6..1b23b06561 100644 --- a/src/status_im/utils/async.cljs +++ b/src/status_im/utils/async.cljs @@ -50,3 +50,53 @@ (run-task task-fn) (recur (async/ any` this function + is passed a finished-fn that must be called to signal that the work + being performed in the work-fn is finished. + + Returns a go channel that represents a way to control the looping process. + + Stop the polling loop with `async-periodic-stop!` + + The work-fn can be forced to run immediately with `async-periodic-run!` + + Or you can queue up another fn `finished-fn -> any` to execute on + the queue with `async-periodic-run!`." + [work-fn interval-ms timeout-ms] + {:pre [(fn? work-fn) (integer? interval-ms) (integer? timeout-ms)]} + (let [do-now-chan (async/chan (async/sliding-buffer 1)) + try-it (fn [exec-fn catch-fn] (try (exec-fn) (catch :default e (catch-fn e))))] + (async/go-loop [] + (let [timeout-chan (timeout interval-ms) + finished-chan (async/promise-chan) + [v ch] (async/alts! [do-now-chan timeout-chan]) + worker (if (and (= ch do-now-chan) (fn? v)) + v work-fn)] + (when-not (and (= ch do-now-chan) (nil? v)) + ;; don't let try catch be parsed by go-block + (try-it #(worker (fn [] (async/put! finished-chan true))) + (fn [e] + (log/error "failed to run job" e) + ;; if an error occurs in work-fn log it and consider it done + (async/put! finished-chan true))) + ;; sanity timeout for work-fn + (async/alts! [finished-chan (timeout timeout-ms)]) + (recur)))) + do-now-chan)) diff --git a/src/status_im/utils/fx.cljs b/src/status_im/utils/fx.cljs index 2e66f366ac..13cb7535d7 100644 --- a/src/status_im/utils/fx.cljs +++ b/src/status_im/utils/fx.cljs @@ -12,7 +12,7 @@ (def ^:private mergable-keys #{:data-store/tx :data-store/base-tx :chat-received-message/add-fx :shh/add-new-sym-keys :shh/get-new-sym-keys :shh/post - :shh/send-direct-message + :shh/send-direct-message :shh/remove-filter :shh/generate-sym-key-from-password :transport/confirm-messages-processed :group-chats/extract-membership-signature :utils/dispatch-later}) diff --git a/src/status_im/utils/publisher.cljs b/src/status_im/utils/publisher.cljs new file mode 100644 index 0000000000..cae8201f1f --- /dev/null +++ b/src/status_im/utils/publisher.cljs @@ -0,0 +1,42 @@ +(ns status-im.utils.publisher + (:require [re-frame.core :as re-frame] + [re-frame.db] + [status-im.accounts.update.publisher :as accounts] + [status-im.contact-code.core :as contact-code] + [status-im.utils.async :as async-util] + [status-im.utils.datetime :as datetime] + [status-im.utils.fx :as fx])) + +(defonce polling-executor (atom nil)) +(def sync-interval-ms 120000) +(def sync-timeout-ms 20000) + +(defn- start-publisher! [web3] + (when @polling-executor + (async-util/async-periodic-stop! @polling-executor)) + (reset! polling-executor + (async-util/async-periodic-exec + (fn [done-fn] + (let [cofx {:web3 web3 + :now (datetime/timestamp) + :db @re-frame.db/app-db}] + (accounts/publish-update! cofx) + (contact-code/publish! cofx) + (done-fn))) + sync-interval-ms + sync-timeout-ms))) + +(re-frame/reg-fx + ::start-publisher + #(start-publisher! %)) + +(re-frame/reg-fx + ::stop-publisher + #(when @polling-executor + (async-util/async-periodic-stop! @polling-executor))) + +(fx/defn start-fx [{:keys [web3]}] + {::start-publisher web3}) + +(fx/defn stop-fx [cofx] + {::stop-publisher []}) diff --git a/test/cljs/status_im/test/chat/models.cljs b/test/cljs/status_im/test/chat/models.cljs index e3e8175aaf..0f37a1c83b 100644 --- a/test/cljs/status_im/test/chat/models.cljs +++ b/test/cljs/status_im/test/chat/models.cljs @@ -168,7 +168,7 @@ (testing "it adds the relevant transactions for realm" (let [actual (chat/remove-chat cofx chat-id)] (is (:data-store/tx actual)) - (is (= 3 (count (:data-store/tx actual)))))))) + (is (= 5 (count (:data-store/tx actual)))))))) (deftest multi-user-chat? (let [chat-id "1"] diff --git a/test/cljs/status_im/test/contact_code/core.cljs b/test/cljs/status_im/test/contact_code/core.cljs new file mode 100644 index 0000000000..d8f5d08972 --- /dev/null +++ b/test/cljs/status_im/test/contact_code/core.cljs @@ -0,0 +1,78 @@ +(ns status-im.test.contact-code.core + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.contact-code.core :as contact-code])) + +(def me "me") +(def member-1 "member-1") +(def member-1-topic "member-1-contact-code") +(def member-2 "member-2") +(def member-2-topic "member-2-contact-code") +(def chat-id "chat-id") +(def chat-id-topic "chat-id-contact-code") + +(deftest listen-to-chat + (testing "an inactive chat" + (testing "it does nothing" + (is (not (contact-code/listen-to-chat {:db {}} chat-id))))) + (testing "an active 1-to-1 chat" + (testing "it listen to the topic" + (is (get-in + (contact-code/listen-to-chat {:db {:chats {chat-id {:is-active true}}}} + chat-id) + [:db :transport/chats chat-id-topic])))) + (testing "an active group chat" + (testing "it listen to any member" + (let [transport (get-in + (contact-code/listen-to-chat {:db {:chats {chat-id + {:is-active true + :group-chat true + :members #{member-1 + member-2}}}}} + chat-id) + [:db :transport/chats])] + (is (not (get transport chat-id-topic))) + (is (get transport member-1-topic)) + (is (get transport member-2-topic)))))) + +(deftest stop-listening + (testing "the user is in our contacts" + (testing "it does not remove transport" + (is (not (contact-code/stop-listening {:db {:contacts/contacts + {chat-id {:pending? false}}}} + chat-id))))) + (testing "the user is not in our contacts" + (testing "the user is not in any group chats or 1-to1-" + (testing "it removes the transport" + (let [transport (contact-code/stop-listening {:db {:transport/chats + {chat-id-topic {}}}} + chat-id)] + (is transport) + (is (not (get transport chat-id-topic)))))) + (testing "the user is still in some group chats" + (testing "we joined, and group chat is active it does not remove transport" + (let [transport (contact-code/stop-listening {:db {:account/account {:public-key me} + :chats + {chat-id {:is-active true + :members-joined #{me} + :members #{member-1}}} + :transport/chats + {member-1-topic {}}}} + member-1)] + (is (not transport)))) + (testing "we didn't join, it removes transport" + (let [transport (contact-code/stop-listening {:db {:account/account {:public-key me} + :chats + {chat-id {:is-active true + :members-joined #{member-1} + :members #{member-1}}} + :transport/chats + {member-1-topic {}}}} + member-1)] + (is transport) + (is (not (get transport member-1-topic)))))) + (testing "we have a 1-to-1 chat with the user" + (testing "it does not remove transport" + (let [transport (contact-code/stop-listening {:db {:chats + {member-1 {:is-active true}}}} + member-1)] + (is (not transport))))))) diff --git a/test/cljs/status_im/test/models/contact.cljs b/test/cljs/status_im/test/models/contact.cljs index 4166304d13..43a7267f7a 100644 --- a/test/cljs/status_im/test/models/contact.cljs +++ b/test/cljs/status_im/test/models/contact.cljs @@ -29,7 +29,7 @@ :profile-image "image" :address "address" :fcm-token "token"} - {}) + {:db {}}) contact (get-in actual [:db :contacts/contacts public-key])] (testing "it stores the contact in the database" (is (:data-store/tx actual))) diff --git a/test/cljs/status_im/test/pairing/core.cljs b/test/cljs/status_im/test/pairing/core.cljs index d2cfc5717a..e74fe600ac 100644 --- a/test/cljs/status_im/test/pairing/core.cljs +++ b/test/cljs/status_im/test/pairing/core.cljs @@ -73,8 +73,7 @@ (is (= expected (pairing/merge-contact contact-1 contact-2)))))) (deftest handle-sync-installation-test - (with-redefs [config/pairing-enabled? (constantly true) - identicon/identicon (constantly "generated")] + (with-redefs [identicon/identicon (constantly "generated")] (testing "syncing contacts" (let [old-contact-1 {:name "old-contact-one" @@ -164,28 +163,27 @@ [:db :chats "status"])))))))) (deftest handle-pair-installation-test - (with-redefs [config/pairing-enabled? (constantly true)] - (let [cofx {:db {:account/account {:public-key "us"} - :pairing/installations {"1" {:has-bundle? true - :installation-id "1"} - "2" {:has-bundle? false - :installation-id "2"}}}} - pair-message {:device-type "ios" - :name "name" - :installation-id "1"}] - (testing "not coming from us" - (is (not (pairing/handle-pair-installation cofx pair-message 1 "not-us")))) - (testing "coming from us" - (is (= {"1" {:has-bundle? true - :installation-id "1" - :name "name" - :last-paired 1 - :device-type "ios"} - "2" {:has-bundle? false - :installation-id "2"}} - (get-in - (pairing/handle-pair-installation cofx pair-message 1 "us") - [:db :pairing/installations]))))))) + (let [cofx {:db {:account/account {:public-key "us"} + :pairing/installations {"1" {:has-bundle? true + :installation-id "1"} + "2" {:has-bundle? false + :installation-id "2"}}}} + pair-message {:device-type "ios" + :name "name" + :installation-id "1"}] + (testing "not coming from us" + (is (not (pairing/handle-pair-installation cofx pair-message 1 "not-us")))) + (testing "coming from us" + (is (= {"1" {:has-bundle? true + :installation-id "1" + :name "name" + :last-paired 1 + :device-type "ios"} + "2" {:has-bundle? false + :installation-id "2"}} + (get-in + (pairing/handle-pair-installation cofx pair-message 1 "us") + [:db :pairing/installations])))))) (deftest sync-installation-messages-test (testing "it creates a sync installation message" @@ -225,23 +223,22 @@ (is (= expected (pairing/sync-installation-messages cofx)))))) (deftest handle-bundles-added-test - (with-redefs [config/pairing-enabled? (constantly true)] - (let [installation-1 {:has-bundle? false - :installation-id "installation-1"} - cofx {:db {:account/account {:public-key "us"} - :pairing/installations {"installation-1" installation-1}}}] - (testing "new installations" - (let [new-installation {:identity "us" :installationID "installation-2"} - expected {"installation-1" installation-1 - "installation-2" {:has-bundle? true - :installation-id "installation-2"}}] - (is (= expected (get-in (pairing/handle-bundles-added cofx new-installation) [:db :pairing/installations]))))) - (testing "already existing installation" - (let [old-installation {:identity "us" :installationID "installation-1"}] - (is (not (pairing/handle-bundles-added cofx old-installation))))) - (testing "not from us" - (let [new-installation {:identity "not-us" :installationID "does-not-matter"}] - (is (not (pairing/handle-bundles-added cofx new-installation)))))))) + (let [installation-1 {:has-bundle? false + :installation-id "installation-1"} + cofx {:db {:account/account {:public-key "us"} + :pairing/installations {"installation-1" installation-1}}}] + (testing "new installations" + (let [new-installation {:identity "us" :installationID "installation-2"} + expected {"installation-1" installation-1 + "installation-2" {:has-bundle? true + :installation-id "installation-2"}}] + (is (= expected (get-in (pairing/handle-bundles-added cofx new-installation) [:db :pairing/installations]))))) + (testing "already existing installation" + (let [old-installation {:identity "us" :installationID "installation-1"}] + (is (not (pairing/handle-bundles-added cofx old-installation))))) + (testing "not from us" + (let [new-installation {:identity "not-us" :installationID "does-not-matter"}] + (is (not (pairing/handle-bundles-added cofx new-installation))))))) (deftest has-paired-installations-test (testing "no paired devices" diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index fd177b5016..869c83856f 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -59,6 +59,7 @@ [status-im.test.accounts.recover.core] [status-im.test.hardwallet.core] [status-im.test.contact-recovery.core] + [status-im.test.contact-code.core] [status-im.test.ui.screens.currency-settings.models] [status-im.test.ui.screens.wallet.db] [status-im.test.sign-in.flow])) @@ -131,6 +132,7 @@ 'status-im.test.ui.screens.wallet.db 'status-im.test.browser.core 'status-im.test.contact-recovery.core + 'status-im.test.contact-code.core 'status-im.test.extensions.ethereum 'status-im.test.browser.permissions 'status-im.test.sign-in.flow)