Update PNs to use data-only messaging, and only encode/decode data values. Fixes #6772

Fix navigation to chat when PN is tapped while signed off. Fixes #3488

Anonymize PN pubkeys. Part of #6772
This commit is contained in:
Pedro Pombeiro 2018-11-20 19:36:11 +01:00 committed by Roman Volosovskyi
parent 4af2073388
commit 5a69b4198e
No known key found for this signature in database
GPG Key ID: 0238A4B5ECEE70DE
26 changed files with 462 additions and 215 deletions

1
.gitignore vendored
View File

@ -156,3 +156,4 @@ conan*.txt
conanbuildinfo.*
conan.cmake
/yarn-error.log
/default.realm/

View File

@ -211,6 +211,8 @@ android-ports: ##@other Add proxies to Android Device/Simulator
adb reverse tcp:4567 tcp:4567
adb forward tcp:5561 tcp:5561
android-logcat:
adb logcat | grep -e StatusModule -e ReactNativeJS -e StatusNativeLogs
startdev-%:
$(eval SYSTEM := $(word 2, $(subst -, , $@)))

View File

@ -1 +1 @@
181221-204011-e80de6
0.19.0-beta.1

View File

@ -68,3 +68,7 @@
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# Firebase
-keep class io.invertase.firebase.** { *; }
-dontwarn io.invertase.firebase.**

View File

@ -13,7 +13,7 @@ pipeline {
timestamps()
disableConcurrentBuilds()
/* Prevent Jenkins jobs from running forever */
timeout(time: 35, unit: 'MINUTES')
timeout(time: 45, unit: 'MINUTES')
/* Limit builds retained */
buildDiscarder(logRotator(
numToKeepStr: '10',

View File

@ -6,7 +6,7 @@ Each business logic module is managed in a module directory.
*There is no rigid structure on how to organize code inside modules outside of core and db namespaces*
```
```txt
- events.cljs
- subs.cljs
- notifications
@ -37,13 +37,13 @@ Core namespace must only contain functions that can be called outside of the mod
- fx producing functions called by events and other modules
```clojure
(def get-current-account
module.db/get-current-account)
```clojure
(def get-current-account
module.db/get-current-account)
(defn set-current-account [{db :db :as cofx}]
{:db (module.db/set-current-account db)})
```
(defn set-current-account [{db :db :as cofx}]
{:db (module.db/set-current-account db)})
```
## db.cljs
@ -61,13 +61,14 @@ These guidelines make db.cljs namespaces the place to go when making changes to
- events must always be declared with `register-handler-fx`, no `register-handler-db`
- events must never use the `trim-v` interceptor
- events must only contain a function call defined in a module
```clojure
(handlers/register-handler-fx
:notifications/handle-push-notification
(fn [cofx [_ event]]
(notifications/handle-push-notification event cofx)))
```
```clojure
(handlers/register-handler-fx
:notifications/handle-push-notification-open
(fn [cofx [_ event]]
(notifications/handle-push-notification-open event cofx)))
```
- events must use synthetic namespaces:
- `:module.ui/` for user triggered events
- `:module.callback/` for callback events, which are events bringing back the result of an fx to the event loop, the name of the event should end with `-success` or `-error` most of the time. Other possibilities can be `-granted`, `-denied` for instance.
- `:module/` for internal events, examples are time based events marked `-timed-out`, external changes marked `-changed` or reception of external events marked `-received`.
- `:module.ui/` for user triggered events
- `:module.callback/` for callback events, which are events bringing back the result of an fx to the event loop, the name of the event should end with `-success` or `-error` most of the time. Other possibilities can be `-granted`, `-denied` for instance.
- `:module/` for internal events, examples are time based events marked `-timed-out`, external changes marked `-changed` or reception of external events marked `-received`.

View File

@ -105,12 +105,12 @@ QList<ModuleMethod *> DesktopNotification::methodsToExport() {
QVariantMap DesktopNotification::constantsToExport() { return QVariantMap(); }
void DesktopNotification::sendNotification(QString title, QString body, bool prioritary) {
void DesktopNotification::displayNotification(QString title, QString body, bool prioritary) {
Q_D(DesktopNotification);
qCDebug(NOTIFICATION) << "::sendNotification";
qCDebug(NOTIFICATION) << "::displayNotification";
if (m_appHasFocus) {
qCDebug(NOTIFICATION) << "Not sending notification since an application window is active";
qCDebug(NOTIFICATION) << "Not displaying notification since an application window is active";
return;
}

View File

@ -35,7 +35,7 @@ public:
QList<ModuleMethod*> methodsToExport() override;
QVariantMap constantsToExport() override;
Q_INVOKABLE void sendNotification(QString title, QString body, bool prioritary);
Q_INVOKABLE void displayNotification(QString title, QString body, bool prioritary);
Q_INVOKABLE void setDockBadgeLabel(const QString label);
private:
QScopedPointer<DesktopNotificationPrivate> d_ptr;

View File

@ -81,7 +81,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
}
@Override
public void onHostResume() { // Actvity `onResume`
public void onHostResume() { // Activity `onResume`
module = this;
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
@ -459,7 +459,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
}
@ReactMethod
public void notifyUsers(final String message, final String payloadJSON, final String tokensJSON, final Callback callback) {
public void notifyUsers(final String dataPayloadJSON, final String tokensJSON, final Callback callback) {
Log.d(TAG, "notifyUsers");
if (!checkAvailability()) {
callback.invoke(false);
@ -469,7 +469,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
Runnable r = new Runnable() {
@Override
public void run() {
String res = Statusgo.NotifyUsers(message, payloadJSON, tokensJSON);
String res = Statusgo.NotifyUsers(dataPayloadJSON, tokensJSON);
callback.invoke(res);
}

View File

@ -136,14 +136,14 @@ void RCTStatus::createAccount(QString password, double callbackId) {
}
void RCTStatus::notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId) {
void RCTStatus::notifyUsers(QString dataPayloadJSON, QString tokensJSON, double callbackId) {
Q_D(RCTStatus);
qCDebug(RCTSTATUS) << "::notifyUsers call - callbackId:" << callbackId;
QtConcurrent::run([&](QString token, QString payloadJSON, QString tokensJSON, double callbackId) {
const char* result = NotifyUsers(token.toUtf8().data(), payloadJSON.toUtf8().data(), tokensJSON.toUtf8().data());
QtConcurrent::run([&](QString dataPayloadJSON, QString tokensJSON, double callbackId) {
const char* result = NotifyUsers(dataPayloadJSON.toUtf8().data(), tokensJSON.toUtf8().data());
logStatusGoResult("::notifyUsers Notify", result);
d->bridge->invokePromiseCallback(callbackId, QVariantList{result});
}, token, payloadJSON, tokensJSON, callbackId);
}, dataPayloadJSON, tokensJSON, callbackId);
}

View File

@ -38,7 +38,7 @@ public:
Q_INVOKABLE void startNode(QString configString);
Q_INVOKABLE void stopNode();
Q_INVOKABLE void createAccount(QString password, double callbackId);
Q_INVOKABLE void notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId);
Q_INVOKABLE void notifyUsers(QString dataPayloadJSON, QString tokensJSON, double callbackId);
Q_INVOKABLE void sendLogs(QString dbJSON);
Q_INVOKABLE void addPeer(QString enode, double callbackId);
Q_INVOKABLE void recoverAccount(QString passphrase, QString password, double callbackId);

View File

@ -185,11 +185,10 @@ RCT_EXPORT_METHOD(createAccount:(NSString *)password
////////////////////////////////////////////////////////////////////
#pragma mark - NotifyUsers method
//////////////////////////////////////////////////////////////////// notifyUsers
RCT_EXPORT_METHOD(notifyUsers:(NSString *)message
payloadJSON:(NSString *)payloadJSON
RCT_EXPORT_METHOD(notifyUsers:(NSString *)dataPayloadJSON
tokensJSON:(NSString *)tokensJSON
callback:(RCTResponseSenderBlock)callback) {
char * result = NotifyUsers((char *) [message UTF8String], (char *) [payloadJSON UTF8String], (char *) [tokensJSON UTF8String]);
char * result = NotifyUsers((char *) [dataPayloadJSON UTF8String], (char *) [tokensJSON UTF8String]);
callback(@[[NSString stringWithUTF8String: result]]);
#if DEBUG
NSLog(@"NotifyUsers() method called");

View File

@ -40,9 +40,7 @@
(.addEventListener react/app-state "change" app-state-change-handler))
:component-did-mount
(fn [this]
(dispatch [:set-initial-props (reagent/props this)])
;; TODO(oskarth): Background click_action handler
(notifications/init))
(dispatch [:set-initial-props (reagent/props this)]))
:component-will-unmount
(fn []
(.stop react/http-bridge)

View File

@ -121,7 +121,7 @@
(get-in db [:account/account :desktop-notifications?])
(< (time/seconds-ago (time/to-date timestamp)) constants/one-earth-day))
(let [{:keys [title body prioritary?]} (build-desktop-notification cofx message)]
(.sendNotification react/desktop-notification title body prioritary?)))
(.displayNotification react/desktop-notification title body prioritary?)))
(fx/merge cofx
{:db (cond->
(-> db
@ -159,19 +159,6 @@
message
(assoc message :clock-value (utils.clocks/send last-clock-value))))
(fx/defn display-notification
[cofx chat-id]
(when config/in-app-notifications-enabled?
(let [view-id (get-in cofx [:db :view-id])
from (accounts.db/current-public-key cofx)
current-chat-id (get-in cofx [:db :current-chat-id])]
(when-not (and (= :chat view-id)
(= current-chat-id chat-id))
{:notifications/display-notification {:title (i18n/label :notifications-new-message-title)
:body (i18n/label :notifications-new-message-body)
:to chat-id
:from from}}))))
(defn check-response-to
[{{:keys [response-to response-to-v2]} :content :as message}
old-id->message]
@ -210,7 +197,6 @@
current-chat? :seen
:else :received))
(commands-receiving/receive message)
(display-notification chat-id)
(send-message-seen chat-id message-id (and (not group-chat)
current-chat?
(not (= constants/system from))
@ -391,14 +377,14 @@
(add-own-status chat-id message-id :sending)
(send chat-id message-id wrapped-record))))
(fx/defn send-push-notification [cofx fcm-token status]
(fx/defn send-push-notification [cofx message-id fcm-token status]
(log/debug "#6772 - send-push-notification" message-id fcm-token)
(when (and fcm-token (= status :sent))
{:send-notification {:message (js/JSON.stringify #js {:from (accounts.db/current-public-key cofx)
:to (get-in cofx [:db :current-chat-id])})
:payload {:title (i18n/label :notifications-new-message-title)
:body (i18n/label :notifications-new-message-body)
:sound notifications/sound-name}
:tokens [fcm-token]}}))
(let [payload {:from (accounts.db/current-public-key cofx)
:to (get-in cofx [:db :current-chat-id])
:id message-id}]
{:send-notification {:data-payload (notifications/encode-notification-payload payload)
:tokens [fcm-token]}})))
(fx/defn update-message-status [{:keys [db]} chat-id message-id status]
(let [from (get-in db [:chats chat-id :messages message-id :from])
@ -484,8 +470,13 @@
(re-frame/reg-fx
:send-notification
(fn [{:keys [message payload tokens]}]
(let [payload-json (types/clj->json payload)
tokens-json (types/clj->json tokens)]
(log/debug "send-notification message: " message " payload-json: " payload-json " tokens-json: " tokens-json)
(status/notify-users {:message message :payload payload-json :tokens tokens-json} #(log/debug "send-notification cb result: " %)))))
(fn [{:keys [data-payload tokens]}]
"Sends a notification to another device. data-payload is a Clojure map of strings to strings"
(let [data-payload-json (types/clj->json data-payload)
tokens-json (types/clj->json tokens)]
(log/debug "send-notification data-payload-json:" data-payload-json "tokens-json:" tokens-json)
;; NOTE: react-native-firebase doesn't have a good implementation of sendMessage
;; (supporting e.g. priority or content_available properties),
;; therefore we must use an implementation in status-go.
(status/notify-users {:data-payload data-payload-json :tokens tokens-json}
#(log/debug "send-notification cb result: " %)))))

View File

@ -1,7 +1,9 @@
(ns status-im.core
(:require [re-frame.core :as re-frame]
[status-im.utils.error-handler :as error-handler]
[status-im.utils.platform :as platform]
[status-im.ui.components.react :as react]
[status-im.notifications.background :as background-messaging]
[reagent.core :as reagent]
status-im.transport.impl.receive
status-im.transport.impl.send
@ -18,4 +20,6 @@
(log/set-level! config/log-level)
(error-handler/register-exception-handler!)
(re-frame/dispatch [:init/app-started])
(.registerComponent react/app-registry "StatusIm" #(reagent/reactify-component app-root)))
(.registerComponent react/app-registry "StatusIm" #(reagent/reactify-component app-root))
(when platform/android?
(.registerHeadlessTask react/app-registry "RNFirebaseBackgroundMessage" background-messaging/message-handler-fn)))

View File

@ -830,9 +830,9 @@
;; notifications module
(handlers/register-handler-fx
:notifications/notification-event-received
(fn [cofx [_ event]]
(notifications/handle-push-notification cofx event)))
:notifications/notification-open-event-received
(fn [cofx [_ decoded-payload ctx]]
(notifications/handle-push-notification-open cofx decoded-payload ctx)))
(handlers/register-handler-fx
:notifications.callback/get-fcm-token-success
@ -849,6 +849,11 @@
(fn [cofx _]
(accounts/show-mainnet-is-default-alert cofx)))
(handlers/register-handler-fx
:notifications.callback/on-message
(fn [cofx [_ decoded-payload opts]]
(notifications/handle-on-message cofx decoded-payload opts)))
;; hardwallet module
(handlers/register-handler-fx

View File

@ -19,7 +19,8 @@
(then #(re-frame/dispatch [:hardwallet.callback/check-nfc-support-success %])))))
(defn check-nfc-enabled []
(when platform/android?
(when (and platform/android?
config/hardwallet-enabled?)
(.. keycard
nfcIsEnabled
(then #(re-frame/dispatch [:hardwallet.callback/check-nfc-enabled-success %])))))

View File

@ -74,23 +74,24 @@
(fx/defn start-app [cofx]
(fx/merge cofx
{:init/get-device-UUID nil
:init/restore-native-settings nil
:ui/listen-to-window-dimensions-change nil
:notifications/handle-initial-push-notification nil
:network/listen-to-network-status nil
:network/listen-to-connection-status nil
:hardwallet/check-nfc-support nil
:hardwallet/check-nfc-enabled nil
:hardwallet/start-module nil
:hardwallet/register-card-events nil}
{:init/get-device-UUID nil
:init/restore-native-settings nil
:ui/listen-to-window-dimensions-change nil
:notifications/init nil
:network/listen-to-network-status nil
:network/listen-to-connection-status nil
:hardwallet/check-nfc-support nil
:hardwallet/check-nfc-enabled nil
:hardwallet/start-module nil
:hardwallet/register-card-events nil}
(initialize-keychain)))
(fx/defn initialize-app-db
"Initialize db to initial state"
[{{:keys [status-module-initialized? view-id hardwallet
initial-props desktop/desktop
network-status network peers-count peers-summary device-UUID]
network-status network peers-count peers-summary device-UUID
push-notifications/stored]
:node/keys [status]
:or {network (get app-db :network)}} :db}]
{:db (assoc app-db
@ -105,7 +106,8 @@
:network network
:hardwallet hardwallet
:device-UUID device-UUID
:view-id view-id)})
:view-id view-id
:push-notifications/stored stored)})
(fx/defn initialize-app
[cofx encryption-key]
@ -140,13 +142,18 @@
(let [{{:accounts/keys [accounts] :as db} :db} cofx]
(if (empty? accounts)
(navigation/navigate-to-clean cofx :intro nil)
(let [account-with-notification (first (keys (:push-notifications/stored db)))
selection-fn (if (not-empty account-with-notification)
#(filter (fn [account]
(= account-with-notification
(:public-key account)))
%)
#(sort-by :last-sign-in > %))
(let [account-with-notification
(when-not platform/desktop?
(notifications/lookup-contact-pubkey-from-hash
cofx
(first (keys (:push-notifications/stored db)))))
selection-fn
(if (not-empty account-with-notification)
#(filter (fn [account]
(= account-with-notification
(:public-key account)))
%)
#(sort-by :last-sign-in > %))
{:keys [address photo-path name]} (first (selection-fn (vals accounts)))]
(accounts.login/open-login cofx address photo-path name)))))
@ -193,12 +200,12 @@
(= view-id :create-account)
(assoc-in [:accounts/create :step] :enter-name))}))
(defn login-only-events [cofx address]
(defn login-only-events [cofx address stored-pns]
(fx/merge cofx
{:notifications/request-notifications-permissions nil}
(navigation/navigate-to-cofx :home nil)
(universal-links/process-stored-event)
(notifications/process-stored-event address)
(notifications/process-stored-event address stored-pns)
(when platform/desktop?
(chat-model/update-dock-badge-label))))
@ -213,22 +220,23 @@
(= (get-in cofx [:db :view-id])
:hardwallet-success))
(fx/defn initialize-account [cofx address]
(fx/merge cofx
{:notifications/get-fcm-token nil}
(initialize-account-db address)
(contact/load-contacts)
(pairing/load-installations)
#(when (dev-mode? %)
(models.dev-server/start))
(browser/initialize-browsers)
(fx/defn initialize-account [{:keys [db] :as cofx} address]
(let [stored-pns (:push-notifications/stored db)]
(fx/merge cofx
{:notifications/get-fcm-token nil}
(initialize-account-db address)
(contact/load-contacts)
(pairing/load-installations)
#(when (dev-mode? %)
(models.dev-server/start))
(browser/initialize-browsers)
(browser/initialize-dapp-permissions)
(extensions.registry/initialize)
(accounts.update/update-sign-in-time)
#(when-not (or (creating-account? %)
(finishing-hardwallet-setup? %))
(login-only-events % address))))
(browser/initialize-dapp-permissions)
(extensions.registry/initialize)
(accounts.update/update-sign-in-time)
#(when-not (or (creating-account? %)
(finishing-hardwallet-setup? %))
(login-only-events % address stored-pns)))))
(re-frame/reg-fx
:init/init-store

View File

@ -35,8 +35,7 @@
(.addEventListener react/app-state "change" app-state-change-handler))
:component-did-mount
(fn [this]
(dispatch [:set-initial-props (reagent/props this)])
(notifications/init))
(dispatch [:set-initial-props (reagent/props this)]))
:component-will-unmount
(fn []
(.stop react/http-bridge)

View File

@ -84,9 +84,9 @@
true)
false))))))
(defn notify-users [{:keys [message payload tokens] :as m} on-result]
(defn notify-users [{:keys [data-payload tokens] :as m} on-result]
(when status
(call-module #(.notifyUsers status message payload tokens on-result))))
(call-module #(.notifyUsers status data-payload tokens on-result))))
(defn send-logs [dbJson]
(when status

View File

@ -0,0 +1,29 @@
(ns status-im.notifications.background
(:require [goog.object :as object]
[re-frame.core :as re-frame]
[status-im.react-native.js-dependencies :as rn]
[status-im.notifications.core :as notifications]
[status-im.i18n :as i18n]
[cljs.core.async :as async]
[taoensso.timbre :as log]
[status-im.utils.platform :as platform]))
(when-not platform/desktop?
(def firebase (object/get rn/react-native-firebase "default")))
(defn message-handler-fn []
;; message-js is firebase.messaging.RemoteMessage: https://github.com/invertase/react-native-firebase-docs/blob/master/docs/messaging/reference/RemoteMessage.md
(fn [message-js]
(js/Promise.
(fn [on-success on-error]
(try
(when message-js
(log/debug "message-handler-fn called" (js/JSON.stringify message-js))
(let [decoded-payload (notifications/decode-notification-payload message-js)]
(when decoded-payload
(log/debug "dispatching :notifications.callback/on-message to display background message" decoded-payload)
(re-frame/dispatch [:notifications.callback/on-message decoded-payload {:force true}]))))
(on-success)
(catch :default e
(log/warn "failed to handle background message" e)
(on-error e)))))))

View File

@ -2,15 +2,22 @@
(:require [goog.object :as object]
[re-frame.core :as re-frame]
[status-im.react-native.js-dependencies :as rn]
[status-im.js-dependencies :as dependencies]
[taoensso.timbre :as log]
[status-im.i18n :as i18n]
[status-im.accounts.db :as accounts.db]
[status-im.chat.models :as chat-model]
[status-im.utils.platform :as platform]
[status-im.utils.fx :as fx]))
[status-im.utils.fx :as fx]
[status-im.utils.utils :as utils]))
;; Work in progress namespace responsible for push notifications and interacting
;; with Firebase Cloud Messaging.
(def ^:private pn-message-id-hash-length 10)
(def ^:private pn-pubkey-hash-length 10)
(def ^:private pn-pubkey-length 132)
(when-not platform/desktop?
(def firebase (object/get rn/react-native-firebase "default")))
@ -28,42 +35,157 @@
(log/debug "notifications-denied")
(re-frame/dispatch [:notifications.callback/request-notifications-permissions-denied {}]))))))
(defn valid-notification-payload?
[{:keys [from to]}]
(and from to
(or
;; is it full pubkey?
(and (= (.-length from) pn-pubkey-length)
(= (.-length to) pn-pubkey-length))
;; partially deanonymized
(and (= (.-length from) pn-pubkey-hash-length)
(= (.-length to) pn-pubkey-length))
;; or is it an anonymized pubkey hash (v2 payload)?
(and (= (.-length from) pn-pubkey-hash-length)
(= (.-length to) pn-pubkey-hash-length)))))
(defn sha3 [s]
(.sha3 dependencies/Web3.prototype s))
(defn anonymize-pubkey
[pubkey]
"Anonymize a public key, if needed, by hashing it and taking the first 4 bytes"
(if (= (count pubkey) pn-pubkey-hash-length)
pubkey
(apply str (take pn-pubkey-hash-length (sha3 pubkey)))))
(defn encode-notification-payload
[{:keys [from to id] :as payload}]
(if (valid-notification-payload? payload)
{:msg-v2 (js/JSON.stringify #js {:from (anonymize-pubkey from)
:to (anonymize-pubkey to)
:id (apply str (take pn-message-id-hash-length id))})}
(throw (str "Invalid push notification payload" payload))))
(when platform/desktop?
(defn handle-initial-push-notification [] ())) ;; no-op
(when-not platform/desktop?
(defn get-fcm-token []
(-> (.getToken (.messaging firebase))
(.then (fn [x]
(log/debug "get-fcm-token: " x)
(re-frame/dispatch [:notifications.callback/get-fcm-token-success x])))))
(defn on-refresh-fcm-token []
(.onTokenRefresh (.messaging firebase)
(fn [x]
(log/debug "on-refresh-fcm-token: " x)
(re-frame/dispatch [:notifications.callback/get-fcm-token-success x]))))
;; TODO(oskarth): Only called in background on iOS right now.
;; NOTE(oskarth): Hardcoded data keys :sum and :msg in status-go right now.
(defn on-notification []
(.onNotification (.notifications firebase)
(fn [event-js]
(let [event (js->clj event-js :keywordize-keys true)
data (select-keys event [:sum :msg])
aps (:aps event)]
(log/debug "on-notification event: " (pr-str event))
(log/debug "on-notification aps: " (pr-str aps))
(log/debug "on-notification data: " (pr-str data))))))
(def channel-id "status-im")
(def channel-name "Status")
(def sound-name "message.wav")
(def group-id "im.status.ethereum.MESSAGE")
(def icon "ic_stat_status_notification")
(defn- hash->pubkey [hash accounts]
(:public-key
(first
(filter #(= (anonymize-pubkey (:public-key %)) hash)
(vals accounts)))))
(defn lookup-contact-pubkey-from-hash
[{:keys [db] :as cofx} contact-pubkey-or-hash]
"Tries to deanonymize a given contact pubkey hash by looking up the
full pubkey (if db is unlocked) in :contacts/contacts.
Returns original value if not a hash (e.g. already a public key)."
(if (and contact-pubkey-or-hash
(= (count contact-pubkey-or-hash) pn-pubkey-hash-length))
(if-let
[account-pubkey (hash->pubkey contact-pubkey-or-hash
(:accounts/accounts db))]
account-pubkey
(if (accounts.db/logged-in? cofx)
;; TODO: for simplicity we're doing a linear lookup of the contacts,
;; but we might want to build a map of hashed pubkeys to pubkeys
;; for this purpose
(hash->pubkey contact-pubkey-or-hash (:contacts/contacts db))
(do
(log/warn "failed to lookup contact from hash, not logged in")
contact-pubkey-or-hash)))
contact-pubkey-or-hash))
(defn parse-notification-v1-payload [msg-json]
(let [msg (js/JSON.parse msg-json)]
{:from (object/get msg "from")
:to (object/get msg "to")}))
(defn parse-notification-v2-payload [msg-v2-json]
(let [msg (js/JSON.parse msg-v2-json)]
{:from (object/get msg "from")
:to (object/get msg "to")
:id (object/get msg "id")}))
(defn decode-notification-payload [message-js]
;; message-js.-data is Notification.data():
;; https://github.com/invertase/react-native-firebase/blob/adcbeac3d11585dd63922ef178ff6fd886d5aa9b/src/modules/notifications/Notification.js#L79
(let [data-js (.. message-js -data)
msg-v2-json (object/get data-js "msg-v2")]
(try
(let [payload (if msg-v2-json
(parse-notification-v2-payload msg-v2-json)
(parse-notification-v1-payload (object/get data-js "msg")))]
(if (valid-notification-payload? payload)
payload
(log/warn "failed to retrieve notification payload from"
(js/JSON.stringify data-js))))
(catch :default e
(log/debug "failed to parse" (js/JSON.stringify data-js)
"exception:" e)))))
(defn rehydrate-payload
[cofx {:keys [from to id] :as decoded-payload}]
"Takes a payload with hashed pubkeys and returns a payload with the real
(matched) pubkeys"
{:from (lookup-contact-pubkey-from-hash cofx from)
:to (lookup-contact-pubkey-from-hash cofx to)
;; TODO: Rehydrate message id
:id id})
(defn- build-notification [{:keys [title body decoded-payload]}]
(let [native-notification
(clj->js
(merge
{:title title
:body body
:data (clj->js (encode-notification-payload decoded-payload))
:sound sound-name}
(when-let [msg-id (:id decoded-payload)]
;; We must prefix the notification ID, otherwise it will
;; cause a crash in iOS
{:notificationId (str "hash:" msg-id)})))]
(firebase.notifications.Notification.
native-notification (.notifications firebase))))
(defn display-notification [{:keys [title body] :as params}]
(let [notification (build-notification params)]
(when platform/android?
(.. notification
(-android.setChannelId channel-id)
(-android.setAutoCancel true)
(-android.setPriority firebase.notifications.Android.Priority.High)
(-android.setCategory firebase.notifications.Android.Category.Message)
(-android.setGroup group-id)
(-android.setSmallIcon icon)))
(.. firebase
notifications
(displayNotification notification)
(then #(log/debug "Display Notification" title body))
(catch (fn [error]
(log/debug "Display Notification error" title body error))))))
(defn get-fcm-token []
(-> (.getToken (.messaging firebase))
(.then (fn [x]
(log/debug "get-fcm-token:" x)
(re-frame/dispatch
[:notifications.callback/get-fcm-token-success x])))))
(defn create-notification-channel []
(let [channel (firebase.notifications.Android.Channel. channel-id
channel-name
firebase.notifications.Android.Importance.Max)]
(let [channel (firebase.notifications.Android.Channel.
channel-id
channel-name
firebase.notifications.Android.Importance.High)]
(.setSound channel sound-name)
(.setShowBadge channel true)
(.enableVibration channel true)
@ -74,88 +196,163 @@
(then #(log/debug "Notification channel created:" channel-id)
#(log/error "Notification channel creation error:" channel-id %)))))
(fx/defn handle-push-notification
[{:keys [db] :as cofx} {:keys [from to] :as event}]
(let [current-public-key (accounts.db/current-public-key cofx)]
(defn- show-notification?
"Ignore push notifications from unknown contacts or removed chats"
[{:keys [db] :as cofx} {:keys [from] :as rehydrated-payload}]
(and (valid-notification-payload? rehydrated-payload)
(accounts.db/logged-in? cofx)
(some #(= (:public-key %) from)
(vals (:contacts/contacts db)))
(some #(= (:chat-id %) from)
(vals (:chats db)))))
(fx/defn handle-on-message
[{:keys [db] :as cofx} decoded-payload {:keys [force]}]
(let [view-id (:view-id db)
current-chat-id (:current-chat-id db)
app-state (:app-state db)
rehydrated-payload (rehydrate-payload cofx decoded-payload)
from (:from rehydrated-payload)]
(log/debug "handle-on-message" "app-state:" app-state
"view-id:" view-id "current-chat-id:" current-chat-id
"from:" from "force:" force)
(when (or force
(and
(not= app-state "active")
(show-notification? cofx rehydrated-payload)))
{:db
(assoc-in db [:push-notifications/stored (:to rehydrated-payload)]
(js/JSON.stringify (clj->js rehydrated-payload)))
:notifications/display-notification
{:title (i18n/label :notifications-new-message-title)
:body (i18n/label :notifications-new-message-body)
:decoded-payload rehydrated-payload}})))
(fx/defn handle-push-notification-open
[{:keys [db] :as cofx} decoded-payload {:keys [stored?] :as ctx}]
(let [current-public-key (accounts.db/current-public-key cofx)
nav-opts (when stored? {:navigation-reset? true})
rehydrated-payload (rehydrate-payload cofx decoded-payload)
from (:from rehydrated-payload)
to (:to rehydrated-payload)]
(log/debug "handle-push-notification-open"
"current-public-key:" current-public-key
"rehydrated-payload:" rehydrated-payload "stored?:" stored?)
(if (= to current-public-key)
(fx/merge cofx
{:db (update db :push-notifications/stored dissoc to)}
(chat-model/navigate-to-chat from nil))
{:db (assoc-in db [:push-notifications/stored to] from)})))
(chat-model/navigate-to-chat from nav-opts))
{:db (assoc-in db [:push-notifications/stored to]
(js/JSON.stringify (clj->js rehydrated-payload)))})))
(defn parse-notification-payload [s]
(try
(js/JSON.parse s)
(catch :default _
#js {})))
;; https://github.com/invertase/react-native-firebase/blob/adcbeac3d11585dd63922ef178ff6fd886d5aa9b/src/modules/notifications/Notification.js#L13
(defn handle-notification-open-event [event]
(log/debug "handle-notification-open-event" event)
(let [decoded-payload (decode-notification-payload (.. event -notification))]
(when decoded-payload
(re-frame/dispatch
[:notifications/notification-open-event-received decoded-payload nil]))))
(defn handle-notification-event [event]
(let [msg (object/get (.. event -notification -data) "msg")
data (parse-notification-payload msg)
from (object/get data "from")
to (object/get data "to")]
(log/debug "on notification" (pr-str msg))
(when (and from to)
(re-frame/dispatch [:notifications/notification-event-received {:from from
:to to}]))))
(defn handle-initial-push-notification
[]
(defn handle-initial-push-notification []
"This method handles pending push notifications.
It is only needed to handle PNs from legacy clients
(which use firebase.notifications API)"
(log/debug "Handle initial push notifications")
(.. firebase
notifications
getInitialNotification
(then (fn [event]
(log/debug "getInitialNotification" event)
(when event
(handle-notification-event event))))))
(handle-notification-open-event event))))))
(defn on-notification-opened []
(defn setup-token-refresh-callback []
(.onTokenRefresh
(.messaging firebase)
(fn [x]
(log/debug "onTokenRefresh:" x)
(re-frame/dispatch [:notifications.callback/get-fcm-token-success x]))))
(defn setup-on-notification-callback []
"Calling onNotification is only needed so that we're able to receive PNs"
"while in foreground from older clients who are still relying"
"on the notifications API. Once that is no longer a consideration"
"we can remove this method"
(log/debug "calling onNotification")
(.onNotification
(.notifications firebase)
(fn [message-js]
(log/debug "handle-on-notification-callback called")
(let [decoded-payload (decode-notification-payload message-js)]
(log/debug "handle-on-notification-callback payload:" decoded-payload)
(when decoded-payload
(re-frame/dispatch
[:notifications.callback/on-message decoded-payload]))))))
(defn setup-on-message-callback []
(log/debug "calling onMessage")
(.onMessage
(.messaging firebase)
(fn [message-js]
(log/debug "handle-on-message-callback called")
(let [decoded-payload (decode-notification-payload message-js)]
(log/debug "handle-on-message-callback decoded-payload:"
decoded-payload)
(when decoded-payload
(re-frame/dispatch
[:notifications.callback/on-message decoded-payload]))))))
(defn setup-on-notification-opened-callback []
(log/debug "setup-on-notification-opened-callback")
(.. firebase
notifications
(onNotificationOpened handle-notification-event)))
(onNotificationOpened handle-notification-open-event)))
(defn init []
(on-refresh-fcm-token)
(on-notification)
(on-notification-opened)
(log/debug "Init notifications")
(setup-token-refresh-callback)
(setup-on-message-callback)
(setup-on-notification-callback)
(setup-on-notification-opened-callback)
(when platform/android?
(create-notification-channel)))
(create-notification-channel))
(handle-initial-push-notification)))
(defn display-notification [{:keys [title body from to]}]
(let [notification (firebase.notifications.Notification.)]
(.. notification
(setTitle title)
(setBody body)
(setData (js/JSON.stringify #js {:from from
:to to}))
(setSound sound-name)
(-android.setChannelId channel-id)
(-android.setAutoCancel true)
(-android.setPriority firebase.notifications.Android.Priority.Max)
(-android.setGroup group-id)
(-android.setGroupSummary true)
(-android.setSmallIcon icon))
(.. firebase
notifications
(displayNotification notification)
(then #(log/debug "Display Notification" title body))
(then #(log/debug "Display Notification error" title body))))))
(fx/defn process-stored-event [cofx address]
(fx/defn process-stored-event [cofx address stored-pns]
(when-not platform/desktop?
(let [to (get-in cofx [:db :accounts/accounts address :public-key])
from (get-in cofx [:db :push-notifications/stored to])]
(when from
(handle-push-notification cofx
{:from from
:to to})))))
(if (accounts.db/logged-in? cofx)
(let [current-account (get-in cofx [:db :account/account])
current-address (:address current-account)
current-account-pubkey (:public-key current-account)
stored-pn-val-json (or (get stored-pns current-account-pubkey)
(get stored-pns (anonymize-pubkey current-account-pubkey)))
stored-pn-payload (if (= (first stored-pn-val-json) \{)
(js->clj (js/JSON.parse stored-pn-val-json) :keywordize-keys true)
{:from stored-pn-val-json
:to current-account-pubkey})
from (lookup-contact-pubkey-from-hash cofx (:from stored-pn-payload))
to (lookup-contact-pubkey-from-hash cofx (:to stored-pn-payload))]
(when (and from
(= address current-address))
(log/debug "process-stored-event" "address" address "from" from "to" to)
(handle-push-notification-open cofx
stored-pn-payload
{:stored? true})))
(log/error "process-stored-event called without user being logged in!"))))
(re-frame/reg-fx
:notifications/display-notification
display-notification)
(re-frame/reg-fx
:notifications/handle-initial-push-notification
handle-initial-push-notification)
:notifications/init
(fn []
(cond
platform/android?
(init)
platform/ios?
(utils/set-timeout init 100))))
(re-frame/reg-fx
:notifications/get-fcm-token

View File

@ -87,7 +87,7 @@
(fx/merge cofx
(remove-hash envelope-hash)
(check-confirmations status chat-id message-id)
(models.message/send-push-notification fcm-token status)))))))
(models.message/send-push-notification message-id fcm-token status)))))))
(fx/defn set-contact-message-envelope-hash
[{:keys [db] :as cofx} chat-id envelope-hash]

View File

@ -24,7 +24,6 @@
(def mailserver-confirmations-enabled? (enabled? (get-config :MAILSERVER_CONFIRMATIONS_ENABLED)))
(def mainnet-warning-enabled? (enabled? (get-config :MAINNET_WARNING_ENABLED 0)))
(def pfs-encryption-enabled? (enabled? (get-config :PFS_ENCRYPTION_ENABLED "0")))
(def in-app-notifications-enabled? (enabled? (get-config :IN_APP_NOTIFICATIONS_ENABLED 0)))
(def cached-webviews-enabled? (enabled? (get-config :CACHED_WEBVIEWS_ENABLED 0)))
(def rn-bridge-threshold-warnings-enabled? (enabled? (get-config :RN_BRIDGE_THRESHOLD_WARNINGS 0)))
(def extensions-enabled? (enabled? (get-config :EXTENSIONS 0)))

View File

@ -55,11 +55,11 @@
(re-frame/dispatch [:handle-universal-link url])))
(fx/defn handle-browse [cofx url]
(log/info "universal-links: handling browse " url)
(log/info "universal-links: handling browse" url)
{:browser/show-browser-selection url})
(fx/defn handle-public-chat [cofx public-chat]
(log/info "universal-links: handling public chat " public-chat)
(log/info "universal-links: handling public chat" public-chat)
(chat/start-public-chat cofx public-chat {:navigation-reset? true}))
(fx/defn handle-view-profile [{:keys [db] :as cofx} public-key]

View File

@ -2,34 +2,43 @@
(:require [cljs.test :refer-macros [deftest is testing]]
[status-im.notifications.core :as notifications]))
(deftest test-handle-push-notification
(deftest test-handle-push-notification-open
(testing "user's signing in having opened PN while signed out"
(is (= {:db {:account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}
:push-notifications/stored {}}
:dispatch [:navigate-to-chat "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" {:navigation-reset? true}]}
(notifications/handle-push-notification-open {:db {:push-notifications/stored {}
:account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"
:stored? true}]))))
(testing "user's signed in"
(is (= {:db {:account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}
:push-notifications/stored {}}
:dispatch [:navigate-to-chat "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"]}
(notifications/handle-push-notification {:db {:push-notifications/stored {}
:account/account {:publi-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}]))))
(notifications/handle-push-notification-open {:db {:push-notifications/stored {}
:account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}]))))
(testing "user's signed in into another account"
(is (= {}
(notifications/handle-push-notification {:db {:account/account {:public-key "0x04bc8bf4a91ab726bd98f2c54b3036caacaeea527867945ab839e9ad4e62696856d7f7fa485f68304de357e38a1553eac5592706a16fcf71fd821bbd6c796f9ab3"}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}]))))
(notifications/handle-push-notification-open {:db {:account/account {:public-key "0x04bc8bf4a91ab726bd98f2c54b3036caacaeea527867945ab839e9ad4e62696856d7f7fa485f68304de357e38a1553eac5592706a16fcf71fd821bbd6c796f9ab3"}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}]))))
(testing "user's not signed in"
(is (= {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7"
:photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX"
:name "Bob"
:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}
:account/account {:public-key nil}
:account/account {:public-key nil}
:push-notifications/stored {"0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"
"0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"}}
:dispatch [:ui/open-login "bd36cd64e2621b054a3b7464ff1b3c4c304880e7" "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX" "Bob"]}
(notifications/handle-push-notification {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7"
:photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX"
:name "Bob"
:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}
:account/account {:public-key nil}
:push-notifications/stored {}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))))
(notifications/handle-push-notification-open {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7"
:photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX"
:name "Bob"
:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}
:account/account {:public-key nil}
:push-notifications/stored {}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))))