bmo 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 39304f792a910d0a9f4e7e1e50067919aa28ec67
4
+ data.tar.gz: 2a478f2e6fc7e7b0e80fc87a492bf63a024e7338
5
+ SHA512:
6
+ metadata.gz: 2979469971ebf385203814428d3d7fe6e18a3bdae760f6994fef7c43eacb964bb160b00f41b179f4346540988fe036678a8e00b186f4847cbf98c99492048ee6
7
+ data.tar.gz: a41eedfd88a9f130fe2408ddc2c6894843792019badc9ed10df6fbd239b6860822cce989e97c1d6bd8975d2794be747f1e188f92d2133049143298104b5f2111
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+
2
+ tags
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format progress
3
+ --warnings
4
+ --order random
data/Gemfile ADDED
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in poleica.gemspec
5
+ gemspec
6
+
7
+ group :test do
8
+ gem 'rspec'
9
+ gem 'coveralls'
10
+ gem 'flay'
11
+ gem 'flog'
12
+ gem 'reek'
13
+ gem 'rubocop'
14
+ gem 'simplecov'
15
+ end
16
+
17
+ group :development do
18
+ gem 'jazz_hands'
19
+
20
+ # Guards
21
+ gem 'guard'
22
+ gem 'guard-bundler'
23
+ gem 'guard-rspec'
24
+ gem 'guard-rubocop'
25
+ gem 'guard-flay', github: 'pericles/guard-flay'
26
+ gem 'guard-reek', github: 'pericles/guard-reek'
27
+ gem 'guard-flog', github: 'antoinelyset/guard-flog'
28
+ end
29
+
30
+ group :darwin do
31
+ gem 'terminal-notifier-guard'
32
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,237 @@
1
+ GIT
2
+ remote: git://github.com/antoinelyset/guard-flog.git
3
+ revision: 40ea28590ae29349860901d3c9b8253b24c31588
4
+ specs:
5
+ guard-flog (0.0.1)
6
+ flog (~> 4.2.0)
7
+ guard (>= 1.8)
8
+
9
+ GIT
10
+ remote: git://github.com/pericles/guard-flay.git
11
+ revision: 75b9be79c12394626af0064bcec1075959c3cbdd
12
+ specs:
13
+ guard-flay (0.0.1)
14
+ flay (~> 2.4.0)
15
+ flay-haml (~> 0.0.3)
16
+ guard (>= 1.8)
17
+
18
+ GIT
19
+ remote: git://github.com/pericles/guard-reek.git
20
+ revision: febe7ac4f4c6f029a30a9213e92bc1ba44b4e2d4
21
+ specs:
22
+ guard-reek (0.0.1)
23
+ guard (>= 1.8)
24
+ reek (~> 1.3.2)
25
+
26
+ PATH
27
+ remote: .
28
+ specs:
29
+ bmo (0.8.0)
30
+ equalizer (~> 0.0.0)
31
+ faraday (~> 0.8.0)
32
+
33
+ GEM
34
+ remote: https://rubygems.org/
35
+ specs:
36
+ actionpack (4.0.2)
37
+ activesupport (= 4.0.2)
38
+ builder (~> 3.1.0)
39
+ erubis (~> 2.7.0)
40
+ rack (~> 1.5.2)
41
+ rack-test (~> 0.6.2)
42
+ activesupport (4.0.2)
43
+ i18n (~> 0.6, >= 0.6.4)
44
+ minitest (~> 4.2)
45
+ multi_json (~> 1.3)
46
+ thread_safe (~> 0.1)
47
+ tzinfo (~> 0.3.37)
48
+ ast (1.1.0)
49
+ atomic (1.1.14)
50
+ awesome_print (1.2.0)
51
+ binding_of_caller (0.7.2)
52
+ debug_inspector (>= 0.0.1)
53
+ builder (3.1.4)
54
+ celluloid (0.15.2)
55
+ timers (~> 1.1.0)
56
+ coderay (1.1.0)
57
+ columnize (0.3.6)
58
+ coolline (0.4.2)
59
+ coveralls (0.7.0)
60
+ multi_json (~> 1.3)
61
+ rest-client
62
+ simplecov (>= 0.7)
63
+ term-ansicolor
64
+ thor
65
+ debug_inspector (0.0.2)
66
+ debugger (1.6.3)
67
+ columnize (>= 0.3.1)
68
+ debugger-linecache (~> 1.2.0)
69
+ debugger-ruby_core_source (~> 1.2.4)
70
+ debugger-linecache (1.2.0)
71
+ debugger-ruby_core_source (1.2.4)
72
+ diff-lcs (1.2.5)
73
+ diffy (3.0.1)
74
+ docile (1.1.1)
75
+ equalizer (0.0.8)
76
+ erubis (2.7.0)
77
+ faraday (0.8.8)
78
+ multipart-post (~> 1.2.0)
79
+ ffi (1.9.3)
80
+ flay (2.4.0)
81
+ ruby_parser (~> 3.0)
82
+ sexp_processor (~> 4.0)
83
+ flay-haml (0.0.3)
84
+ flay (>= 1.2, < 3)
85
+ haml (>= 3, < 5)
86
+ flog (4.2.0)
87
+ ruby_parser (~> 3.1, > 3.1.0)
88
+ sexp_processor (~> 4.4)
89
+ formatador (0.2.4)
90
+ grit (2.5.0)
91
+ diff-lcs (~> 1.1)
92
+ mime-types (~> 1.15)
93
+ posix-spawn (~> 0.3.6)
94
+ guard (2.2.4)
95
+ formatador (>= 0.2.4)
96
+ listen (~> 2.1)
97
+ lumberjack (~> 1.0)
98
+ pry (>= 0.9.12)
99
+ thor (>= 0.18.1)
100
+ guard-bundler (2.0.0)
101
+ bundler (~> 1.0)
102
+ guard (~> 2.2)
103
+ guard-rspec (4.2.0)
104
+ guard (>= 2.1.1)
105
+ rspec (>= 2.14, < 4.0)
106
+ guard-rubocop (1.0.1)
107
+ guard (~> 2.0)
108
+ rubocop (~> 0.10)
109
+ haml (4.0.4)
110
+ tilt
111
+ hirb (0.7.1)
112
+ i18n (0.6.9)
113
+ jazz_hands (0.5.2)
114
+ awesome_print (~> 1.2)
115
+ coolline (>= 0.4.2)
116
+ hirb (~> 0.7.1)
117
+ pry (~> 0.9.12)
118
+ pry-debugger (~> 0.2.2)
119
+ pry-doc (~> 0.4.6)
120
+ pry-git (~> 0.2.3)
121
+ pry-rails (~> 0.3.2)
122
+ pry-remote (>= 0.1.7)
123
+ pry-stack_explorer (~> 0.4.9)
124
+ railties (>= 3.0, < 5.0)
125
+ listen (2.4.0)
126
+ celluloid (>= 0.15.2)
127
+ rb-fsevent (>= 0.9.3)
128
+ rb-inotify (>= 0.9)
129
+ lumberjack (1.0.4)
130
+ method_source (0.8.2)
131
+ mime-types (1.25.1)
132
+ minitest (4.7.5)
133
+ multi_json (1.8.2)
134
+ multipart-post (1.2.0)
135
+ parser (2.0.0)
136
+ ast (~> 1.1)
137
+ slop (~> 3.4, >= 3.4.5)
138
+ posix-spawn (0.3.8)
139
+ powerpack (0.0.9)
140
+ pry (0.9.12.4)
141
+ coderay (~> 1.0)
142
+ method_source (~> 0.8)
143
+ slop (~> 3.4)
144
+ pry-debugger (0.2.2)
145
+ debugger (~> 1.3)
146
+ pry (~> 0.9.10)
147
+ pry-doc (0.4.6)
148
+ pry (>= 0.9)
149
+ yard (>= 0.8)
150
+ pry-git (0.2.3)
151
+ diffy
152
+ grit
153
+ pry (>= 0.9.8)
154
+ pry-rails (0.3.2)
155
+ pry (>= 0.9.10)
156
+ pry-remote (0.1.7)
157
+ pry (~> 0.9)
158
+ slop (~> 3.0)
159
+ pry-stack_explorer (0.4.9.1)
160
+ binding_of_caller (>= 0.7)
161
+ pry (>= 0.9.11)
162
+ rack (1.5.2)
163
+ rack-test (0.6.2)
164
+ rack (>= 1.0)
165
+ railties (4.0.2)
166
+ actionpack (= 4.0.2)
167
+ activesupport (= 4.0.2)
168
+ rake (>= 0.8.7)
169
+ thor (>= 0.18.1, < 2.0)
170
+ rainbow (1.1.4)
171
+ rake (10.1.0)
172
+ rb-fsevent (0.9.3)
173
+ rb-inotify (0.9.2)
174
+ ffi (>= 0.5.0)
175
+ reek (1.3.4)
176
+ ruby2ruby (~> 2.0.2)
177
+ ruby_parser (~> 3.2)
178
+ sexp_processor
179
+ rest-client (1.6.7)
180
+ mime-types (>= 1.16)
181
+ rspec (2.14.1)
182
+ rspec-core (~> 2.14.0)
183
+ rspec-expectations (~> 2.14.0)
184
+ rspec-mocks (~> 2.14.0)
185
+ rspec-core (2.14.7)
186
+ rspec-expectations (2.14.4)
187
+ diff-lcs (>= 1.1.3, < 2.0)
188
+ rspec-mocks (2.14.4)
189
+ rubocop (0.15.0)
190
+ parser (~> 2.0)
191
+ powerpack (~> 0.0.6)
192
+ rainbow (>= 1.1.4)
193
+ ruby2ruby (2.0.6)
194
+ ruby_parser (~> 3.1)
195
+ sexp_processor (~> 4.0)
196
+ ruby_parser (3.2.2)
197
+ sexp_processor (~> 4.1)
198
+ sexp_processor (4.4.0)
199
+ simplecov (0.8.2)
200
+ docile (~> 1.1.0)
201
+ multi_json
202
+ simplecov-html (~> 0.8.0)
203
+ simplecov-html (0.8.0)
204
+ slop (3.4.7)
205
+ term-ansicolor (1.2.2)
206
+ tins (~> 0.8)
207
+ terminal-notifier-guard (1.5.3)
208
+ thor (0.18.1)
209
+ thread_safe (0.1.3)
210
+ atomic
211
+ tilt (2.0.0)
212
+ timers (1.1.0)
213
+ tins (0.13.1)
214
+ tzinfo (0.3.38)
215
+ yard (0.8.7.3)
216
+
217
+ PLATFORMS
218
+ ruby
219
+
220
+ DEPENDENCIES
221
+ bmo!
222
+ coveralls
223
+ flay
224
+ flog
225
+ guard
226
+ guard-bundler
227
+ guard-flay!
228
+ guard-flog!
229
+ guard-reek!
230
+ guard-rspec
231
+ guard-rubocop
232
+ jazz_hands
233
+ reek
234
+ rspec
235
+ rubocop
236
+ simplecov
237
+ terminal-notifier-guard
data/Guardfile ADDED
@@ -0,0 +1,27 @@
1
+ guard 'bundler' do
2
+ watch('Gemfile')
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard :rspec do
7
+ watch(%r{^spec/.+_spec\.rb$})
8
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
9
+ watch('spec/spec_helper.rb') { "spec" }
10
+ end
11
+
12
+ guard :rubocop do
13
+ watch(%r{.+\.rb$})
14
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
15
+ end
16
+
17
+ guard :flog do
18
+ watch(%r{^lib/(.+)\.rb$})
19
+ end
20
+
21
+ guard :reek do
22
+ watch(%r{^lib/(.+)\.rb$})
23
+ end
24
+
25
+ guard :flay do
26
+ watch(%r{^lib/(.+)\.rb$})
27
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Antoine Lyset
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ 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, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ <img src="https://raw.github.com/antoinelyset/bmo/master/bmo.png"
2
+ alt="BMO"
3
+ align="right"
4
+ width="150px"/>
5
+
6
+ <br>
7
+ <br>
8
+ <br>
9
+
10
+ # BMO (Beemo)
11
+
12
+ BMO is a gem to Push Notifications to iOS (via APNS) and Android (via GCM)
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ gem install bmo
18
+ ```
19
+
20
+ In Gemfile :
21
+
22
+ ```
23
+ gem 'bmo'
24
+ ```
25
+
26
+ ## APNS
27
+
28
+ ### Usage
29
+
30
+ ```ruby
31
+ token = "123456789" # The device token given to you by Apple
32
+ data = {aps: {alert: "Hello from BMO!"}}
33
+ BMO.send_ios_notification(token, data)
34
+ ```
35
+
36
+ The aps Hash content's is described here :
37
+
38
+ [https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html)
39
+
40
+ ### Configuration
41
+
42
+ Default Params :
43
+
44
+ ```ruby
45
+ BMO.configuration do |config|
46
+ config.apns.gateway_host = 'gateway.push.apple.com'
47
+ config.apns.gateway_port = 2195
48
+ config.apns.feedback_host = 'feedback.push.apple.com'
49
+ config.apns.feedback_port = 2196
50
+ config.apns.cert_path = nil
51
+ config.apns.cert_pass = nil
52
+ end
53
+ ```
54
+
55
+ If you set a cert_path option it will use a SSL encapsulation otherwise it will use Pure TCP.
56
+ This option is particularly useful if you use a stunnel.
57
+
58
+ ## GCM
59
+
60
+ ### Usage
61
+
62
+ ```ruby
63
+ token = "123456789" # The device token given to you by Apple
64
+ data = {message: "Hello from BMO!"}
65
+ BMO.send_android_notification(token, data)
66
+ ```
67
+
68
+ The data contennt is described here :
69
+
70
+ [http://developer.android.com/google/gcm/server.html](http://developer.android.com/google/gcm/server.html)
71
+
72
+ ### Configuration
73
+
74
+ Default Params :
75
+
76
+ ```ruby
77
+ BMO.configuration do |config|
78
+ config.gcm.gateway_url = 'https://android.googleapis.com/gcm/send'
79
+ config.gcm.api_key = nil
80
+ end
81
+ ```
82
+
83
+ You should set the api_key.
84
+
85
+ ## License
86
+
87
+ BMO is released under the [MIT
88
+ License](http://www.opensource.org/licenses/MIT)
data/bmo.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path('../lib/bmo/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = 'bmo'
7
+ gem.summary = 'Push notifications to iOS and Android devices'
8
+ gem.description = gem.summary
9
+ gem.authors = ['Antoine Lyset']
10
+ gem.email = ['antoinelyset@gmail.com']
11
+ gem.homepage = 'https://github.com/antoinelyset/bmo'
12
+ gem.require_paths = ['lib']
13
+ gem.version = BMO::VERSION.dup
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ gem.add_runtime_dependency('equalizer', '~> 0.0.0')
18
+ gem.add_runtime_dependency('faraday', '~> 0.8.0')
19
+ gem.license = 'MIT'
20
+ end
data/bmo.png ADDED
Binary file
@@ -0,0 +1,84 @@
1
+ # encoding: utf-8
2
+ module BMO
3
+ module APNS
4
+ # APNS Client Class
5
+ #
6
+ # @!attribute gateway_host
7
+ # @!attribute gateway_port
8
+ # @!attribute feedback_host
9
+ # @!attribute feedback_port
10
+ # @!attribute cert_path
11
+ # @!attribute cert_pass
12
+ class Client
13
+ attr_reader :cert_path, :cert_pass,
14
+ :gateway_host, :gateway_port,
15
+ :feedback_host, :feedback_port
16
+
17
+ # The constructor of the Client object
18
+ # it will only use a ssl connection if you pass a cert_path option
19
+ #
20
+ # @param gateway_host [String]
21
+ # @param gateway_port [Integer]
22
+ # @param feedback_host [String]
23
+ # @param feedback_port [Integer]
24
+ #
25
+ # @options options [String] :cert_pass
26
+ # @options options [String] :cert_path path to certificate file
27
+ #
28
+ def initialize(gateway_host, gateway_port,
29
+ feedback_host, feedback_port,
30
+ options = {})
31
+ @gateway_host = gateway_host
32
+ @gateway_port = gateway_port
33
+ @feedback_host = feedback_host
34
+ @feedback_port = feedback_port
35
+ @cert_path = options[:cert_path]
36
+ @cert_pass = options[:cert_pass]
37
+ end
38
+
39
+ # @param notification [Notification] the notification to send to Apple
40
+ #
41
+ def send_notification(notification)
42
+ connection = APNS::Connection.new(@gateway_host, @gateway_port,
43
+ @cert_path, @cert_pass)
44
+ connection.connect do |socket|
45
+ socket.write(notification.to_package)
46
+ end
47
+ end
48
+
49
+ # Get the Feedback from Apple
50
+ #
51
+ # @return <Array[FeedbackTuple]> A feedback tuple contains the time
52
+ # when Apple determined that the app no longer exists on the device,
53
+ # and a the token of device token
54
+ def get_feedback
55
+ connection = APNS::Connection.new(@feedback_host, @feedback_port,
56
+ @cert_path, @cert_pass)
57
+ connection.connect do |socket|
58
+ feedback_tuples = []
59
+ while (data = socket.read(38))
60
+ tuple = data.unpack('N1n1H*')
61
+ feedback_tuples << FeedbackTuple.new(tuple[0], tuple[2])
62
+ end
63
+ feedback_tuples
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Handle the Feedback Object
70
+ #
71
+ # @!attribute time [Time] Time when the device was notified
72
+ # but didn't have the app installed
73
+ # @!attribute token [String] The Token of the device notified wrongly
74
+ class FeedbackTuple
75
+ attr_reader :time, :token
76
+
77
+ def initialize(timestamp, token)
78
+ @time = Time.at(timestamp)
79
+ @token = token
80
+ end
81
+ end
82
+ end # class Client
83
+ end # module APNS
84
+ end # module BMO
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+ module BMO
3
+ module APNS
4
+ # Handle the connection state SSL or Pure TCP
5
+ class Connection
6
+ attr_reader :host, :port, :cert_path, :cert_pass
7
+
8
+ def initialize(host, port, cert_path = nil, cert_pass = nil)
9
+ @host = host
10
+ @port = port
11
+ @cert_path = cert_path
12
+ @cert_pass = cert_pass
13
+ end
14
+
15
+ # Create a connection to Apple. If a cert_path exists it uses SSL else
16
+ # a pure TCPSocket. It then yields the socket and handles the closing
17
+ #
18
+ # @return The yielded return
19
+ def connect(&block)
20
+ socket = cert_path ? ssl_socket : tcp_socket
21
+
22
+ yielded = yield socket
23
+
24
+ socket.close
25
+ yielded
26
+ end
27
+
28
+ private
29
+
30
+ # @return [TCPSocket]
31
+ def tcp_socket
32
+ TCPSocket.new(host, port)
33
+ end
34
+
35
+ # @return [SSLSocket] the SSLSocket connected and setted to sync_close
36
+ # to sync tcp_socket and ssl_socket closing.
37
+ def ssl_socket
38
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
39
+ ssl_socket.sync_close = true
40
+ ssl_socket.connect
41
+ ssl_socket
42
+ end
43
+
44
+ # @return [SSLContext] SSLContext setted with the certificate
45
+ #
46
+ def ssl_context
47
+ cert = File.read(cert_path)
48
+ context = OpenSSL::SSL::SSLContext.new
49
+ context.cert = OpenSSL::X509::Certificate.new(cert)
50
+ context.key = OpenSSL::PKey::RSA.new(cert, cert_pass)
51
+ context
52
+ end
53
+ end # class Connection
54
+ end # module APNS
55
+ end # module BMO
@@ -0,0 +1,92 @@
1
+ # encoding: utf-8
2
+ module BMO
3
+ module APNS
4
+ # The Notification Class handles all the packaging logic
5
+ class Notification
6
+ # Define ==
7
+ include Equalizer.new(:device_token, :payload)
8
+
9
+ attr_reader :device_token, :payload
10
+
11
+ def initialize(device_token, payload)
12
+ @device_token = DeviceToken.new(device_token)
13
+ @payload = Payload.new(payload)
14
+ end
15
+
16
+ def to_package
17
+ payload_packaged = payload.to_package
18
+ device_token_packaged = device_token.to_package
19
+ [
20
+ 0, 0,
21
+ device_token_packaged.size,
22
+ device_token_packaged,
23
+ 0,
24
+ payload_packaged.size,
25
+ payload_packaged
26
+ ].pack('ccca*cca*')
27
+ end
28
+
29
+ def validate!
30
+ device_token.validate!
31
+ payload.validate!
32
+ end
33
+
34
+ private
35
+
36
+ # The Payload contains the data sent to Apple
37
+ class Payload
38
+ class PayloadTooLarge < Exception; end
39
+
40
+ MAX_BYTE_SIZE = 256
41
+
42
+ # Define ==
43
+ include Equalizer.new(:data)
44
+
45
+ attr_reader :data
46
+
47
+ def initialize(data)
48
+ @data = Utils.coerce_to_symbols(data)
49
+ end
50
+
51
+ def to_package
52
+ data.to_json.bytes.to_a.pack('C*')
53
+ end
54
+
55
+ def validate!
56
+ if to_package.size > MAX_BYTE_SIZE
57
+ str = <<-EOS
58
+ Payload size should be less than #{Payload::MAX_BYTE_SIZE} bytes"
59
+ EOS
60
+ fail PayloadTooLarge, str
61
+ end
62
+ true
63
+ end
64
+ end # class Payload
65
+
66
+ # The DeviceToken is the id of a Device for an App
67
+ class DeviceToken
68
+ # Define ==
69
+ include Equalizer.new(:token)
70
+
71
+ class MalformedDeviceToken < Exception; end
72
+
73
+ attr_reader :token
74
+
75
+ def initialize(token)
76
+ @token = token
77
+ end
78
+
79
+ def to_package
80
+ [token].pack('H*')
81
+ end
82
+
83
+ def validate!
84
+ unless token =~ /^[a-z0-9]{64}$/i
85
+ fail(MalformedDeviceToken, 'Malformed Device Token')
86
+ end
87
+ true
88
+ end
89
+ end # class DeviceToken
90
+ end # class Notification
91
+ end # module APNS
92
+ end # module BMO
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+ # Main BMO module
3
+ module BMO
4
+ # Handles all the configuration per provider
5
+ class Configuration
6
+ attr_reader :apns, :gcm
7
+
8
+ def initialize
9
+ @apns = APNS.new
10
+ @gcm = GCM.new
11
+ end
12
+
13
+ # APNS Configuration
14
+ #
15
+ # @!attribute gateway_host
16
+ # @!attribute gateway_port
17
+ # @!attribute feedback_host
18
+ # @!attribute feedback_port
19
+ # @!attribute cert_path
20
+ # @!attribute cert_pass
21
+ class APNS
22
+ attr_accessor :cert_path, :cert_pass,
23
+ :gateway_host, :gateway_port,
24
+ :feedback_host, :feedback_port
25
+
26
+ def initialize
27
+ @gateway_host = 'gateway.push.apple.com'
28
+ @gateway_port = 2195
29
+ @feedback_host = 'feedback.push.apple.com'
30
+ @feedback_port = 2196
31
+ @cert_path = nil
32
+ @cert_pass = nil
33
+ end
34
+ end
35
+
36
+ # GCM Configuration
37
+ class GCM
38
+ attr_accessor :gateway_url, :api_key
39
+ def initialize
40
+ @gateway_url = 'https://android.googleapis.com/gcm/send'
41
+ @api_key = nil
42
+ end
43
+ end
44
+ end # class Configuration
45
+
46
+ module_function
47
+
48
+ def configure
49
+ yield(configuration) if block_given?
50
+ configuration
51
+ end
52
+
53
+ def configuration
54
+ @configuration ||= Configuration.new
55
+ end
56
+
57
+ # Needed because we can't use module_function and attr_writer
58
+ # rubocop:disable TrivialAccessors
59
+ def configuration=(configuration)
60
+ @configuration = configuration
61
+ end
62
+ # rubocop:enable
63
+
64
+ def reset_configuration
65
+ @configuration = Configuration.new
66
+ end
67
+ end # BMO
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ module BMO
3
+ module GCM
4
+ # GCM Client Class
5
+ class Client
6
+ attr_reader :gateway_url, :api_key
7
+
8
+ def initialize(gateway_url, api_key)
9
+ @gateway_url = gateway_url
10
+ @api_key = api_key
11
+ end
12
+
13
+ # @param notification [Notification] the notification to send to Google
14
+ #
15
+ def send_notification(notification)
16
+ connection = GCM::Connection.new(gateway_url, api_key)
17
+ connection.connect do |request|
18
+ request.body = notification.to_package
19
+ end
20
+ end
21
+ end # class Client
22
+ end # module APNS
23
+ end # module BMO
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ module BMO
3
+ module GCM
4
+ # Handle the connection state SSL or Pure TCP
5
+ class Connection
6
+ def initialize(gateway_url, api_key)
7
+ @gateway_url = gateway_url
8
+ @faraday_connection = Faraday::Connection.new(gateway_url)
9
+ @api_key = api_key
10
+ end
11
+
12
+ def connect(&block)
13
+ faraday_connection.post(gateway_url) do |request|
14
+ request.headers.merge!(content_type: 'application/json',
15
+ authorization: "key=#{api_key}")
16
+ yield request
17
+ end
18
+ end
19
+
20
+ protected
21
+
22
+ attr_reader :faraday_connection, :gateway_url, :api_key
23
+ end # class Connection
24
+ end # module GCM
25
+ end # module BMO
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ module BMO
3
+ module GCM
4
+ # The Notification Class handles all the packaging logic
5
+ class Notification
6
+ # Define ==
7
+ include Equalizer.new(:device_token, :payload)
8
+
9
+ attr_reader :device_token, :payload
10
+
11
+ def initialize(device_token, payload)
12
+ @device_token = device_token
13
+ @payload = Payload.new(payload)
14
+ end
15
+
16
+ def to_package
17
+ {
18
+ registration_ids: [device_token],
19
+ data: payload.to_package
20
+ }.to_json
21
+ end
22
+
23
+ def validate!
24
+ payload.validate!
25
+ end
26
+
27
+ private
28
+
29
+ # The Payload contains the data sent to Apple
30
+ class Payload
31
+ # Error Raised when the payload packaged > MAX_BYTE_SIZE
32
+ class PayloadTooLarge < Exception; end
33
+
34
+ MAX_BYTE_SIZE = 4096
35
+
36
+ # Define ==
37
+ include Equalizer.new(:data)
38
+
39
+ attr_reader :data
40
+
41
+ def initialize(data)
42
+ @data = Utils.coerce_to_symbols(data)
43
+ end
44
+
45
+ def to_package
46
+ data
47
+ end
48
+
49
+ def validate!
50
+ if to_package.size > MAX_BYTE_SIZE
51
+ str = <<-EOS
52
+ Payload size should be less than #{Payload::MAX_BYTE_SIZE} bytes"
53
+ EOS
54
+ fail PayloadTooLarge, str
55
+ end
56
+ true
57
+ end
58
+ end # class Payload
59
+ end # class Notification
60
+ end # module APNS
61
+ end # module BMO
data/lib/bmo/utils.rb ADDED
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+ module BMO
3
+ # Utility module
4
+ module Utils
5
+ module_function
6
+
7
+ # Coerce string hash keys to symbols
8
+ def coerce_to_symbols(hash)
9
+ hash_symbolized = {}
10
+ hash.each_pair do |key, value|
11
+ key = key.to_sym if key.respond_to?(:to_sym)
12
+ hash_symbolized[key] = value
13
+ end
14
+ hash_symbolized
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+ # Main BMO module, version get right here
3
+ module BMO
4
+ VERSION = '0.8.0'.freeze
5
+ end
data/lib/bmo.rb ADDED
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8
2
+
3
+ require 'socket'
4
+ require 'openssl'
5
+ require 'json'
6
+ require 'equalizer'
7
+ require 'faraday'
8
+
9
+ require 'bmo/version'
10
+ require 'bmo/configuration'
11
+ require 'bmo/utils'
12
+
13
+ require 'bmo/apns/notification'
14
+ require 'bmo/apns/connection'
15
+ require 'bmo/apns/client'
16
+
17
+ require 'bmo/gcm/notification'
18
+ require 'bmo/gcm/connection'
19
+ require 'bmo/gcm/client'
20
+
21
+ # Main BMO namespace
22
+ module BMO
23
+ # All the methods will be Class Methods
24
+ module_function
25
+
26
+ # Send ios notification with the configuration of BMO
27
+ # (see #BMO::Configuration)
28
+ #
29
+ # @param device_token [String]
30
+ # @param data [Hash] The data you want to send
31
+ #
32
+ # @return The Socket#write return
33
+ #
34
+ # @see https://developer.apple.com/library/ios/documentation/
35
+ # NetworkingInternet/Conceptual/RemoteNotificationsPG/
36
+ # Chapters/ApplePushService.html
37
+ def send_ios_notification(device_token, data)
38
+ notification = APNS::Notification.new(device_token, data)
39
+ apns_client.send_notification(notification)
40
+ end
41
+
42
+ # Get the iOS Feedback tuples
43
+ #
44
+ # @return [Array<FeedbackTuple>] Feedback Object containing
45
+ # a time and a token
46
+ def get_ios_feedback
47
+ apns_client.get_feedback
48
+ end
49
+
50
+ # Send android notification with the configuration of BMO
51
+ # (see #BMO::Configuration)
52
+ #
53
+ # @param device_token [String]
54
+ # @param data [Hash] The data you want to send
55
+ #
56
+ # @return [Faraday::Response] The HTTP Response
57
+ #
58
+ # @see http://developer.android.com/google/gcm/server.html]
59
+ def send_android_notification(device_token, data)
60
+ notification = GCM::Notification.new(device_token, data)
61
+ android_client.send_notification(notification)
62
+ end
63
+
64
+ private
65
+
66
+ def apns_client
67
+ conf = BMO.configuration.apns
68
+ APNS::Client.new(conf.gateway_host,
69
+ conf.gateway_port,
70
+ conf.feedback_host,
71
+ conf.feedback_port,
72
+ cert_path: conf.cert_path,
73
+ cert_pass: conf.cert_pass)
74
+ end
75
+
76
+ def gcm_client
77
+ conf = BMO.configuration.gcm
78
+ GCM::Client.new(conf.gateway_url,
79
+ conf.api_key)
80
+ end
81
+ end
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '../../../spec_helper'
4
+
5
+ describe BMO::APNS::Notification::DeviceToken do
6
+ describe '#token' do
7
+ it 'returns the token' do
8
+ device_token = described_class.new('abc')
9
+ expect(device_token.token).to eq('abc')
10
+ end
11
+ end
12
+
13
+ describe '#==' do
14
+ it 'returns true for equal device token' do
15
+ device_token = described_class.new('abc')
16
+ device_token_bis = described_class.new('abc')
17
+ expect(device_token == device_token_bis).to be_true
18
+ end
19
+
20
+ it 'returns true for equal device token' do
21
+ device_token = described_class.new('abc')
22
+ device_token_bis = described_class.new('abc')
23
+ expect(device_token).to eq(device_token_bis)
24
+ end
25
+
26
+ it 'returns false for equal device token' do
27
+ device_token = described_class.new('abc')
28
+ device_token_bis = described_class.new('def')
29
+ expect(device_token).to_not eq(device_token_bis)
30
+ end
31
+
32
+ it 'returns true for equal device token' do
33
+ device_token = described_class.new('abc')
34
+ device_token_bis = described_class.new('def')
35
+ expect(device_token == device_token_bis).to be_false
36
+ end
37
+ end
38
+
39
+ describe '#validate!' do
40
+ it 'returns true if the token is 64 chars' do
41
+ device_token = described_class.new('a' * 64)
42
+ expect(device_token.validate!).to be_true
43
+ end
44
+
45
+ it 'returns false if the token is not 64 chars' do
46
+ device_token = described_class.new('a' * 63)
47
+ expect { device_token.validate! }.to raise_error
48
+ BMO::APNS::Notification::DeviceToken::MalformedDeviceToken
49
+ end
50
+
51
+ it 'returns false if the token contains a special char' do
52
+ device_token = described_class.new(('a' * 63) + '"')
53
+ expect { device_token.validate! }.to raise_error
54
+ BMO::APNS::Notification::DeviceToken::MalformedDeviceToken
55
+ end
56
+ end
57
+
58
+ describe '#to_package' do
59
+ it 'returns the packaged token' do
60
+ device_token = described_class.new('0' * 64)
61
+ expect(device_token.to_package).to eq("\x00" * 32)
62
+ end
63
+ end
64
+ end
65
+
66
+ describe BMO::APNS::Notification::Payload do
67
+ it 'coerce hash keys to symbols' do
68
+ payload = described_class.new('Finn' => 'The Human')
69
+ expect(payload.data).to eq(Finn: 'The Human')
70
+ end
71
+
72
+ it "doesn't coerce incompatible types" do
73
+ payload = described_class.new(1 => 'For Money')
74
+ expect(payload.data).to eq(1 => 'For Money')
75
+ end
76
+
77
+ it 'returns true for equality between coerced hash and symbolized hash ' do
78
+ payload = described_class.new('Jake' => 'The Dog')
79
+ expect(payload).to eq(described_class.new(Jake: 'The Dog'))
80
+ end
81
+ end
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ require 'bmo'
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bmo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Antoine Lyset
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: equalizer
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 0.8.0
41
+ description: Push notifications to iOS and Android devices
42
+ email:
43
+ - antoinelyset@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - .gitignore
49
+ - .rspec
50
+ - Gemfile
51
+ - Gemfile.lock
52
+ - Guardfile
53
+ - LICENSE
54
+ - README.md
55
+ - bmo.gemspec
56
+ - bmo.png
57
+ - lib/bmo.rb
58
+ - lib/bmo/apns/client.rb
59
+ - lib/bmo/apns/connection.rb
60
+ - lib/bmo/apns/notification.rb
61
+ - lib/bmo/configuration.rb
62
+ - lib/bmo/gcm/client.rb
63
+ - lib/bmo/gcm/connection.rb
64
+ - lib/bmo/gcm/notification.rb
65
+ - lib/bmo/utils.rb
66
+ - lib/bmo/version.rb
67
+ - spec/bmo/apns/notification_spec.rb
68
+ - spec/spec_helper.rb
69
+ homepage: https://github.com/antoinelyset/bmo
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.1.11
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Push notifications to iOS and Android devices
93
+ test_files:
94
+ - spec/bmo/apns/notification_spec.rb
95
+ - spec/spec_helper.rb
96
+ has_rdoc: