-
Notifications
You must be signed in to change notification settings - Fork 0
/
firebase.clj
161 lines (143 loc) · 7.11 KB
/
firebase.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
(ns dev.gethop.notifications.firebase
(:require [clojure.spec.alpha :as s]
[clojure.string :as str]
[dev.gethop.notifications.core :as core]
[dev.gethop.notifications.firebase.config :as config]
[duct.logger :refer [log]]
[integrant.core :as ig])
(:import [com.google.auth.oauth2 ServiceAccountCredentials]
[com.google.firebase FirebaseApp FirebaseOptions]
[com.google.firebase.messaging
FirebaseMessaging FirebaseMessagingException MulticastMessage
MulticastMessage$Builder SendResponse]
[java.util UUID]))
(def ^:const ^:private firebase-scope
"https://www.googleapis.com/auth/firebase.messaging")
(def ^:const ^:private multicast-recipient-limit
500)
(defn- construct-service-credentials
^ServiceAccountCredentials
[{:keys [client-id client-email private-key-id private-key project-id]}]
(let [credentials (ServiceAccountCredentials/fromPkcs8
client-id client-email
(str/replace private-key "\\n" "\n")
private-key-id
[firebase-scope])]
(-> (.toBuilder credentials)
(.setProjectId project-id)
(.build))))
(defn- random-firebase-app-name
"Firebase Application names must be unique, so we create a random name
to avoid collisions. This is important if any other Firebase library
is being used, or multiple records of this one are initialized."
[]
(str (UUID/randomUUID)))
(defn- init-firebase-app! ^FirebaseApp [google-credentials]
(let [serviceCredentials (construct-service-credentials google-credentials)
firebaseOptions (-> (FirebaseOptions/builder)
(.setCredentials serviceCredentials)
(.build))
firebase-app-name (random-firebase-app-name)]
(FirebaseApp/initializeApp firebaseOptions firebase-app-name)))
(defn- message->firebase-message [message]
(reduce-kv (fn [m k v]
(if (keyword? v)
(assoc m (name k) (name v))
(assoc m (name k) (str v))))
{} message))
(defn- firebase-responses->errors [recipient firebase-responses]
(reduce-kv
(fn [errors k ^SendResponse v]
(if (.isSuccessful v)
errors
(let [exception ^FirebaseMessagingException (.getException v)
error-class (class exception)
error-message (.getMessage exception)
error-type (if (and (= error-class FirebaseMessagingException)
(or (= error-message "Requested entity was not found.")
(= error-message "The registration token is not a valid FCM registration token")))
::core/invalid-recipient
::core/other-error)]
(conj errors {:recipient k
:error-type error-type
:implementation-error-details {:error-class error-class
:error-message error-message}}))))
[]
(zipmap recipient firebase-responses)))
(defn- send-notification* [firebaseApp logger recipient firebase-message opts]
(log logger :debug ::firebase-send-notification* {:firebaseApp firebaseApp
:recipient recipient
:firebase-message firebase-message
:opts opts})
(let [multicast-message-builder (-> (MulticastMessage/builder)
(.putAllData firebase-message)
(.addAllTokens recipient)
(config/set-message-config opts))
multicast-message (.build ^MulticastMessage$Builder multicast-message-builder)
response (-> (FirebaseMessaging/getInstance firebaseApp)
(.sendMulticast multicast-message))]
(if (= (.getSuccessCount response) (count recipient))
{:success? true}
(let [errors (firebase-responses->errors recipient (.getResponses response))]
(log logger :error ::firebase-notification {:success-count (.getSuccessCount response)
:error-count (.getFailureCount response)
:errors errors})
{:success? false :errors errors}))))
(defn- send-notification
[firebaseApp logger recipient message opts]
{:pre [(s/valid? ::core/logger logger)
(s/valid? ::core/recipient recipient)
(s/valid? ::core/message message)
(s/valid? ::core/opts opts)]}
(log logger :debug ::pre-send-notification {:message message
:opts opts
:recipient recipient})
(let [firebase-message (message->firebase-message message)
results (->> (flatten (vector recipient))
(partition-all multicast-recipient-limit)
(pmap #(send-notification* firebaseApp logger % firebase-message opts)))]
(if (every? :success? results)
{:success? true}
(let [errors (reduce concat (keep :errors results))]
(log logger :error ::pre-send-notification-error {:errors errors})
{:success? false
:errors errors}))))
(s/def ::firebaseApp #(instance? FirebaseApp %))
(s/def ::send-notification-args (s/cat :firebaseApp ::firebaseApp
:logger ::core/logger
:recipient ::core/recipient
:message ::core/message
:opts ::core/opts))
(s/fdef send-notification
:args ::send-notification-args
:ret ::core/send-notification-ret)
(defn- send-notification-async [firebaseApp logger recipient message opts]
(log logger :debug ::firebase-notification-async)
(future
(send-notification firebaseApp logger recipient message opts)))
(s/fdef send-notification-async
:args ::send-notification-args
:ret ::core/send-notification-async-ret)
(defrecord Firebase [^FirebaseApp firebaseApp]
core/Notifications
(send-notification [_ logger recipient message]
(send-notification firebaseApp logger recipient message {}))
(send-notification [_ logger recipient message opts]
(send-notification firebaseApp logger recipient message opts))
(send-notification-async [_ logger recipient message]
(send-notification-async firebaseApp logger recipient message {}))
(send-notification-async [_ logger recipient message opts]
(send-notification-async firebaseApp logger recipient message opts)))
(defn init-record
"Initiates the Firebase record"
[{:keys [google-credentials] :as _config}]
(let [firebaseApp (init-firebase-app! google-credentials)]
(->Firebase firebaseApp)))
(defn halt-record
"Halts the Firebase record"
[{:keys [^FirebaseApp firebaseApp] :as _record}]
(.delete firebaseApp))
(defmethod ig/init-key :dev.gethop.notifications/firebase [_ config]
(init-record config))
(defmethod ig/halt-key! :dev.gethop.notifications/firebase [_ record]
(halt-record record))