fcm 0.0.2 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +1 -0
- data/Gemfile +1 -0
- data/README.md +176 -37
- data/fcm.gemspec +14 -16
- data/lib/fcm.rb +235 -120
- data/spec/fcm_spec.rb +257 -88
- metadata +25 -20
- data/.travis.yml +0 -6
data/lib/fcm.rb
CHANGED
@@ -1,190 +1,263 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require "faraday"
|
2
|
+
require "cgi"
|
3
|
+
require "json"
|
4
|
+
require "googleauth"
|
4
5
|
|
5
6
|
class FCM
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
BASE_URI = "https://fcm.googleapis.com"
|
8
|
+
BASE_URI_V1 = "https://fcm.googleapis.com/v1/projects/"
|
9
|
+
DEFAULT_TIMEOUT = 30
|
10
|
+
FORMAT = :json
|
10
11
|
|
11
12
|
# constants
|
12
|
-
GROUP_NOTIFICATION_BASE_URI =
|
13
|
+
GROUP_NOTIFICATION_BASE_URI = "https://android.googleapis.com"
|
14
|
+
INSTANCE_ID_API = "https://iid.googleapis.com"
|
15
|
+
TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/
|
13
16
|
|
14
|
-
attr_accessor :timeout, :api_key
|
17
|
+
attr_accessor :timeout, :api_key, :json_key_path, :project_base_uri
|
15
18
|
|
16
|
-
def initialize(api_key, client_options = {})
|
19
|
+
def initialize(api_key, json_key_path = "", project_name = "", client_options = {})
|
17
20
|
@api_key = api_key
|
18
21
|
@client_options = client_options
|
22
|
+
@json_key_path = json_key_path
|
23
|
+
@project_base_uri = BASE_URI_V1 + project_name.to_s
|
19
24
|
end
|
20
25
|
|
26
|
+
# See https://firebase.google.com/docs/cloud-messaging/send-message
|
21
27
|
# {
|
22
|
-
# "
|
23
|
-
# "
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# "
|
28
|
+
# "token": "4sdsx",
|
29
|
+
# "notification": {
|
30
|
+
# "title": "Breaking News",
|
31
|
+
# "body": "New news story available."
|
32
|
+
# },
|
33
|
+
# "data": {
|
34
|
+
# "story_id": "story_12345"
|
35
|
+
# },
|
36
|
+
# "android": {
|
37
|
+
# "notification": {
|
38
|
+
# "click_action": "TOP_STORY_ACTIVITY",
|
39
|
+
# "body": "Check out the Top Story"
|
40
|
+
# }
|
41
|
+
# },
|
42
|
+
# "apns": {
|
43
|
+
# "payload": {
|
44
|
+
# "aps": {
|
45
|
+
# "category" : "NEW_MESSAGE_CATEGORY"
|
46
|
+
# }
|
47
|
+
# }
|
29
48
|
# }
|
30
49
|
# }
|
50
|
+
# fcm = FCM.new(api_key, json_key_path, project_name)
|
51
|
+
# fcm.send(
|
52
|
+
# { "token": "4sdsx",, "to" : "notification": {}.. }
|
53
|
+
# )
|
54
|
+
def send_notification_v1(message)
|
55
|
+
return if @project_base_uri.empty?
|
56
|
+
|
57
|
+
post_body = { 'message': message }
|
58
|
+
|
59
|
+
response = Faraday.post("#{@project_base_uri}/messages:send") do |req|
|
60
|
+
req.headers["Content-Type"] = "application/json"
|
61
|
+
req.headers["Authorization"] = "Bearer #{jwt_token}"
|
62
|
+
req.body = post_body.to_json
|
63
|
+
end
|
64
|
+
build_response(response)
|
65
|
+
end
|
66
|
+
|
67
|
+
alias send_v1 send_notification_v1
|
68
|
+
|
69
|
+
# See https://developers.google.com/cloud-messaging/http for more details.
|
70
|
+
# { "notification": {
|
71
|
+
# "title": "Portugal vs. Denmark",
|
72
|
+
# "text": "5 to 1"
|
73
|
+
# },
|
74
|
+
# "to" : "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1..."
|
75
|
+
# }
|
31
76
|
# fcm = FCM.new("API_KEY")
|
32
|
-
# fcm.send(
|
77
|
+
# fcm.send(
|
78
|
+
# ["4sdsx", "8sdsd"], # registration_ids
|
79
|
+
# { "notification": { "title": "Portugal vs. Denmark", "text": "5 to 1" }, "to" : "bk3RNwTe3HdFQ3P1..." }
|
80
|
+
# )
|
33
81
|
def send_notification(registration_ids, options = {})
|
34
82
|
post_body = build_post_body(registration_ids, options)
|
35
83
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
'Content-Type' => 'application/json'
|
41
|
-
}
|
42
|
-
}
|
43
|
-
response = self.class.post('/send', params.merge(@client_options))
|
44
|
-
build_response(response, registration_ids)
|
84
|
+
for_uri(BASE_URI) do |connection|
|
85
|
+
response = connection.post("/fcm/send", post_body.to_json)
|
86
|
+
build_response(response, registration_ids)
|
87
|
+
end
|
45
88
|
end
|
89
|
+
|
46
90
|
alias send send_notification
|
47
91
|
|
48
92
|
def create_notification_key(key_name, project_id, registration_ids = [])
|
49
|
-
post_body = build_post_body(registration_ids, operation:
|
50
|
-
|
51
|
-
|
52
|
-
params = {
|
53
|
-
body: post_body.to_json,
|
54
|
-
headers: {
|
55
|
-
'Content-Type' => 'application/json',
|
56
|
-
'project_id' => project_id,
|
57
|
-
'Authorization' => "key=#{@api_key}"
|
58
|
-
}
|
59
|
-
}
|
93
|
+
post_body = build_post_body(registration_ids, operation: "create",
|
94
|
+
notification_key_name: key_name)
|
60
95
|
|
61
|
-
|
96
|
+
extra_headers = {
|
97
|
+
"project_id" => project_id,
|
98
|
+
}
|
62
99
|
|
63
|
-
for_uri(GROUP_NOTIFICATION_BASE_URI) do
|
64
|
-
response =
|
100
|
+
for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
|
101
|
+
response = connection.post("/gcm/notification", post_body.to_json)
|
102
|
+
build_response(response)
|
65
103
|
end
|
66
|
-
|
67
|
-
build_response(response)
|
68
104
|
end
|
105
|
+
|
69
106
|
alias create create_notification_key
|
70
107
|
|
71
108
|
def add_registration_ids(key_name, project_id, notification_key, registration_ids)
|
72
|
-
post_body = build_post_body(registration_ids, operation:
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
params = {
|
77
|
-
body: post_body.to_json,
|
78
|
-
headers: {
|
79
|
-
'Content-Type' => 'application/json',
|
80
|
-
'project_id' => project_id,
|
81
|
-
'Authorization' => "key=#{@api_key}"
|
82
|
-
}
|
83
|
-
}
|
109
|
+
post_body = build_post_body(registration_ids, operation: "add",
|
110
|
+
notification_key_name: key_name,
|
111
|
+
notification_key: notification_key)
|
84
112
|
|
85
|
-
|
113
|
+
extra_headers = {
|
114
|
+
"project_id" => project_id,
|
115
|
+
}
|
86
116
|
|
87
|
-
for_uri(GROUP_NOTIFICATION_BASE_URI) do
|
88
|
-
response =
|
117
|
+
for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
|
118
|
+
response = connection.post("/gcm/notification", post_body.to_json)
|
119
|
+
build_response(response)
|
89
120
|
end
|
90
|
-
build_response(response)
|
91
121
|
end
|
122
|
+
|
92
123
|
alias add add_registration_ids
|
93
124
|
|
94
125
|
def remove_registration_ids(key_name, project_id, notification_key, registration_ids)
|
95
|
-
post_body = build_post_body(registration_ids, operation:
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
params = {
|
100
|
-
body: post_body.to_json,
|
101
|
-
headers: {
|
102
|
-
'Content-Type' => 'application/json',
|
103
|
-
'project_id' => project_id,
|
104
|
-
'Authorization' => "key=#{@api_key}"
|
105
|
-
}
|
106
|
-
}
|
126
|
+
post_body = build_post_body(registration_ids, operation: "remove",
|
127
|
+
notification_key_name: key_name,
|
128
|
+
notification_key: notification_key)
|
107
129
|
|
108
|
-
|
130
|
+
extra_headers = {
|
131
|
+
"project_id" => project_id,
|
132
|
+
}
|
109
133
|
|
110
|
-
for_uri(GROUP_NOTIFICATION_BASE_URI) do
|
111
|
-
response =
|
134
|
+
for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
|
135
|
+
response = connection.post("/gcm/notification", post_body.to_json)
|
136
|
+
build_response(response)
|
112
137
|
end
|
113
|
-
build_response(response)
|
114
138
|
end
|
139
|
+
|
115
140
|
alias remove remove_registration_ids
|
116
141
|
|
117
142
|
def recover_notification_key(key_name, project_id)
|
118
|
-
params = {
|
119
|
-
query: {
|
120
|
-
notification_key_name: key_name
|
121
|
-
},
|
122
|
-
headers: {
|
123
|
-
'Content-Type' => 'application/json',
|
124
|
-
'project_id' => project_id,
|
125
|
-
'Authorization' => "key=#{@api_key}"
|
126
|
-
}
|
127
|
-
}
|
143
|
+
params = { notification_key_name: key_name }
|
128
144
|
|
129
|
-
|
145
|
+
extra_headers = {
|
146
|
+
"project_id" => project_id,
|
147
|
+
}
|
130
148
|
|
131
|
-
for_uri(GROUP_NOTIFICATION_BASE_URI) do
|
132
|
-
response =
|
149
|
+
for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
|
150
|
+
response = connection.get("/gcm/notification", params)
|
151
|
+
build_response(response)
|
133
152
|
end
|
134
|
-
build_response(response)
|
135
153
|
end
|
136
154
|
|
137
155
|
def send_with_notification_key(notification_key, options = {})
|
138
156
|
body = { to: notification_key }.merge(options)
|
157
|
+
execute_notification(body)
|
158
|
+
end
|
139
159
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
}
|
160
|
+
def topic_subscription(topic, registration_id)
|
161
|
+
for_uri(INSTANCE_ID_API) do |connection|
|
162
|
+
response = connection.post("/iid/v1/#{registration_id}/rel/topics/#{topic}")
|
163
|
+
build_response(response)
|
164
|
+
end
|
165
|
+
end
|
147
166
|
|
148
|
-
|
149
|
-
|
167
|
+
def batch_topic_subscription(topic, registration_ids)
|
168
|
+
manage_topics_relationship(topic, registration_ids, "Add")
|
169
|
+
end
|
170
|
+
|
171
|
+
def batch_topic_unsubscription(topic, registration_ids)
|
172
|
+
manage_topics_relationship(topic, registration_ids, "Remove")
|
173
|
+
end
|
174
|
+
|
175
|
+
def manage_topics_relationship(topic, registration_ids, action)
|
176
|
+
body = { to: "/topics/#{topic}", registration_tokens: registration_ids }
|
177
|
+
|
178
|
+
for_uri(INSTANCE_ID_API) do |connection|
|
179
|
+
response = connection.post("/iid/v1:batch#{action}", body.to_json)
|
180
|
+
build_response(response)
|
181
|
+
end
|
150
182
|
end
|
151
183
|
|
152
184
|
def send_to_topic(topic, options = {})
|
153
|
-
if topic
|
154
|
-
send_with_notification_key(
|
185
|
+
if topic.gsub(TOPIC_REGEX, "").length == 0
|
186
|
+
send_with_notification_key("/topics/" + topic, options)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def get_instance_id_info(iid_token, options = {})
|
191
|
+
params = options
|
192
|
+
|
193
|
+
for_uri(INSTANCE_ID_API) do |connection|
|
194
|
+
response = connection.get("/iid/info/" + iid_token, params)
|
195
|
+
build_response(response)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def subscribe_instance_id_to_topic(iid_token, topic_name)
|
200
|
+
batch_subscribe_instance_ids_to_topic([iid_token], topic_name)
|
201
|
+
end
|
202
|
+
|
203
|
+
def unsubscribe_instance_id_from_topic(iid_token, topic_name)
|
204
|
+
batch_unsubscribe_instance_ids_from_topic([iid_token], topic_name)
|
205
|
+
end
|
206
|
+
|
207
|
+
def batch_subscribe_instance_ids_to_topic(instance_ids, topic_name)
|
208
|
+
manage_topics_relationship(topic_name, instance_ids, "Add")
|
209
|
+
end
|
210
|
+
|
211
|
+
def batch_unsubscribe_instance_ids_from_topic(instance_ids, topic_name)
|
212
|
+
manage_topics_relationship(topic_name, instance_ids, "Remove")
|
213
|
+
end
|
214
|
+
|
215
|
+
def send_to_topic_condition(condition, options = {})
|
216
|
+
if validate_condition?(condition)
|
217
|
+
body = { condition: condition }.merge(options)
|
218
|
+
execute_notification(body)
|
155
219
|
end
|
156
220
|
end
|
157
221
|
|
158
222
|
private
|
159
223
|
|
160
|
-
def for_uri(uri)
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
224
|
+
def for_uri(uri, extra_headers = {})
|
225
|
+
connection = ::Faraday.new(
|
226
|
+
url: uri,
|
227
|
+
request: { timeout: DEFAULT_TIMEOUT }
|
228
|
+
) do |faraday|
|
229
|
+
faraday.adapter Faraday.default_adapter
|
230
|
+
faraday.headers["Content-Type"] = "application/json"
|
231
|
+
faraday.headers["Authorization"] = "key=#{api_key}"
|
232
|
+
extra_headers.each do |key, value|
|
233
|
+
faraday.headers[key] = value
|
234
|
+
end
|
235
|
+
end
|
236
|
+
yield connection
|
165
237
|
end
|
166
238
|
|
167
239
|
def build_post_body(registration_ids, options = {})
|
168
|
-
|
240
|
+
ids = registration_ids.is_a?(String) ? [registration_ids] : registration_ids
|
241
|
+
{ registration_ids: ids }.merge(options)
|
169
242
|
end
|
170
243
|
|
171
244
|
def build_response(response, registration_ids = [])
|
172
245
|
body = response.body || {}
|
173
|
-
response_hash = { body: body, headers: response.headers, status_code: response.
|
174
|
-
case response.
|
246
|
+
response_hash = { body: body, headers: response.headers, status_code: response.status }
|
247
|
+
case response.status
|
175
248
|
when 200
|
176
|
-
response_hash[:response] =
|
249
|
+
response_hash[:response] = "success"
|
177
250
|
body = JSON.parse(body) unless body.empty?
|
178
251
|
response_hash[:canonical_ids] = build_canonical_ids(body, registration_ids) unless registration_ids.empty?
|
179
252
|
response_hash[:not_registered_ids] = build_not_registered_ids(body, registration_ids) unless registration_ids.empty?
|
180
253
|
when 400
|
181
|
-
response_hash[:response] =
|
254
|
+
response_hash[:response] = "Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields."
|
182
255
|
when 401
|
183
|
-
response_hash[:response] =
|
256
|
+
response_hash[:response] = "There was an error authenticating the sender account."
|
184
257
|
when 503
|
185
|
-
response_hash[:response] =
|
258
|
+
response_hash[:response] = "Server is temporarily unavailable."
|
186
259
|
when 500..599
|
187
|
-
response_hash[:response] =
|
260
|
+
response_hash[:response] = "There was an internal error in the FCM server while trying to process the request."
|
188
261
|
end
|
189
262
|
response_hash
|
190
263
|
end
|
@@ -192,9 +265,9 @@ class FCM
|
|
192
265
|
def build_canonical_ids(body, registration_ids)
|
193
266
|
canonical_ids = []
|
194
267
|
unless body.empty?
|
195
|
-
if body[
|
196
|
-
body[
|
197
|
-
canonical_ids << { old: registration_ids[index], new: result[
|
268
|
+
if body["canonical_ids"] > 0
|
269
|
+
body["results"].each_with_index do |result, index|
|
270
|
+
canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result)
|
198
271
|
end
|
199
272
|
end
|
200
273
|
end
|
@@ -204,8 +277,8 @@ class FCM
|
|
204
277
|
def build_not_registered_ids(body, registration_id)
|
205
278
|
not_registered_ids = []
|
206
279
|
unless body.empty?
|
207
|
-
if body[
|
208
|
-
body[
|
280
|
+
if body["failure"] > 0
|
281
|
+
body["results"].each_with_index do |result, index|
|
209
282
|
not_registered_ids << registration_id[index] if is_not_registered?(result)
|
210
283
|
end
|
211
284
|
end
|
@@ -213,11 +286,53 @@ class FCM
|
|
213
286
|
not_registered_ids
|
214
287
|
end
|
215
288
|
|
289
|
+
def execute_notification(body)
|
290
|
+
for_uri(BASE_URI) do |connection|
|
291
|
+
response = connection.post("/fcm/send", body.to_json)
|
292
|
+
build_response(response)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
216
296
|
def has_canonical_id?(result)
|
217
|
-
!result[
|
297
|
+
!result["registration_id"].nil?
|
218
298
|
end
|
219
299
|
|
220
300
|
def is_not_registered?(result)
|
221
|
-
result[
|
301
|
+
result["error"] == "NotRegistered"
|
302
|
+
end
|
303
|
+
|
304
|
+
def validate_condition?(condition)
|
305
|
+
validate_condition_format?(condition) && validate_condition_topics?(condition)
|
306
|
+
end
|
307
|
+
|
308
|
+
def validate_condition_format?(condition)
|
309
|
+
bad_characters = condition.gsub(
|
310
|
+
/(topics|in|\s|\(|\)|(&&)|[!]|(\|\|)|'([a-zA-Z0-9\-_.~%]+)')/,
|
311
|
+
""
|
312
|
+
)
|
313
|
+
bad_characters.length == 0
|
314
|
+
end
|
315
|
+
|
316
|
+
def validate_condition_topics?(condition)
|
317
|
+
topics = condition.scan(/(?:^|\S|\s)'([^']*?)'(?:$|\S|\s)/).flatten
|
318
|
+
topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").length == 0 }
|
319
|
+
end
|
320
|
+
|
321
|
+
def jwt_token
|
322
|
+
scope = "https://www.googleapis.com/auth/firebase.messaging"
|
323
|
+
@authorizer ||= Google::Auth::ServiceAccountCredentials.make_creds(
|
324
|
+
json_key_io: json_key,
|
325
|
+
scope: scope,
|
326
|
+
)
|
327
|
+
token = @authorizer.fetch_access_token!
|
328
|
+
token["access_token"]
|
329
|
+
end
|
330
|
+
|
331
|
+
def json_key
|
332
|
+
@json_key ||= if @json_key_path.respond_to?(:read)
|
333
|
+
@json_key_path
|
334
|
+
else
|
335
|
+
File.open(@json_key_path)
|
336
|
+
end
|
222
337
|
end
|
223
338
|
end
|