lowdown 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/alloy/lowdown.svg?branch=master)](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
|
+
|