lowdown 0.2.0 → 0.3.0
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/.rubocop.yml +121 -0
- data/Gemfile +11 -3
- data/README.md +145 -14
- data/Rakefile +13 -6
- data/bin/lowdown +38 -19
- data/examples/long-running.rb +63 -0
- data/examples/simple.rb +37 -0
- data/lib/lowdown.rb +2 -21
- data/lib/lowdown/certificate.rb +21 -1
- data/lib/lowdown/client.rb +156 -60
- data/lib/lowdown/client/request_group.rb +70 -0
- data/lib/lowdown/connection.rb +257 -182
- data/lib/lowdown/connection/monitor.rb +84 -0
- data/lib/lowdown/mock.rb +57 -49
- data/lib/lowdown/notification.rb +24 -6
- data/lib/lowdown/response.rb +9 -20
- data/lib/lowdown/version.rb +4 -1
- data/lowdown.gemspec +5 -3
- metadata +22 -4
- data/lib/lowdown/threading.rb +0 -188
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab366da937c62aeebaaa7070b47f3f3a108e1460
|
4
|
+
data.tar.gz: 568554722d19bf9899cfdfda1fa96de1698365ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90a597f2d9faae4afe0a4640b3618303b8740de0132fbfd5d9b291526ef3a240b0bbbe35208279197811a021305cb0edff7408c4d96e356a14a7a3a686b4db01
|
7
|
+
data.tar.gz: 81fe7bf93ff1c4fa94090720c32293155a08b59fe2b6e6e0b5ca9dce7feac5d322d0f6c260a9ba68a8476443656710882dd33610b824a21601131cc9db96abba
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.1
|
3
|
+
|
4
|
+
Documentation:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
# They are idiomatic
|
8
|
+
Lint/AssignmentInCondition:
|
9
|
+
Enabled: false
|
10
|
+
|
11
|
+
Lint/RescueException:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Lint/StringConversionInInterpolation:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Lint/UselessAssignment:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Lint/UnusedMethodArgument:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Lint/HandleExceptions:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Metrics/LineLength:
|
27
|
+
Max: 120
|
28
|
+
AllowURI: true
|
29
|
+
URISchemes:
|
30
|
+
- http
|
31
|
+
- https
|
32
|
+
|
33
|
+
# Arbitrary max lengths for classes simply do not work and enabling this will
|
34
|
+
# lead to a never ending stream of annoyance and changes.
|
35
|
+
Metrics/ClassLength:
|
36
|
+
Enabled: false
|
37
|
+
|
38
|
+
# Arbitrary max lengths for modules simply do not work and enabling this will
|
39
|
+
# lead to a never ending stream of annoyance and changes.
|
40
|
+
Metrics/ModuleLength:
|
41
|
+
Enabled: false
|
42
|
+
|
43
|
+
# Arbitrary max lengths for methods simply do not work and enabling this will
|
44
|
+
# lead to a never ending stream of annoyance and changes.
|
45
|
+
Metrics/MethodLength:
|
46
|
+
Enabled: false
|
47
|
+
|
48
|
+
# No enforced convention here.
|
49
|
+
Metrics/BlockNesting:
|
50
|
+
Enabled: false
|
51
|
+
|
52
|
+
# It will be obvious which code is complex, Rubocop should only lint simple
|
53
|
+
# rules for us.
|
54
|
+
Metrics/AbcSize:
|
55
|
+
Enabled: false
|
56
|
+
|
57
|
+
# It will be obvious which code is complex, Rubocop should only lint simple
|
58
|
+
# rules for us.
|
59
|
+
Metrics/CyclomaticComplexity:
|
60
|
+
Enabled: false
|
61
|
+
|
62
|
+
# It will be obvious which code is complex, Rubocop should only lint simple
|
63
|
+
# rules for us.
|
64
|
+
Metrics/PerceivedComplexity:
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
Metrics/ParameterLists:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
Style/GuardClause:
|
71
|
+
Enabled: false
|
72
|
+
|
73
|
+
Style/Alias:
|
74
|
+
EnforcedStyle: prefer_alias_method
|
75
|
+
|
76
|
+
Style/AsciiComments:
|
77
|
+
Enabled: false
|
78
|
+
|
79
|
+
Style/SignalException:
|
80
|
+
EnforcedStyle: only_raise
|
81
|
+
|
82
|
+
Style/DoubleNegation:
|
83
|
+
Enabled: false
|
84
|
+
|
85
|
+
Style/EmptyLines:
|
86
|
+
Exclude:
|
87
|
+
- 'test/test_helper.rb'
|
88
|
+
|
89
|
+
Style/FileName:
|
90
|
+
Exclude:
|
91
|
+
- 'examples/*.rb'
|
92
|
+
|
93
|
+
Style/GlobalVars:
|
94
|
+
Enabled: false
|
95
|
+
|
96
|
+
# Should have `EnforcedStyle: hash_rockets`, because that’s the only consistent style,
|
97
|
+
# but that incorrectly flags keyword arguments as well.
|
98
|
+
Style/HashSyntax:
|
99
|
+
Enabled: false
|
100
|
+
|
101
|
+
Style/IfInsideElse:
|
102
|
+
Enabled: false
|
103
|
+
|
104
|
+
Style/IndentHash:
|
105
|
+
Enabled: false
|
106
|
+
|
107
|
+
Style/ParallelAssignment:
|
108
|
+
Enabled: false
|
109
|
+
|
110
|
+
Style/StringLiterals:
|
111
|
+
EnforcedStyle: double_quotes
|
112
|
+
|
113
|
+
Style/StructInheritance:
|
114
|
+
Enabled: false
|
115
|
+
|
116
|
+
Style/TrailingBlankLines:
|
117
|
+
EnforcedStyle: final_blank_line
|
118
|
+
|
119
|
+
Style/TrailingCommaInLiteral:
|
120
|
+
EnforcedStyleForMultiline: comma
|
121
|
+
|
data/Gemfile
CHANGED
@@ -1,9 +1,17 @@
|
|
1
|
-
source
|
1
|
+
source "https://rubygems.org"
|
2
2
|
|
3
|
-
#gem
|
3
|
+
# gem "http-2", :git => "https://github.com/alloy/http-2.git", :branch => "apns"
|
4
|
+
|
5
|
+
# Test on minimum required dependency for now.
|
6
|
+
# gem "celluloid-io", "0.17.0"
|
4
7
|
|
5
8
|
gemspec
|
6
9
|
|
10
|
+
group :development do
|
11
|
+
gem "rubocop"
|
12
|
+
end
|
13
|
+
|
7
14
|
group :doc do
|
8
|
-
gem
|
15
|
+
gem "yard"
|
9
16
|
end
|
17
|
+
|
data/README.md
CHANGED
@@ -4,18 +4,29 @@
|
|
4
4
|
|
5
5
|
[](https://travis-ci.org/alloy/lowdown)
|
6
6
|
|
7
|
-
|
7
|
+
NOTE: _This is not battle-tested yet. This will follow over the next few weeks._
|
8
8
|
|
9
|
-
|
9
|
+
Lowdown is a Ruby client for the HTTP/2 version of the Apple Push Notification Service.
|
10
10
|
|
11
|
-
|
12
|
-
instance a daemon, is beyond the scope of this library. We might release an extra daemon/server tool in the future that
|
13
|
-
provides this functionality, but for now you should simply use the Client provided in this library without the block
|
14
|
-
form (which automatically closes the connection) and build your own daemon/server setup, as required.
|
11
|
+
For efficiency, multiple notification requests are multiplexed and a single client can manage a pool of connections.
|
15
12
|
|
16
|
-
|
13
|
+
```
|
14
|
+
$ bundle exec ruby examples/simple.rb path/to/certificate.pem development <device-token>
|
15
|
+
Sent notification with ID: 13
|
16
|
+
Sent notification with ID: 1
|
17
|
+
Sent notification with ID: 10
|
18
|
+
Sent notification with ID: 7
|
19
|
+
Sent notification with ID: 25
|
20
|
+
...
|
21
|
+
Sent notification with ID: 10000
|
22
|
+
Sent notification with ID: 9984
|
23
|
+
Sent notification with ID: 9979
|
24
|
+
Sent notification with ID: 9992
|
25
|
+
Sent notification with ID: 9999
|
26
|
+
Finished in 14.98157 seconds
|
27
|
+
```
|
17
28
|
|
18
|
-
|
29
|
+
_This example was run with a pool of 10 connections._
|
19
30
|
|
20
31
|
## Installation
|
21
32
|
|
@@ -25,19 +36,135 @@ Add this line to your application's Gemfile:
|
|
25
36
|
gem 'lowdown'
|
26
37
|
```
|
27
38
|
|
28
|
-
|
29
|
-
|
30
|
-
$ bundle install
|
31
|
-
|
32
|
-
Or install it yourself as:
|
39
|
+
Or install it yourself, for instance for the command-line usage, as:
|
33
40
|
|
34
|
-
|
41
|
+
```
|
42
|
+
$ gem install lowdown
|
43
|
+
```
|
35
44
|
|
36
45
|
## Usage
|
37
46
|
|
38
47
|
You can use the `lowdown` bin that comes with this gem or for code usage see
|
39
48
|
[the documentation](http://www.rubydoc.info/gems/lowdown).
|
40
49
|
|
50
|
+
There are mainly two different modes in which you’ll typically use [this client][client]. Either you deliver a batch of
|
51
|
+
notifications every now and then, in which case you only want to open a connection to the remote service when needed, or
|
52
|
+
you need to be able to continuously deliver transactional notifications, in which case you’ll want to maintain
|
53
|
+
persistent connections. You can find examples of both these modes in the `examples` directory.
|
54
|
+
|
55
|
+
But first things first, this is how you create [a notification object][notification]:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
notification = Lowdown::Notification.new(:token => "device-token", :payload => { :alert => "Hello World!" })
|
59
|
+
```
|
60
|
+
|
61
|
+
There’s plenty more options for a notification, please refer to [the Notification documentation][notification].
|
62
|
+
|
63
|
+
### Short-lived connection
|
64
|
+
|
65
|
+
After obtaining a client, the simplest way to open a connection for a short period is by passing a block to `connect`.
|
66
|
+
This will open the connection, yield the block, and close the connection by the end of the block:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
client = Lowdown::Client.production(true, File.read("path/to/certificate.pem")
|
70
|
+
client.connect do |group|
|
71
|
+
# ...
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
### Persistent connection
|
76
|
+
|
77
|
+
The trick to creating a persistent connection is to specify the `keep_alive: true` option when creating the client:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
client = Lowdown::Client.production(true, File.read("path/to/certificate.pem"), keep_alive: true)
|
81
|
+
|
82
|
+
# Send a batch of notifications
|
83
|
+
client.group do |group|
|
84
|
+
# ...
|
85
|
+
end
|
86
|
+
|
87
|
+
# Send another batch of notifications
|
88
|
+
client.group do |group|
|
89
|
+
# ...
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
One big difference you’ll notice with the short-lived connection example, is that you no longer use the `Client#connect`
|
94
|
+
method, nor do you close the connection (at least not until your process ends). Instead you use the `group` method to
|
95
|
+
group a set of deliveries.
|
96
|
+
|
97
|
+
### Grouping requests
|
98
|
+
|
99
|
+
Because Lowdown uses background threads to deliver notifications, the thread you’re delivering them _from_ would
|
100
|
+
normally chug along, which is often not what you’d want. To solve this, the `group` method provides you with [a group
|
101
|
+
object][group] which allows you to handle responses for the requests made in that group and halts the caller thread
|
102
|
+
until all responses have been handled.
|
103
|
+
|
104
|
+
All responses in a group will be handled in a single background thread, without halting the connection threads.
|
105
|
+
|
106
|
+
In typical Ruby fashion, a group provides a way to specify callbacks as blocks:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
group.send_notification(notification) do |response|
|
110
|
+
# ...
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
But there’s another possiblity, which is to provide [a delegate object][delegate] which gets a message sent for each
|
115
|
+
response:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class Delegate
|
119
|
+
def handle_apns_response(response, context:)
|
120
|
+
# ...
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
delegate = Delegate.new
|
125
|
+
|
126
|
+
client.group do |group|
|
127
|
+
group.send_notification(notification, delegate: delegate)
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
Keep in mind that, like with the block version, this message is sent on the group’s background thread.
|
132
|
+
|
133
|
+
### Threading
|
134
|
+
|
135
|
+
While we’re on the topic of threading anyways, here’s an important thing to keep in mind; each set of `group` callbacks
|
136
|
+
is performed on its own thread. It is thus _your_ responsibility to take this into account. E.g. if you are planning to
|
137
|
+
update a DB model with the status of a notification delivery, be sure to respect the treading rules of your DB client,
|
138
|
+
which usually means to not re-use models that were loaded on a different thread.
|
139
|
+
|
140
|
+
A simple approach to this is by passing the data you need to be able to update the DB as a `context`, which can be any
|
141
|
+
type of object or an array objects:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
group.send_notification(notification, context: model.id) do |response, model_id|
|
145
|
+
reloaded_model = Model.find(model_id)
|
146
|
+
if response.success?
|
147
|
+
reloaded_model.touch(:sent_at)
|
148
|
+
else
|
149
|
+
reloaded_model.update_attribute(:last_response, response.status)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
### Connection pool
|
155
|
+
|
156
|
+
When you need to be able to deliver many notifications in a short amount of time, it can be beneficial to open multiple
|
157
|
+
connections to the remote service. By default Lowdown will initialize clients with a single connection, but you may
|
158
|
+
increase this with the `pool_size` option:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
Lowdown::Client.production(true, File.read("path/to/certificate.pem"), pool_size: 3)
|
162
|
+
```
|
163
|
+
|
164
|
+
## Related tool ☞
|
165
|
+
|
166
|
+
Also checkout [this library](https://github.com/alloy/time_zone_scheduler) for scheduling across time zones.
|
167
|
+
|
41
168
|
## Contributing
|
42
169
|
|
43
170
|
Bug reports and pull requests are welcome on GitHub at https://github.com/alloy/lowdown.
|
@@ -46,3 +173,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/alloy/
|
|
46
173
|
|
47
174
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
48
175
|
|
176
|
+
[client]: http://www.rubydoc.info/gems/lowdown/Lowdown/Client
|
177
|
+
[notification]: http://www.rubydoc.info/gems/lowdown/Lowdown/Notification
|
178
|
+
[group]: http://www.rubydoc.info/gems/lowdown/Lowdown/RequestGroup
|
179
|
+
[delegate]: http://www.rubydoc.info/gems/lowdown/Lowdown/Connection/DelegateProtocol
|
data/Rakefile
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
desc "Install all dependencies"
|
2
2
|
task :bootstrap do
|
3
|
-
if system(
|
3
|
+
if system("which bundle")
|
4
4
|
sh "bundle install"
|
5
|
-
#sh "git submodule update --init"
|
5
|
+
# sh "git submodule update --init"
|
6
6
|
else
|
7
7
|
$stderr.puts "\033[0;31m[!] Please install the bundler gem manually: $ [sudo] gem install bundler\e[0m"
|
8
8
|
exit 1
|
@@ -10,23 +10,30 @@ task :bootstrap do
|
|
10
10
|
end
|
11
11
|
|
12
12
|
begin
|
13
|
-
require
|
13
|
+
require "bundler/gem_tasks"
|
14
14
|
|
15
15
|
desc "Generate documentation"
|
16
16
|
task :doc do
|
17
17
|
sh "yard doc"
|
18
18
|
end
|
19
19
|
|
20
|
+
desc "Run rubocop"
|
21
|
+
task :rubocop do
|
22
|
+
sh "rubocop"
|
23
|
+
end
|
24
|
+
|
20
25
|
require "rake/testtask"
|
21
26
|
Rake::TestTask.new(:test) do |t|
|
22
27
|
t.options = "--verbose"
|
23
28
|
t.libs << "test"
|
24
29
|
t.libs << "lib"
|
25
|
-
t.test_files = FileList[
|
30
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
26
31
|
end
|
27
32
|
|
28
|
-
task :default => :test
|
33
|
+
task :default => [:test, :rubocop]
|
29
34
|
|
30
35
|
rescue LoadError
|
31
|
-
$stderr.puts "\033[0;33m[!] Disabling rake tasks because the environment couldn’t be loaded. Be sure to run `rake
|
36
|
+
$stderr.puts "\033[0;33m[!] Disabling rake tasks because the environment couldn’t be loaded. Be sure to run `rake " \
|
37
|
+
"bootstrap` first.\e[0m"
|
32
38
|
end
|
39
|
+
|
data/bin/lowdown
CHANGED
@@ -6,17 +6,20 @@ include Lowdown
|
|
6
6
|
require "json"
|
7
7
|
require "optparse"
|
8
8
|
|
9
|
-
|
9
|
+
Celluloid.logger.level = Logger::WARN
|
10
|
+
|
11
|
+
options = { :payload => {}, :custom_data => {}, :pool_size => 1 }
|
10
12
|
|
11
13
|
OPTION_PARSER = OptionParser.new do |opts|
|
12
14
|
opts.banner = "Usage: lowdown [options] <tokens …>"
|
13
15
|
|
14
|
-
opts.on("-v", "--version", "Print version") do
|
16
|
+
opts.on("-v", "--version", "Print version") do
|
15
17
|
puts VERSION
|
16
18
|
exit
|
17
19
|
end
|
18
20
|
|
19
|
-
opts.on("-m", "--alert ALERT", "Body of the alert to send in the push notification"
|
21
|
+
opts.on("-m", "--alert ALERT", "Body of the alert to send in the push notification (the %d format code will be " \
|
22
|
+
"replaced by the notification ID)") do |alert|
|
20
23
|
options[:alert] = alert
|
21
24
|
end
|
22
25
|
|
@@ -37,7 +40,8 @@ OPTION_PARSER = OptionParser.new do |opts|
|
|
37
40
|
options[:custom_data][key] = value
|
38
41
|
end
|
39
42
|
|
40
|
-
opts.on("-P", "--payload PAYLOAD", "JSON payload for notifications, merged with --alert, --badge, --sound, and
|
43
|
+
opts.on("-P", "--payload PAYLOAD", "JSON payload for notifications, merged with --alert, --badge, --sound, and " \
|
44
|
+
"--data") do |payload|
|
41
45
|
options[:payload] = JSON.parse(payload)
|
42
46
|
end
|
43
47
|
|
@@ -45,7 +49,8 @@ OPTION_PARSER = OptionParser.new do |opts|
|
|
45
49
|
options[:topic] = topic
|
46
50
|
end
|
47
51
|
|
48
|
-
opts.on("-e", "--environment ENV", "Environment to send push notification (production or development), defaults to
|
52
|
+
opts.on("-e", "--environment ENV", "Environment to send push notification (production or development), defaults to " \
|
53
|
+
"production if the certificate supports that or otherwise development") do |env|
|
49
54
|
options[:env] = env
|
50
55
|
end
|
51
56
|
|
@@ -56,6 +61,19 @@ OPTION_PARSER = OptionParser.new do |opts|
|
|
56
61
|
opts.on("-p", "--passphrase PASSPHRASE", "Certificate passphrase") do |passphrase|
|
57
62
|
options[:certificate_passphrase] = passphrase
|
58
63
|
end
|
64
|
+
|
65
|
+
opts.on("-n", "--connections NUMBER", "Number of simultaneous connections to make") do |pool_size|
|
66
|
+
options[:pool_size] = pool_size.to_i
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on("--debug", "Debug logging") do
|
70
|
+
Celluloid.logger.level = Logger::INFO
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on("--verbose", "Verbose logging") do
|
74
|
+
$CELLULOID_DEBUG = true
|
75
|
+
Celluloid.logger.level = Logger::DEBUG
|
76
|
+
end
|
59
77
|
end
|
60
78
|
|
61
79
|
OPTION_PARSER.parse!
|
@@ -70,13 +88,15 @@ end
|
|
70
88
|
|
71
89
|
certificate = nil
|
72
90
|
file, passphrase = options.values_at(:certificate_file, :certificate_passphrase)
|
91
|
+
# rubocop:disable Style/RescueModifier
|
73
92
|
unless file && File.exist?(file) && certificate = (Certificate.from_pem_data(File.read(file), passphrase) rescue nil)
|
93
|
+
# rubocop:enable Style/RescueModifier
|
74
94
|
help! "A valid certificate path is required."
|
75
95
|
end
|
76
96
|
|
77
97
|
production = false
|
78
98
|
if options[:env]
|
79
|
-
unless %w
|
99
|
+
unless %w( production development ).include?(options[:env])
|
80
100
|
help! "Invalid environment specified."
|
81
101
|
end
|
82
102
|
production = options[:env] == "production"
|
@@ -89,7 +109,7 @@ else
|
|
89
109
|
end
|
90
110
|
|
91
111
|
begin
|
92
|
-
client = Client.production(production, certificate)
|
112
|
+
client = Client.production(production, certificate: certificate, pool_size: options[:pool_size])
|
93
113
|
rescue ArgumentError => e
|
94
114
|
help! e.message
|
95
115
|
end
|
@@ -99,23 +119,22 @@ payload.merge!(options[:custom_data])
|
|
99
119
|
payload["alert"] = options[:alert] if options[:alert]
|
100
120
|
payload["badge"] = options[:badge] if options[:badge]
|
101
121
|
payload["sound"] = options[:sound] if options[:sound]
|
102
|
-
payload["content-available"] = options[:content_available] ? 1 : 0 if options.
|
103
|
-
if payload.empty?
|
104
|
-
help! "No payload data specified."
|
105
|
-
end
|
122
|
+
payload["content-available"] = options[:content_available] ? 1 : 0 if options.key?(:content_available)
|
106
123
|
|
107
|
-
if
|
108
|
-
|
109
|
-
end
|
124
|
+
help! "No payload data specified." if payload.empty?
|
125
|
+
help! "No device tokens specified." if tokens.empty?
|
110
126
|
|
111
|
-
notifications = tokens.map
|
112
|
-
Notification.new(:token => token, :
|
127
|
+
notifications = tokens.map do |token|
|
128
|
+
Notification.new(:token => token, :payload => payload.dup, :topic => options[:topic]).tap do |notification|
|
129
|
+
notification.payload["alert"] = notification.payload["alert"] % notification.id
|
130
|
+
end
|
113
131
|
end
|
114
132
|
|
115
|
-
client.connect do
|
133
|
+
client.connect do |group|
|
116
134
|
notifications.each do |notification|
|
117
|
-
|
118
|
-
|
135
|
+
group.send_notification(notification) do |response|
|
136
|
+
Celluloid.logger.unknown "[#{notification.token} ##{notification.id}] #{response}"
|
119
137
|
end
|
120
138
|
end
|
121
139
|
end
|
140
|
+
|