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 <andrea.maria.piana@gmail.com>
This commit is contained in:
Andrea Maria Piana 2019-01-25 14:07:33 +01:00
parent 6a7efb8339
commit 7960fdef85
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
24 changed files with 547 additions and 192 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -230,3 +230,6 @@
{:type "string[]" :optional true}
:recent-stickers
{:type "string[]" :optional true}}))
(def v20 (assoc-in v19
[:properties :last-published-contact-code]
{:type :int :default 0}))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,3 +50,53 @@
(run-task task-fn)
(recur (async/<! task-queue)))
task-queue))
;; ---------------------------------------------------------------------------
;; 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))))]
(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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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