mirror of
https://github.com/status-im/status-react.git
synced 2025-03-04 04:21:10 +00:00
385 lines
16 KiB
Clojure
385 lines
16 KiB
Clojure
(ns status-im.notifications.core
|
|
(: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.contact.db :as contact.db]
|
|
[status-im.chat.models :as chat-model]
|
|
[status-im.utils.platform :as platform]
|
|
[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)
|
|
(def ^:private pull-recent-messages-window (* 15 60))
|
|
|
|
(when-not platform/desktop?
|
|
|
|
(defn firebase [] (object/get (rn/react-native-firebase) "default")))
|
|
|
|
;; NOTE: Only need to explicitly request permissions on iOS.
|
|
(defn request-permissions []
|
|
(if platform/desktop?
|
|
(re-frame/dispatch [:notifications.callback/request-notifications-permissions-granted {}])
|
|
(-> (.requestPermission (.messaging (firebase)))
|
|
(.then
|
|
(fn [_]
|
|
(log/debug "notifications-granted")
|
|
(re-frame/dispatch [:notifications.callback/request-notifications-permissions-granted {}]))
|
|
(fn [_]
|
|
(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?
|
|
|
|
(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->contact [hash-or-pubkey accounts]
|
|
(let [hash (anonymize-pubkey hash-or-pubkey)]
|
|
(->> accounts
|
|
(filter #(= (anonymize-pubkey (:public-key %)) hash))
|
|
first)))
|
|
|
|
(defn- hash->pubkey [hash accounts]
|
|
(:public-key (hash->contact hash 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
|
|
(-> db :accounts/accounts vals))]
|
|
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
|
|
(contact.db/get-active-contacts (: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- get-contact-name [{:keys [db] :as cofx} from]
|
|
(if (accounts.db/logged-in? cofx)
|
|
(:name (hash->contact from (-> db :contacts/contacts vals)))
|
|
(anonymize-pubkey from)))
|
|
|
|
(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 (firebase)]
|
|
(firebase.notifications.Notification.
|
|
native-notification (.notifications firebase))))
|
|
|
|
(defn display-notification [{:keys [title body] :as params}]
|
|
(let [notification (build-notification params)
|
|
firebase (firebase)]
|
|
(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 [firebase (firebase)
|
|
channel (firebase.notifications.Android.Channel.
|
|
channel-id
|
|
channel-name
|
|
firebase.notifications.Android.Importance.High)]
|
|
(.setSound channel sound-name)
|
|
(.setShowBadge channel true)
|
|
(.enableVibration channel true)
|
|
(.. firebase
|
|
notifications
|
|
-android
|
|
(createChannel channel)
|
|
(then #(log/debug "Notification channel created:" channel-id)
|
|
#(log/error "Notification channel creation error:" channel-id %)))))
|
|
|
|
(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)
|
|
(contact.db/get-active-contacts (:contacts/contacts db)))
|
|
(some #(= (:chat-id %) from)
|
|
(vals (:chats db)))))
|
|
|
|
(fx/defn handle-on-message
|
|
[{:keys [db now] :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)
|
|
(merge
|
|
(when (and (= (count from) pn-pubkey-length)
|
|
(show-notification? cofx rehydrated-payload))
|
|
{:dispatch [:mailserver/fetch-history from (- (quot now 1000) pull-recent-messages-window)]})
|
|
(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 (get-contact-name cofx from)
|
|
: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 nav-opts))
|
|
{:db (assoc-in db [:push-notifications/stored to]
|
|
(js/JSON.stringify (clj->js rehydrated-payload)))})))
|
|
|
|
;; 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-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-open-event event))))))
|
|
|
|
(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-open-event)))
|
|
|
|
(defn init []
|
|
(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))
|
|
(handle-initial-push-notification)))
|
|
|
|
(fx/defn process-stored-event [{:keys [db] :as cofx} address stored-pns]
|
|
(when-not platform/desktop?
|
|
(if (accounts.db/logged-in? cofx)
|
|
(let [current-account (:account/account db)
|
|
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
|
|
(not (contact.db/blocked? db 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/init
|
|
(fn []
|
|
(cond
|
|
platform/android?
|
|
(init)
|
|
|
|
platform/ios?
|
|
(utils/set-timeout init 100))))
|
|
|
|
(re-frame/reg-fx
|
|
:notifications/get-fcm-token
|
|
(fn [_]
|
|
(when platform/mobile?
|
|
(get-fcm-token))))
|
|
|
|
(re-frame/reg-fx
|
|
:notifications/request-notifications-permissions
|
|
(fn [_]
|
|
(request-permissions)))
|