apnotic 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +33 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +2 -0
- data/LICENSE.md +21 -0
- data/README.md +225 -0
- data/Rakefile +6 -0
- data/apnotic.gemspec +26 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/apnotic.rb +10 -0
- data/lib/apnotic/connection.rb +136 -0
- data/lib/apnotic/connection_pool.rb +13 -0
- data/lib/apnotic/notification.rb +34 -0
- data/lib/apnotic/response.rb +25 -0
- data/lib/apnotic/stream.rb +71 -0
- data/lib/apnotic/version.rb +3 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4ce6144cfd4d92ded62f555d22b37758855eb2cc
|
4
|
+
data.tar.gz: c239e3fad3adb4a77629d638358eee7f91552c42
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3fd69fe8c84117767cf19b448a3758bc309ca4d6c208e445aba50a71b3b1fb9f793e4b2bde12d2719a5c476eee2a5c5d1fe50424a2630f759c65e7eb69d63427
|
7
|
+
data.tar.gz: db924a220d2c2084703ca2b10c0759b6b74a8d9f38833c5247781b69ad6d631290dbfd181119e677a0403229ec2e7e9cc749498f4486df18f89d79a0713f5b8e
|
data/.gitignore
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# vim
|
2
|
+
.*.sw[a-z]
|
3
|
+
*.un~
|
4
|
+
Session.vim
|
5
|
+
|
6
|
+
# mine
|
7
|
+
.idea
|
8
|
+
|
9
|
+
# OSX ignores
|
10
|
+
.DS_Store
|
11
|
+
.AppleDouble
|
12
|
+
.LSOverride
|
13
|
+
Icon
|
14
|
+
|
15
|
+
._*
|
16
|
+
.Spotlight-V100
|
17
|
+
.Trashes
|
18
|
+
.AppleDB
|
19
|
+
.AppleDesktop
|
20
|
+
Network Trash Folder
|
21
|
+
Temporary Items
|
22
|
+
.apdisk
|
23
|
+
|
24
|
+
# gem
|
25
|
+
/.bundle/
|
26
|
+
/.yardoc
|
27
|
+
/Gemfile.lock
|
28
|
+
/_yardoc/
|
29
|
+
/coverage/
|
30
|
+
/doc/
|
31
|
+
/pkg/
|
32
|
+
/spec/reports/
|
33
|
+
/tmp/
|
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
apnotic
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.3.0
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Roberto Ostinelli.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/ostinelli/apnotic.svg?branch=master)](https://travis-ci.org/ostinelli/apnotic)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/ostinelli/apnotic/badges/gpa.svg)](https://codeclimate.com/github/ostinelli/apnotic)
|
3
|
+
|
4
|
+
# Apnotic
|
5
|
+
|
6
|
+
Apnotic is a gem for sending Apple Push Notifications using the [HTTP-2 specifics](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW9).
|
7
|
+
|
8
|
+
|
9
|
+
## Why "Yet Another APN" gem?
|
10
|
+
If you have used the previous Apple Push Notification specifications you may have noticed that it was hard to know whether a Push Notification was successful or not. It is a common problem that has been reported multiple times. In addition, you had to run a separate Feedback service to retrieve the list of the device tokens that were no longer valid, and ensure to purge them from your systems.
|
11
|
+
|
12
|
+
All of this is solved by using the HTTP-2 APN specifications. Every Push Notification you make returns a response stating if the Push was successful or, if not, which problems were encountered. This includes the case when invalid device tokens are used, hence making it unnecessary to have a separate Feedback service.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
Just install the gem:
|
16
|
+
|
17
|
+
```
|
18
|
+
$ gem install apnotic
|
19
|
+
```
|
20
|
+
|
21
|
+
Or add it to your Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'apnotic'
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Standalone
|
30
|
+
```ruby
|
31
|
+
require 'apnotic'
|
32
|
+
|
33
|
+
# create a persistent connection
|
34
|
+
connection = Apnotic::Connection.new(cert_path: "apns_certificate.pem", cert_pass: "pass")
|
35
|
+
|
36
|
+
# create a notification for a specific device token
|
37
|
+
token = "6c267f26b173cd9595ae2f6702b1ab560371a60e7c8a9e27419bd0fa4a42e58f"
|
38
|
+
|
39
|
+
notification = Apnotic::Notification.new(token)
|
40
|
+
notification.alert = "Notification from Apnotic!"
|
41
|
+
|
42
|
+
# send (this is a blocking call)
|
43
|
+
response = connection.push(notification)
|
44
|
+
|
45
|
+
# read the response
|
46
|
+
response.ok? # => true
|
47
|
+
response.status # => '200'
|
48
|
+
response.headers # => {":status"=>"200", "apns-id"=>"6f2cd350-bfad-4af0-a8bc-0d501e9e1799"}
|
49
|
+
response.body # => ""
|
50
|
+
|
51
|
+
# close the connection
|
52
|
+
connection.close
|
53
|
+
```
|
54
|
+
|
55
|
+
### With Sidekiq / Rescue
|
56
|
+
A practical example of a Sidekiq / Rescue worker will probably have to:
|
57
|
+
|
58
|
+
* Use a pool of persistent connections.
|
59
|
+
* Send a push notification.
|
60
|
+
* Remove a device with an invalid token.
|
61
|
+
|
62
|
+
An example of a Sidekiq worker with such features follows.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
require 'apnotic'
|
66
|
+
|
67
|
+
class MyWorker
|
68
|
+
include Sidekiq::Worker
|
69
|
+
|
70
|
+
sidekiq_options queue: :push_notifications
|
71
|
+
|
72
|
+
APNOTIC_POOL = Apnotic::ConnectionPool.new({
|
73
|
+
cert_path: Rails.root.join("config", "certs", "apns_certificate.pem"),
|
74
|
+
cert_pass: "mypass"
|
75
|
+
}, size: 5)
|
76
|
+
|
77
|
+
def perform(token)
|
78
|
+
APNOTIC_POOL.with do |connection|
|
79
|
+
notification = Apnotic::Notification.new(token)
|
80
|
+
notification.alert = "Hello from Apnotic!"
|
81
|
+
|
82
|
+
response = connection.push(notification)
|
83
|
+
|
84
|
+
if response.status == '410' ||
|
85
|
+
(response.status == '400' && response.body['reason'] == 'BadDeviceToken')
|
86
|
+
Device.find_by(token: token).destroy
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
> The official [APNs Provider API documentation](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html) explains how to interpret the responses given by the APNS.
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
## Objects
|
98
|
+
|
99
|
+
### `Apnotic::Connection`
|
100
|
+
To create a new persistent connection:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
Apnotic::Connection.new(options)
|
104
|
+
```
|
105
|
+
|
106
|
+
| Option | Description
|
107
|
+
|-----|-----
|
108
|
+
| :cert_path | Required. The path to a valid APNS push certificate in .pem format (see "Convert your certificate" here below for instructions).
|
109
|
+
| :cert_pass | Optional. The certificate's password.
|
110
|
+
| :uri | Optional. Defaults to https://api.push.apple.com:443.
|
111
|
+
|
112
|
+
It is also possible to create a connection that points to the Apple Development servers by calling instead:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
Apnotic::Connection.development(options)
|
116
|
+
```
|
117
|
+
|
118
|
+
> The concepts of PRODUCTION and DEVELOPMENT are different from what they used to be in previous specifications. Anything built directly from XCode and loaded on your phone will have the app generate DEVELOPMENT tokens, while everything else (TestFlight, Apple Store, HockeyApp, ...) will be considered as PRODUCTION environment.
|
119
|
+
|
120
|
+
#### Methods
|
121
|
+
|
122
|
+
* **uri** → **`URI`**
|
123
|
+
Returns the URI of the APNS endpoint.
|
124
|
+
|
125
|
+
* **cert_path** → **`string`**
|
126
|
+
Returns the path to the certificate
|
127
|
+
|
128
|
+
* **push(notification, timeout=30)** → **`Apnotic::Response` or `nil`**
|
129
|
+
Sends a notification. Returns `nil` in case a timeout occurs.
|
130
|
+
|
131
|
+
|
132
|
+
### `Apnotic::ConnectionPool`
|
133
|
+
For your convenience, a wrapper around the [Connection Pool](https://github.com/mperham/connection_pool) gem is here for you. To create a new connection pool:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
Apnotic::ConnectionPool.new(connection_options, connection_pool_options)
|
137
|
+
```
|
138
|
+
|
139
|
+
For example:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
APNOTIC_POOL = Apnotic::ConnectionPool.new({
|
143
|
+
cert_path: "apns_certificate.pem"
|
144
|
+
}, size: 5)
|
145
|
+
```
|
146
|
+
|
147
|
+
### `Apnotic::Notification`
|
148
|
+
To create a notification for a specific device token:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
notification = Apnotic::Notification.new(token)
|
152
|
+
```
|
153
|
+
|
154
|
+
#### Methods
|
155
|
+
These are all Accessor attributes.
|
156
|
+
|
157
|
+
| Method | Documentation
|
158
|
+
|-----|-----
|
159
|
+
| `alert` | Refer to the official Apple documentation of [The Notification Payload](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html) for details.
|
160
|
+
| `badge` | "
|
161
|
+
| `sound` | "
|
162
|
+
| `content_available` | "
|
163
|
+
| `category` | "
|
164
|
+
| `custom_payload` | "
|
165
|
+
| `apns_id` | Refer to the [APNs Provider API](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html) for details. If you don't provide any, one will be generated for you.
|
166
|
+
| `expiration` | "
|
167
|
+
| `priority` | "
|
168
|
+
| `topic` | "
|
169
|
+
|
170
|
+
For example:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
notification = Apnotic::Notification.new(token)
|
174
|
+
notification.alert = "Notification from Apnotic!"
|
175
|
+
notification.badge = 2
|
176
|
+
notification.sound = "bells.wav"
|
177
|
+
notification.priority = 5
|
178
|
+
```
|
179
|
+
|
180
|
+
|
181
|
+
### `Apnotic::Response`
|
182
|
+
The response to a call to `connection.push`.
|
183
|
+
|
184
|
+
#### Methods
|
185
|
+
|
186
|
+
* **headers** → **`hash`**
|
187
|
+
Returns a Hash containing the Headers of the response.
|
188
|
+
|
189
|
+
* **status** → **`string`**
|
190
|
+
Returns the status code.
|
191
|
+
|
192
|
+
* **body** → **`hash` or `string`**
|
193
|
+
Returns the body of the response in Hash format if a valid JSON was returned, otherwise just the RAW body.
|
194
|
+
|
195
|
+
* **headers** → **`boolean`**
|
196
|
+
Returns if the push was successful.
|
197
|
+
|
198
|
+
|
199
|
+
## Converting Your Certificate
|
200
|
+
|
201
|
+
> These instructions come from another great gem, [apn_on_rails](https://github.com/PRX/apn_on_rails).
|
202
|
+
|
203
|
+
Once you have the certificate from Apple for your application, export your key and the apple certificate as p12 files. Here is a quick walkthrough on how to do this:
|
204
|
+
|
205
|
+
1. Click the disclosure arrow next to your certificate in Keychain Access and select the certificate and the key.
|
206
|
+
2. Right click and choose `Export 2 items…`.
|
207
|
+
3. Choose the p12 format from the drop down and name it `cert.p12`.
|
208
|
+
|
209
|
+
Now covert the p12 file to a pem file:
|
210
|
+
```
|
211
|
+
$ openssl pkcs12 -in cert.p12 -out apple_push_notification_production.pem -nodes -clcerts
|
212
|
+
```
|
213
|
+
|
214
|
+
## Contributing
|
215
|
+
So you want to contribute? That's great! Please follow the guidelines below. It will make it easier to get merged in.
|
216
|
+
|
217
|
+
Before implementing a new feature, please submit a ticket to discuss what you intend to do. Your feature might already be in the works, or an alternative implementation might have already been discussed.
|
218
|
+
|
219
|
+
Do not commit to master in your fork. Provide a clean branch without merge commits. Every pull request should have its own topic branch. In this way, every additional adjustments to the original pull request might be done easily, and squashed with `git rebase -i`. The updated branch will be visible in the same pull request, so there will be no need to open new pull requests when there are changes to be applied.
|
220
|
+
|
221
|
+
Ensure to include proper testing. To run tests you simply have to be in the project's root directory and run:
|
222
|
+
|
223
|
+
```bash
|
224
|
+
$ rake
|
225
|
+
```
|
data/Rakefile
ADDED
data/apnotic.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'apnotic/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "apnotic"
|
8
|
+
spec.version = Apnotic::VERSION
|
9
|
+
spec.licenses = ['MIT']
|
10
|
+
spec.authors = ["Roberto Ostinelli"]
|
11
|
+
spec.email = ["roberto@ostinelli.net"]
|
12
|
+
spec.summary = %q{Apnotic is an Apple Push Notification gem able to provide instant feedback.}
|
13
|
+
spec.homepage = "http://github.com/ostinelli/apnotic"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "http-2", "~> 0.8.1"
|
21
|
+
spec.add_dependency "connection_pool", "~> 2.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "apnotic"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/apnotic.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'apnotic/connection'
|
2
|
+
require 'apnotic/connection_pool'
|
3
|
+
require 'apnotic/notification'
|
4
|
+
require 'apnotic/response'
|
5
|
+
require 'apnotic/stream'
|
6
|
+
require 'apnotic/version'
|
7
|
+
|
8
|
+
module Apnotic
|
9
|
+
raise "Cannot require Apnotic, unsupported engine '#{RUBY_ENGINE}'" unless RUBY_ENGINE == "ruby"
|
10
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
require 'uri'
|
4
|
+
require 'http/2'
|
5
|
+
|
6
|
+
module Apnotic
|
7
|
+
|
8
|
+
APPLE_DEVELOPMENT_SERVER_URI = "https://api.development.push.apple.com:443"
|
9
|
+
APPLE_PRODUCTION_SERVER_URI = "https://api.push.apple.com:443"
|
10
|
+
|
11
|
+
class Connection
|
12
|
+
attr_reader :uri, :cert_path
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def development(options={})
|
16
|
+
options.merge!(uri: APPLE_DEVELOPMENT_SERVER_URI)
|
17
|
+
new(options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(options={})
|
22
|
+
@uri = URI.parse(options[:uri] || APPLE_PRODUCTION_SERVER_URI)
|
23
|
+
@cert_path = options[:cert_path]
|
24
|
+
@cert_pass = options[:cert_pass]
|
25
|
+
|
26
|
+
@pipe_r, @pipe_w = Socket.pair(:UNIX, :STREAM, 0)
|
27
|
+
@socket_thread = nil
|
28
|
+
@mutex = Mutex.new
|
29
|
+
|
30
|
+
raise "URI needs to be a HTTPS address" if uri.scheme != 'https'
|
31
|
+
raise "Cert file not found: #{@cert_path}" unless @cert_path && File.exist?(@cert_path)
|
32
|
+
end
|
33
|
+
|
34
|
+
def push(notification, options={})
|
35
|
+
open
|
36
|
+
|
37
|
+
new_stream.push(notification, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def close
|
41
|
+
exit_thread(@socket_thread)
|
42
|
+
|
43
|
+
@ssl_context = nil
|
44
|
+
@h2 = nil
|
45
|
+
@pipe_r = nil
|
46
|
+
@pipe_w = nil
|
47
|
+
@socket_thread = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def new_stream
|
53
|
+
Apnotic::Stream.new(uri: @uri, h2_stream: h2.new_stream)
|
54
|
+
end
|
55
|
+
|
56
|
+
def open
|
57
|
+
return if @socket_thread
|
58
|
+
|
59
|
+
@socket_thread = Thread.new do
|
60
|
+
|
61
|
+
socket = new_socket
|
62
|
+
|
63
|
+
begin
|
64
|
+
thread_loop(socket)
|
65
|
+
ensure
|
66
|
+
socket.close unless socket.closed?
|
67
|
+
@socket_thread = nil
|
68
|
+
end
|
69
|
+
end.tap { |t| t.abort_on_exception = true }
|
70
|
+
end
|
71
|
+
|
72
|
+
def thread_loop(socket)
|
73
|
+
loop do
|
74
|
+
|
75
|
+
available = socket.pending
|
76
|
+
if available > 0
|
77
|
+
data_received = socket.sysread(available)
|
78
|
+
h2 << data_received
|
79
|
+
break if socket.nil? || socket.closed?
|
80
|
+
end
|
81
|
+
|
82
|
+
ready = IO.select([socket, @pipe_r])
|
83
|
+
|
84
|
+
if ready[0].include?(@pipe_r)
|
85
|
+
data_to_send = @pipe_r.read_nonblock(1024)
|
86
|
+
socket.write(data_to_send)
|
87
|
+
end
|
88
|
+
|
89
|
+
if ready[0].include?(socket)
|
90
|
+
data_received = socket.read_nonblock(1024)
|
91
|
+
h2 << data_received
|
92
|
+
break if socket.nil? || socket.closed?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def new_socket
|
98
|
+
tcp = TCPSocket.new(@uri.host, @uri.port)
|
99
|
+
socket = OpenSSL::SSL::SSLSocket.new(tcp, ssl_context)
|
100
|
+
socket.sync_close = true
|
101
|
+
socket.hostname = @uri.hostname
|
102
|
+
|
103
|
+
socket.connect
|
104
|
+
|
105
|
+
socket
|
106
|
+
end
|
107
|
+
|
108
|
+
def ssl_context
|
109
|
+
@ssl_context ||= begin
|
110
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
111
|
+
certificate = File.read(@cert_path)
|
112
|
+
passphrase = @cert_pass
|
113
|
+
ctx.key = OpenSSL::PKey::RSA.new(certificate, passphrase)
|
114
|
+
ctx.cert = OpenSSL::X509::Certificate.new(certificate)
|
115
|
+
ctx
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def h2
|
120
|
+
@h2 ||= HTTP2::Client.new.tap do |h2|
|
121
|
+
h2.on(:frame) do |bytes|
|
122
|
+
@mutex.synchronize do
|
123
|
+
@pipe_w.write(bytes)
|
124
|
+
@pipe_w.flush
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def exit_thread(thread)
|
131
|
+
return unless thread && thread.alive?
|
132
|
+
thread.exit
|
133
|
+
thread.join
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Apnotic
|
5
|
+
|
6
|
+
class Notification
|
7
|
+
attr_reader :token
|
8
|
+
attr_accessor :alert, :badge, :sound, :content_available, :category, :custom_payload
|
9
|
+
attr_accessor :apns_id, :expiration, :priority, :topic
|
10
|
+
|
11
|
+
def initialize(token)
|
12
|
+
@token = token
|
13
|
+
@apns_id = SecureRandom.uuid
|
14
|
+
end
|
15
|
+
|
16
|
+
def body
|
17
|
+
JSON.dump(to_hash).force_encoding(Encoding::BINARY)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def to_hash
|
23
|
+
aps = { alert: alert }
|
24
|
+
aps.merge!(badge: badge) if badge
|
25
|
+
aps.merge!(sound: sound) if sound
|
26
|
+
aps.merge!(content_available: content_available) if content_available
|
27
|
+
aps.merge!(category: category) if category
|
28
|
+
|
29
|
+
n = { aps: aps }
|
30
|
+
n.merge!(custom_payload) if custom_payload
|
31
|
+
n
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Apnotic
|
4
|
+
|
5
|
+
class Response
|
6
|
+
attr_reader :headers
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
@headers = options[:headers]
|
10
|
+
@body = options[:body]
|
11
|
+
end
|
12
|
+
|
13
|
+
def status
|
14
|
+
@headers[':status'] if @headers
|
15
|
+
end
|
16
|
+
|
17
|
+
def ok?
|
18
|
+
status == '200'
|
19
|
+
end
|
20
|
+
|
21
|
+
def body
|
22
|
+
JSON.parse(@body) rescue @body
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Apnotic
|
2
|
+
|
3
|
+
class Stream
|
4
|
+
|
5
|
+
DEFAULT_TIMEOUT = 30
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
@h2_stream = options[:h2_stream]
|
9
|
+
@uri = options[:uri]
|
10
|
+
@headers = {}
|
11
|
+
@data = ''
|
12
|
+
@completed = false
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@cv = ConditionVariable.new
|
15
|
+
|
16
|
+
@h2_stream.on(:headers) do |hs|
|
17
|
+
hs.each { |k, v| @headers[k] = v }
|
18
|
+
end
|
19
|
+
|
20
|
+
@h2_stream.on(:data) { |d| @data << d }
|
21
|
+
@h2_stream.on(:close) do
|
22
|
+
@mutex.synchronize do
|
23
|
+
@completed = true
|
24
|
+
@cv.signal
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def push(notification, options={})
|
30
|
+
headers = build_headers_for notification
|
31
|
+
body = notification.body
|
32
|
+
|
33
|
+
@h2_stream.headers(headers, end_stream: false)
|
34
|
+
@h2_stream.data(body, end_stream: true)
|
35
|
+
|
36
|
+
respond(options)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def build_headers_for(notification)
|
42
|
+
headers = {
|
43
|
+
':scheme' => @uri.scheme,
|
44
|
+
':method' => 'POST',
|
45
|
+
':path' => "/3/device/#{notification.token}",
|
46
|
+
'host' => @uri.host,
|
47
|
+
'content-length' => notification.body.bytesize.to_s
|
48
|
+
}
|
49
|
+
headers.merge!('apns-id' => notification.apns_id) if notification.apns_id
|
50
|
+
headers.merge!('apns-expiration' => notification.expiration) if notification.expiration
|
51
|
+
headers.merge!('apns-priority' => notification.priority) if notification.priority
|
52
|
+
headers.merge!('apns-topic' => notification.topic) if notification.topic
|
53
|
+
headers
|
54
|
+
end
|
55
|
+
|
56
|
+
def respond(options={})
|
57
|
+
options[:timeout] ||= DEFAULT_TIMEOUT
|
58
|
+
|
59
|
+
@mutex.synchronize { @cv.wait(@mutex, options[:timeout]) }
|
60
|
+
|
61
|
+
if @completed
|
62
|
+
Apnotic::Response.new(
|
63
|
+
headers: @headers,
|
64
|
+
body: @data
|
65
|
+
)
|
66
|
+
else
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apnotic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Roberto Ostinelli
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: http-2
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.8.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.8.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: connection_pool
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- roberto@ostinelli.net
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".ruby-gemset"
|
93
|
+
- ".ruby-version"
|
94
|
+
- ".travis.yml"
|
95
|
+
- Gemfile
|
96
|
+
- LICENSE.md
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- apnotic.gemspec
|
100
|
+
- bin/console
|
101
|
+
- bin/setup
|
102
|
+
- lib/apnotic.rb
|
103
|
+
- lib/apnotic/connection.rb
|
104
|
+
- lib/apnotic/connection_pool.rb
|
105
|
+
- lib/apnotic/notification.rb
|
106
|
+
- lib/apnotic/response.rb
|
107
|
+
- lib/apnotic/stream.rb
|
108
|
+
- lib/apnotic/version.rb
|
109
|
+
homepage: http://github.com/ostinelli/apnotic
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 2.5.1
|
130
|
+
signing_key:
|
131
|
+
specification_version: 4
|
132
|
+
summary: Apnotic is an Apple Push Notification gem able to provide instant feedback.
|
133
|
+
test_files: []
|