fcm 2.0.2 → 2.0.3
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 +4 -4
- data/.github/workflows/ci.yml +16 -1
- data/.hound.yml +2 -0
- data/.rubocop.yml +67 -0
- data/Gemfile +8 -5
- data/README.md +40 -0
- data/Rakefile +9 -7
- data/fcm.gemspec +10 -8
- data/lib/fcm.rb +125 -57
- data/spec/fcm_spec.rb +247 -161
- data/spec/spec_helper.rb +7 -5
- metadata +33 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f15e3ef0efb046941ee9c8e615206668feb9e0f087e03be74b25303d40a8ebfe
|
|
4
|
+
data.tar.gz: 492c261d5f4d58a0511d809aa7ad7278fdc18462590459ec8027a7215b146724
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd794a9f1aedf3f82ae9c1e43f299ab8be6a432738383d6db2d8fc1add8b44577e50ee1563d00bd7878d801e2eff46994b9a84d06b18a9b4892d2d6d9146a950
|
|
7
|
+
data.tar.gz: bd237deb478ab400e9e8e82cc6679c6c2560727757c8a74b25a2db51df7473b012f00b8019402f7426c601a31710dcdd28710cab9b1e96de132bdbd41b31e109
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
strategy:
|
|
15
15
|
matrix:
|
|
16
|
-
ruby: ['2.7', '3.0', '3.1', '3.3']
|
|
16
|
+
ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
|
|
17
17
|
|
|
18
18
|
steps:
|
|
19
19
|
- uses: actions/checkout@master
|
|
@@ -28,3 +28,18 @@ jobs:
|
|
|
28
28
|
- name: Run tests
|
|
29
29
|
run: |
|
|
30
30
|
bundle exec rspec
|
|
31
|
+
|
|
32
|
+
lint:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@master
|
|
36
|
+
|
|
37
|
+
- name: Set up Ruby
|
|
38
|
+
uses: ruby/setup-ruby@v1
|
|
39
|
+
with:
|
|
40
|
+
ruby-version: '3.3'
|
|
41
|
+
bundler: default
|
|
42
|
+
bundler-cache: true
|
|
43
|
+
|
|
44
|
+
- name: Run RuboCop
|
|
45
|
+
run: bundle exec rubocop --parallel
|
data/.hound.yml
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
|
|
4
|
+
AllCops:
|
|
5
|
+
TargetRubyVersion: 2.7
|
|
6
|
+
NewCops: enable
|
|
7
|
+
SuggestExtensions: false
|
|
8
|
+
Exclude:
|
|
9
|
+
- "vendor/**/*"
|
|
10
|
+
- "tmp/**/*"
|
|
11
|
+
|
|
12
|
+
# This is a small gem; some style cops add noise without value.
|
|
13
|
+
Style/Documentation:
|
|
14
|
+
Enabled: false
|
|
15
|
+
Style/StringLiterals:
|
|
16
|
+
EnforcedStyle: double_quotes
|
|
17
|
+
Style/StringLiteralsInInterpolation:
|
|
18
|
+
EnforcedStyle: double_quotes
|
|
19
|
+
|
|
20
|
+
# Renaming `is_not_registered?` / `has_canonical_id?` would change the gem's
|
|
21
|
+
# (admittedly internal) Ruby surface area. Out of scope for a lint pass.
|
|
22
|
+
Naming/PredicatePrefix:
|
|
23
|
+
Enabled: false
|
|
24
|
+
|
|
25
|
+
Layout/LineLength:
|
|
26
|
+
Max: 120
|
|
27
|
+
Exclude:
|
|
28
|
+
- "fcm.gemspec"
|
|
29
|
+
|
|
30
|
+
# Out of scope for a lint pass: bumping required_ruby_version is a semver-relevant
|
|
31
|
+
# change for gem consumers and should be considered separately.
|
|
32
|
+
Gemspec/RequiredRubyVersion:
|
|
33
|
+
Enabled: false
|
|
34
|
+
|
|
35
|
+
# Enabling MFA is a publishing-policy decision for the gem maintainer, not
|
|
36
|
+
# something a lint pass should silently introduce.
|
|
37
|
+
Gemspec/RequireMFA:
|
|
38
|
+
Enabled: false
|
|
39
|
+
|
|
40
|
+
# Code metrics are coarse signals that fight a lot of legacy code.
|
|
41
|
+
Metrics/AbcSize:
|
|
42
|
+
Enabled: false
|
|
43
|
+
Metrics/ClassLength:
|
|
44
|
+
Enabled: false
|
|
45
|
+
Metrics/CyclomaticComplexity:
|
|
46
|
+
Enabled: false
|
|
47
|
+
Metrics/MethodLength:
|
|
48
|
+
Enabled: false
|
|
49
|
+
Metrics/PerceivedComplexity:
|
|
50
|
+
Enabled: false
|
|
51
|
+
Metrics/BlockLength:
|
|
52
|
+
Exclude:
|
|
53
|
+
- "spec/**/*"
|
|
54
|
+
- "fcm.gemspec"
|
|
55
|
+
|
|
56
|
+
# Specs are intentionally long, descriptive, and use `.should` syntax in
|
|
57
|
+
# places — none of these add value here.
|
|
58
|
+
RSpec/ExampleLength:
|
|
59
|
+
Enabled: false
|
|
60
|
+
RSpec/MultipleExpectations:
|
|
61
|
+
Enabled: false
|
|
62
|
+
RSpec/MultipleMemoizedHelpers:
|
|
63
|
+
Enabled: false
|
|
64
|
+
RSpec/NestedGroups:
|
|
65
|
+
Enabled: false
|
|
66
|
+
RSpec/NoExpectationExample:
|
|
67
|
+
Enabled: false
|
data/Gemfile
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
source "https://rubygems.org"
|
|
2
4
|
gemspec
|
|
3
5
|
|
|
4
|
-
gem
|
|
5
|
-
gem
|
|
6
|
-
gem
|
|
7
|
-
gem
|
|
8
|
-
gem
|
|
6
|
+
gem "ci_reporter_rspec"
|
|
7
|
+
gem "rake"
|
|
8
|
+
gem "rspec"
|
|
9
|
+
gem "rubocop", require: false
|
|
10
|
+
gem "rubocop-rspec", require: false
|
|
11
|
+
gem "webmock"
|
data/README.md
CHANGED
|
@@ -54,6 +54,39 @@ fcm = FCM.new(
|
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Request timeouts
|
|
58
|
+
|
|
59
|
+
Both the read timeout and the TCP connect timeout are configurable via
|
|
60
|
+
`http_options`. Both default to `DEFAULT_TIMEOUT` (30s):
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
fcm = FCM.new(
|
|
64
|
+
GOOGLE_APPLICATION_CREDENTIALS_PATH,
|
|
65
|
+
FIREBASE_PROJECT_ID,
|
|
66
|
+
timeout: 10, # response read timeout
|
|
67
|
+
open_timeout: 5 # TCP connect timeout — fail fast on stuck handshakes
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## HTTP keep-alive
|
|
72
|
+
|
|
73
|
+
By default each request opens a fresh TCP/TLS connection to FCM. For high-volume
|
|
74
|
+
senders this dominates per-request latency. Pass `keep_alive_connections: true`
|
|
75
|
+
to reuse a thread-local connection (per host) backed by `net-http-persistent`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
fcm = FCM.new(
|
|
79
|
+
GOOGLE_APPLICATION_CREDENTIALS_PATH,
|
|
80
|
+
FIREBASE_PROJECT_ID,
|
|
81
|
+
keep_alive_connections: true,
|
|
82
|
+
keep_alive_idle_timeout_seconds: 30, # optional, default 30
|
|
83
|
+
keep_alive_pool_size: 1 # optional, default 1
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`Net::HTTP` is not thread-safe, so connections are cached per `(thread, uri)`.
|
|
88
|
+
Connections are dropped on error so half-closed sockets are not reused.
|
|
89
|
+
|
|
57
90
|
## Usage
|
|
58
91
|
|
|
59
92
|
## HTTP v1 API
|
|
@@ -274,6 +307,13 @@ The guide to set up an iOS app to get notifications is here: [Setting up a FCM C
|
|
|
274
307
|
|
|
275
308
|
## ChangeLog
|
|
276
309
|
|
|
310
|
+
### 2.0.3
|
|
311
|
+
- Add opt-in HTTP keep-alive via `keep_alive_connections` [#145](https://github.com/decision-labs/fcm/pull/145)
|
|
312
|
+
- Add configurable `open_timeout` via `http_options` [#146](https://github.com/decision-labs/fcm/pull/146)
|
|
313
|
+
- Cover modern Ruby versions in the CI test matrix [#147](https://github.com/decision-labs/fcm/pull/147)
|
|
314
|
+
- Replace Hound with RuboCop [#148](https://github.com/decision-labs/fcm/pull/148)
|
|
315
|
+
- Drop unused `cgi` require and duplicate Gemfile `googleauth` [#149](https://github.com/decision-labs/fcm/pull/149)
|
|
316
|
+
|
|
277
317
|
### 2.0.2
|
|
278
318
|
- Ensure JSON key path is an actual file or IO object before opening [#134](https://github.com/spacialdb/fcm/pull/134)
|
|
279
319
|
|
data/Rakefile
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core/rake_task"
|
|
2
4
|
require "bundler/gem_tasks"
|
|
3
5
|
require "rake/tasklib"
|
|
4
|
-
require
|
|
6
|
+
require "ci/reporter/rake/rspec"
|
|
5
7
|
|
|
6
|
-
RSpec::Core::RakeTask.new(:
|
|
7
|
-
t.pattern =
|
|
8
|
+
RSpec::Core::RakeTask.new(spec: ["ci:setup:rspec"]) do |t|
|
|
9
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
11
|
-
spec.pattern =
|
|
12
|
-
spec.rspec_opts = [
|
|
13
|
+
spec.pattern = "spec/**/*_spec.rb"
|
|
14
|
+
spec.rspec_opts = ["--format documentation"]
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
task :
|
|
17
|
+
task default: :spec
|
data/fcm.gemspec
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.push File.expand_path("lib", __dir__)
|
|
3
4
|
|
|
4
5
|
Gem::Specification.new do |s|
|
|
5
6
|
s.name = "fcm"
|
|
6
|
-
s.version = "2.0.
|
|
7
|
+
s.version = "2.0.3"
|
|
7
8
|
s.platform = Gem::Platform::RUBY
|
|
8
9
|
s.authors = ["Kashif Rasul", "Shoaib Burq"]
|
|
9
10
|
s.email = ["kashif@decision-labs.com", "shoaib@decision-labs.com"]
|
|
10
11
|
s.homepage = "https://github.com/decision-labs/fcm"
|
|
11
|
-
s.summary =
|
|
12
|
-
s.description =
|
|
12
|
+
s.summary = "Reliably deliver messages and notifications via FCM"
|
|
13
|
+
s.description = "fcm provides ruby bindings to Firebase Cloud Messaging (FCM) a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost to Android, iOS or Web browsers."
|
|
13
14
|
s.license = "MIT"
|
|
14
15
|
|
|
15
16
|
s.required_ruby_version = ">= 2.4.0"
|
|
16
17
|
|
|
17
18
|
s.files = `git ls-files`.split("\n")
|
|
18
|
-
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
20
20
|
s.require_paths = ["lib"]
|
|
21
21
|
|
|
22
|
-
s.
|
|
23
|
-
s.
|
|
22
|
+
s.add_dependency("faraday", ">= 1.0.0", "< 3.0")
|
|
23
|
+
s.add_dependency("faraday-net_http_persistent", "~> 2.0")
|
|
24
|
+
s.add_dependency("googleauth", "~> 1")
|
|
25
|
+
s.add_dependency("net-http-persistent", "~> 4.0")
|
|
24
26
|
end
|
data/lib/fcm.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "faraday"
|
|
2
|
-
require "cgi"
|
|
3
4
|
require "json"
|
|
4
5
|
require "googleauth"
|
|
5
6
|
|
|
@@ -9,15 +10,27 @@ class FCM
|
|
|
9
10
|
BASE_URI = "https://fcm.googleapis.com"
|
|
10
11
|
BASE_URI_V1 = "https://fcm.googleapis.com/v1/projects/"
|
|
11
12
|
DEFAULT_TIMEOUT = 30
|
|
13
|
+
DEFAULT_KEEP_ALIVE_IDLE_TIMEOUT_SECONDS = 30
|
|
14
|
+
DEFAULT_KEEP_ALIVE_POOL_SIZE = 1
|
|
12
15
|
|
|
13
16
|
GROUP_NOTIFICATION_BASE_URI = "https://android.googleapis.com"
|
|
14
17
|
INSTANCE_ID_API = "https://iid.googleapis.com"
|
|
15
|
-
TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]
|
|
18
|
+
TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/.freeze
|
|
16
19
|
|
|
17
20
|
def initialize(json_key_path = "", project_name = "", http_options = {})
|
|
18
21
|
@json_key_path = json_key_path
|
|
19
22
|
@project_name = project_name
|
|
20
23
|
@http_options = http_options
|
|
24
|
+
@keep_alive_connections = http_options.fetch(:keep_alive_connections, false)
|
|
25
|
+
@keep_alive_idle_timeout_seconds =
|
|
26
|
+
http_options.fetch(:keep_alive_idle_timeout_seconds, DEFAULT_KEEP_ALIVE_IDLE_TIMEOUT_SECONDS)
|
|
27
|
+
@keep_alive_pool_size = http_options.fetch(:keep_alive_pool_size, DEFAULT_KEEP_ALIVE_POOL_SIZE)
|
|
28
|
+
|
|
29
|
+
# Per-instance key for the thread-local connection cache so multiple FCM
|
|
30
|
+
# clients in the same process do not share sockets.
|
|
31
|
+
@thread_connections_key = :"_fcm_connections_#{object_id}"
|
|
32
|
+
|
|
33
|
+
require "faraday/net_http_persistent" if @keep_alive_connections
|
|
21
34
|
end
|
|
22
35
|
|
|
23
36
|
# See https://firebase.google.com/docs/cloud-messaging/send-message
|
|
@@ -51,7 +64,7 @@ class FCM
|
|
|
51
64
|
def send_notification_v1(message)
|
|
52
65
|
return if @project_name.empty?
|
|
53
66
|
|
|
54
|
-
post_body = {
|
|
67
|
+
post_body = { message: message }
|
|
55
68
|
for_uri(BASE_URI_V1) do |connection|
|
|
56
69
|
response = connection.post(
|
|
57
70
|
"#{@project_name}/messages:send", post_body.to_json
|
|
@@ -67,7 +80,7 @@ class FCM
|
|
|
67
80
|
notification_key_name: key_name)
|
|
68
81
|
|
|
69
82
|
extra_headers = {
|
|
70
|
-
"project_id" => project_id
|
|
83
|
+
"project_id" => project_id
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
|
|
@@ -84,7 +97,7 @@ class FCM
|
|
|
84
97
|
notification_key: notification_key)
|
|
85
98
|
|
|
86
99
|
extra_headers = {
|
|
87
|
-
"project_id" => project_id
|
|
100
|
+
"project_id" => project_id
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
|
|
@@ -101,7 +114,7 @@ class FCM
|
|
|
101
114
|
notification_key: notification_key)
|
|
102
115
|
|
|
103
116
|
extra_headers = {
|
|
104
|
-
"project_id" => project_id
|
|
117
|
+
"project_id" => project_id
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
|
|
@@ -116,7 +129,7 @@ class FCM
|
|
|
116
129
|
params = { notification_key_name: key_name }
|
|
117
130
|
|
|
118
131
|
extra_headers = {
|
|
119
|
-
"project_id" => project_id
|
|
132
|
+
"project_id" => project_id
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
|
|
@@ -139,11 +152,11 @@ class FCM
|
|
|
139
152
|
end
|
|
140
153
|
|
|
141
154
|
def batch_topic_subscription(topic, registration_tokens)
|
|
142
|
-
manage_topics_relationship(topic, registration_tokens,
|
|
155
|
+
manage_topics_relationship(topic, registration_tokens, "Add")
|
|
143
156
|
end
|
|
144
157
|
|
|
145
158
|
def batch_topic_unsubscription(topic, registration_tokens)
|
|
146
|
-
manage_topics_relationship(topic, registration_tokens,
|
|
159
|
+
manage_topics_relationship(topic, registration_tokens, "Remove")
|
|
147
160
|
end
|
|
148
161
|
|
|
149
162
|
def manage_topics_relationship(topic, registration_tokens, action)
|
|
@@ -165,47 +178,101 @@ class FCM
|
|
|
165
178
|
end
|
|
166
179
|
|
|
167
180
|
def send_to_topic(topic, options = {})
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
return unless topic.gsub(TOPIC_REGEX, "").empty?
|
|
182
|
+
|
|
183
|
+
body = { message: { topic: topic }.merge(options) }
|
|
184
|
+
|
|
185
|
+
for_uri(BASE_URI_V1) do |connection|
|
|
186
|
+
response = connection.post(
|
|
187
|
+
"#{@project_name}/messages:send", body.to_json
|
|
188
|
+
)
|
|
189
|
+
build_response(response)
|
|
177
190
|
end
|
|
178
191
|
end
|
|
179
192
|
|
|
180
193
|
def send_to_topic_condition(condition, options = {})
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
return unless validate_condition?(condition)
|
|
195
|
+
|
|
196
|
+
body = { message: { condition: condition }.merge(options) }
|
|
197
|
+
|
|
198
|
+
for_uri(BASE_URI_V1) do |connection|
|
|
199
|
+
response = connection.post(
|
|
200
|
+
"#{@project_name}/messages:send", body.to_json
|
|
201
|
+
)
|
|
202
|
+
build_response(response)
|
|
190
203
|
end
|
|
191
204
|
end
|
|
192
205
|
|
|
193
206
|
private
|
|
194
207
|
|
|
195
|
-
def for_uri(uri, extra_headers = {})
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
def for_uri(uri, extra_headers = {}, &block)
|
|
209
|
+
if @keep_alive_connections
|
|
210
|
+
with_persistent_connection(uri, extra_headers, &block)
|
|
211
|
+
else
|
|
212
|
+
yield build_one_shot_connection(uri, extra_headers)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def build_one_shot_connection(uri, extra_headers)
|
|
217
|
+
::Faraday.new(url: uri, request: request_options) do |faraday|
|
|
200
218
|
faraday.adapter Faraday.default_adapter
|
|
201
|
-
faraday
|
|
202
|
-
faraday.headers["Authorization"] = "Bearer #{jwt_token}"
|
|
203
|
-
faraday.headers["access_token_auth"]= "true"
|
|
204
|
-
extra_headers.each do |key, value|
|
|
205
|
-
faraday.headers[key] = value
|
|
206
|
-
end
|
|
219
|
+
apply_default_headers(faraday, extra_headers)
|
|
207
220
|
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Reuses a thread-local Faraday connection (one per uri) backed by
|
|
224
|
+
# net-http-persistent so the TCP/TLS handshake and HTTP/2 stream are
|
|
225
|
+
# amortised across requests. Bearer tokens and per-call headers are
|
|
226
|
+
# re-applied each yield because JWTs expire and extra_headers vary.
|
|
227
|
+
# On error, the cached connection is dropped: the underlying socket may
|
|
228
|
+
# be half-closed and reusing it would just fail again.
|
|
229
|
+
def with_persistent_connection(uri, extra_headers)
|
|
230
|
+
connection = persistent_connection_for(uri)
|
|
231
|
+
apply_default_headers(connection, extra_headers)
|
|
208
232
|
yield connection
|
|
233
|
+
rescue StandardError
|
|
234
|
+
discard_persistent_connection(uri)
|
|
235
|
+
raise
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def apply_default_headers(connection, extra_headers)
|
|
239
|
+
connection.headers["Content-Type"] = "application/json"
|
|
240
|
+
connection.headers["Authorization"] = "Bearer #{jwt_token}"
|
|
241
|
+
connection.headers["access_token_auth"] = "true"
|
|
242
|
+
extra_headers.each { |key, value| connection.headers[key] = value }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Net::HTTP is not thread-safe, so connections are cached per (thread, uri)
|
|
246
|
+
# rather than shared across threads.
|
|
247
|
+
def persistent_connection_for(uri)
|
|
248
|
+
thread_connections[uri] ||= build_persistent_connection(uri)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def discard_persistent_connection(uri)
|
|
252
|
+
connection = thread_connections.delete(uri)
|
|
253
|
+
connection.close if connection.respond_to?(:close)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def thread_connections
|
|
257
|
+
Thread.current[@thread_connections_key] ||= {}
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def request_options
|
|
261
|
+
{
|
|
262
|
+
timeout: @http_options.fetch(:timeout, DEFAULT_TIMEOUT),
|
|
263
|
+
open_timeout: @http_options.fetch(:open_timeout, DEFAULT_TIMEOUT)
|
|
264
|
+
}
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def build_persistent_connection(uri)
|
|
268
|
+
::Faraday.new(url: uri, request: request_options) do |faraday|
|
|
269
|
+
# pool_size defaults to 1: we already cache one Faraday connection per
|
|
270
|
+
# (thread, uri), and Net::HTTP is not thread-safe — so a single socket
|
|
271
|
+
# per pool is the safe default. Override only with a specific reason.
|
|
272
|
+
faraday.adapter :net_http_persistent, pool_size: @keep_alive_pool_size do |http|
|
|
273
|
+
http.idle_timeout = @keep_alive_idle_timeout_seconds
|
|
274
|
+
end
|
|
275
|
+
end
|
|
209
276
|
end
|
|
210
277
|
|
|
211
278
|
def build_post_body(registration_ids, options = {})
|
|
@@ -221,9 +288,14 @@ class FCM
|
|
|
221
288
|
response_hash[:response] = "success"
|
|
222
289
|
body = JSON.parse(body) unless body.empty?
|
|
223
290
|
response_hash[:canonical_ids] = build_canonical_ids(body, registration_ids) unless registration_ids.empty?
|
|
224
|
-
|
|
291
|
+
unless registration_ids.empty?
|
|
292
|
+
response_hash[:not_registered_ids] =
|
|
293
|
+
build_not_registered_ids(body, registration_ids)
|
|
294
|
+
end
|
|
225
295
|
when 400
|
|
226
|
-
response_hash[:response] =
|
|
296
|
+
response_hash[:response] =
|
|
297
|
+
"Only applies for JSON requests. Indicates that the request could not be parsed as JSON, " \
|
|
298
|
+
"or it contained invalid fields."
|
|
227
299
|
when 401
|
|
228
300
|
response_hash[:response] = "There was an error authenticating the sender account."
|
|
229
301
|
when 503
|
|
@@ -236,11 +308,9 @@ class FCM
|
|
|
236
308
|
|
|
237
309
|
def build_canonical_ids(body, registration_ids)
|
|
238
310
|
canonical_ids = []
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result)
|
|
243
|
-
end
|
|
311
|
+
if !body.empty? && body["canonical_ids"].positive?
|
|
312
|
+
body["results"].each_with_index do |result, index|
|
|
313
|
+
canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result)
|
|
244
314
|
end
|
|
245
315
|
end
|
|
246
316
|
canonical_ids
|
|
@@ -248,11 +318,9 @@ class FCM
|
|
|
248
318
|
|
|
249
319
|
def build_not_registered_ids(body, registration_id)
|
|
250
320
|
not_registered_ids = []
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
not_registered_ids << registration_id[index] if is_not_registered?(result)
|
|
255
|
-
end
|
|
321
|
+
if !body.empty? && body["failure"].positive?
|
|
322
|
+
body["results"].each_with_index do |result, index|
|
|
323
|
+
not_registered_ids << registration_id[index] if is_not_registered?(result)
|
|
256
324
|
end
|
|
257
325
|
end
|
|
258
326
|
not_registered_ids
|
|
@@ -272,32 +340,32 @@ class FCM
|
|
|
272
340
|
|
|
273
341
|
def validate_condition_format?(condition)
|
|
274
342
|
bad_characters = condition.gsub(
|
|
275
|
-
/(topics|in|\s|\(|\)|(&&)
|
|
343
|
+
/(topics|in|\s|\(|\)|(&&)|!|(\|\|)|'([a-zA-Z0-9\-_.~%]+)')/,
|
|
276
344
|
""
|
|
277
345
|
)
|
|
278
|
-
bad_characters.
|
|
346
|
+
bad_characters.empty?
|
|
279
347
|
end
|
|
280
348
|
|
|
281
349
|
def validate_condition_topics?(condition)
|
|
282
350
|
topics = condition.scan(/(?:^|\S|\s)'([^']*?)'(?:$|\S|\s)/).flatten
|
|
283
|
-
topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").
|
|
351
|
+
topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").empty? }
|
|
284
352
|
end
|
|
285
353
|
|
|
286
354
|
def jwt_token
|
|
287
355
|
scope = "https://www.googleapis.com/auth/firebase.messaging"
|
|
288
356
|
@authorizer ||= Google::Auth::ServiceAccountCredentials.make_creds(
|
|
289
357
|
json_key_io: json_key,
|
|
290
|
-
scope: scope
|
|
358
|
+
scope: scope
|
|
291
359
|
)
|
|
292
360
|
token = @authorizer.fetch_access_token!
|
|
293
361
|
token["access_token"]
|
|
294
362
|
end
|
|
295
363
|
|
|
296
364
|
def raise_credentials_error(param)
|
|
297
|
-
error_msg =
|
|
298
|
-
|
|
365
|
+
error_msg = "credentials must be an IO-like " \
|
|
366
|
+
"object or path. You passed"
|
|
299
367
|
|
|
300
|
-
param_klass = param.nil? ?
|
|
368
|
+
param_klass = param.nil? ? "nil" : "a #{param.class.name}"
|
|
301
369
|
error_msg += " #{param_klass}."
|
|
302
370
|
raise InvalidCredentialError, error_msg
|
|
303
371
|
end
|