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.
data/lib/fcm.rb CHANGED
@@ -1,190 +1,263 @@
1
- require 'httparty'
2
- require 'cgi'
3
- require 'json'
1
+ require "faraday"
2
+ require "cgi"
3
+ require "json"
4
+ require "googleauth"
4
5
 
5
6
  class FCM
6
- include HTTParty
7
- base_uri 'https://fcm.googleapis.com/fcm'
8
- default_timeout 30
9
- format :json
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 = 'https://android.googleapis.com/gcm'
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
- # "collapse_key": "score_update",
23
- # "time_to_live": 108,
24
- # "delay_while_idle": true,
25
- # "registration_ids": ["4", "8", "15", "16", "23", "42"],
26
- # "data" : {
27
- # "score": "5x1",
28
- # "time": "15:10"
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(registration_ids: ["4sdsx", "8sdsd"], {data: {score: "5x1"}})
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
- params = {
37
- body: post_body.to_json,
38
- headers: {
39
- 'Authorization' => "key=#{@api_key}",
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: 'create',
50
- notification_key_name: key_name)
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
- response = nil
96
+ extra_headers = {
97
+ "project_id" => project_id,
98
+ }
62
99
 
63
- for_uri(GROUP_NOTIFICATION_BASE_URI) do
64
- response = self.class.post('/notification', params.merge(@client_options))
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: 'add',
73
- notification_key_name: key_name,
74
- notification_key: notification_key)
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
- response = nil
113
+ extra_headers = {
114
+ "project_id" => project_id,
115
+ }
86
116
 
87
- for_uri(GROUP_NOTIFICATION_BASE_URI) do
88
- response = self.class.post('/notification', params.merge(@client_options))
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: 'remove',
96
- notification_key_name: key_name,
97
- notification_key: notification_key)
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
- response = nil
130
+ extra_headers = {
131
+ "project_id" => project_id,
132
+ }
109
133
 
110
- for_uri(GROUP_NOTIFICATION_BASE_URI) do
111
- response = self.class.post('/notification', params.merge(@client_options))
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
- response = nil
145
+ extra_headers = {
146
+ "project_id" => project_id,
147
+ }
130
148
 
131
- for_uri(GROUP_NOTIFICATION_BASE_URI) do
132
- response = self.class.post('/notification', params.merge(@client_options))
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
- params = {
141
- body: body.to_json,
142
- headers: {
143
- 'Authorization' => "key=#{@api_key}",
144
- 'Content-Type' => 'application/json'
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
- response = self.class.post('/send', params.merge(@client_options))
149
- build_response(response)
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 =~ /[a-zA-Z0-9\-_.~%]+/
154
- send_with_notification_key('/topics/' + topic, options)
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
- current_uri = self.class.base_uri
162
- self.class.base_uri uri
163
- yield
164
- self.class.base_uri current_uri
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
- { registration_ids: registration_ids }.merge(options)
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.code }
174
- case response.code
246
+ response_hash = { body: body, headers: response.headers, status_code: response.status }
247
+ case response.status
175
248
  when 200
176
- response_hash[:response] = 'success'
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] = 'Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields.'
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] = 'There was an error authenticating the sender account.'
256
+ response_hash[:response] = "There was an error authenticating the sender account."
184
257
  when 503
185
- response_hash[:response] = 'Server is temporarily unavailable.'
258
+ response_hash[:response] = "Server is temporarily unavailable."
186
259
  when 500..599
187
- response_hash[:response] = 'There was an internal error in the FCM server while trying to process the request.'
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['canonical_ids'] > 0
196
- body['results'].each_with_index do |result, index|
197
- canonical_ids << { old: registration_ids[index], new: result['registration_id'] } if has_canonical_id?(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['failure'] > 0
208
- body['results'].each_with_index do |result, index|
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['registration_id'].nil?
297
+ !result["registration_id"].nil?
218
298
  end
219
299
 
220
300
  def is_not_registered?(result)
221
- result['error'] == 'NotRegistered'
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