gravel 0.1.0.alpha1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e0af416f5d3e8c7a7048a6b56a57ab951b3d701f
4
+ data.tar.gz: 19462716bc99702d0fdc51365ae7df20caac0657
5
+ SHA512:
6
+ metadata.gz: b61e235460ef57ed8651ba8848609f0b4604adfe23d31067b25393a8ad2b5be8fd1ca099241152995932950178e485d0c07bf3eb25235b9619bac522e592c2b4
7
+ data.tar.gz: fae035c45f431282047db1a5286c8e9e68de1884faaddd130559051704615ccfadcfb86993d975863c957fa17b5a8ddc97d8c0592aabdd153dadc1a8b1f66e5f
@@ -0,0 +1,45 @@
1
+ # Packages #
2
+ ############
3
+ *.7z
4
+ *.dmg
5
+ *.gz
6
+ *.iso
7
+ *.jar
8
+ *.rar
9
+ *.tar
10
+ *.zip
11
+
12
+ # Logs #
13
+ ########
14
+ *.log
15
+
16
+ # Databases #
17
+ #############
18
+ *.sql
19
+ *.sqlite
20
+
21
+ # OS Files #
22
+ ############
23
+ .DS_Store
24
+ .Trashes
25
+ ehthumbs.db
26
+ Icon?
27
+ Thumbs.db
28
+
29
+ # Vagrant #
30
+ ###########
31
+ .vagrant
32
+
33
+ # Ruby Files #
34
+ ##############
35
+ /.bundle/
36
+ /.yardoc
37
+ /Gemfile.lock
38
+ /_yardoc/
39
+ /coverage/
40
+ /doc/
41
+ /pkg/
42
+ /spec/reports/
43
+ /tmp/
44
+ /vendor/bundle/
45
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format doc
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,116 @@
1
+ # Gravel
2
+
3
+ Unified Push Notifications
4
+
5
+ ## Installation
6
+
7
+ You can install **Gravel** using the following command:
8
+
9
+ $ gem install gravel
10
+
11
+ ## Usage
12
+
13
+ Currently, Gravel only supports APNS (but FCM support is on the list).
14
+
15
+ ### APNS
16
+
17
+ Gravel uses the new APNS token based authentication so if you don't have an
18
+ APNS token, you'll need to generate one using the Apple Developer portal.
19
+
20
+ You'll also need the ID for that key, your team's ID and the bundle identifier
21
+ of your application (which should be passed as the 'topic' value - see below).
22
+
23
+ The APNS class provides the connection to APNS, you can create an instance
24
+ like this:
25
+
26
+ ```ruby
27
+ apns = Gravel::APNS.new(
28
+ key: Gravel::APNS.key_from_file('/path/to/APNsAuthKey_XXXXXXXXXX.p8'),
29
+ key_id: 'XXXXXXXXXX',
30
+ team_id: 'XXXXXXXXXX',
31
+ topic: 'com.example.app'
32
+ )
33
+ ```
34
+
35
+ There are also a few other parameters you can specify, you can check the
36
+ documentation for the full list but a couple that you might need are:
37
+
38
+ ##### :environment
39
+
40
+ This can be set to either ```:production``` or ```:development```.
41
+
42
+ The default value is ```:development```.
43
+
44
+ ##### :concurrency
45
+
46
+ This tells Gravel how many connections it should open to APNS.
47
+
48
+ One notification can be sent at a time through a connection, so opening multiple connections will improve the speed at which you can crunch through a notification task.
49
+
50
+ The default value is ```1```.
51
+
52
+ ---
53
+
54
+ Next, you'll want to send a notification. You can create a notification like this:
55
+
56
+ ```ruby
57
+ notification = Gravel::APNS::Notification.new
58
+ notification.title = 'Hello, World!'
59
+ notification.body = 'How are you today?'
60
+ notification.sound = :default
61
+ notification.device_token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
62
+ ```
63
+
64
+ Finally, you can send the notification:
65
+
66
+ ```ruby
67
+ apns.send(notification) do |success, response|
68
+ puts "Success: #{success}, Response: #{response}"
69
+ end
70
+ ```
71
+
72
+ The requests are all asynchronous, so if you need to wait for them to finish
73
+ you can call the ```wait``` method:
74
+
75
+ ```ruby
76
+ apns.wait
77
+ ```
78
+
79
+ This will block the thread until all requests have completed.
80
+
81
+ ---
82
+
83
+ If you're sending notifications to lots of devices, there's a helper method
84
+ that allows you to quickly generate the same notification for each device token:
85
+
86
+ ```ruby
87
+ tokens = [
88
+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
89
+ 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
90
+ ]
91
+
92
+ notifications = notification.for_device_tokens(*tokens)
93
+ ```
94
+
95
+ Then you can just loop over each notification and deliver it:
96
+
97
+ ```ruby
98
+ notifications.each do |notification|
99
+ apns.send(notification) do |success, result|
100
+ puts "Success: #{success}, Response: #{response}"
101
+ end
102
+ end
103
+ ```
104
+
105
+ ---
106
+
107
+ You should keep the APNS instance in memory for as long a possible, this
108
+ keeps the connection(s) to APNS open.
109
+
110
+ See [Communicating with APNs - Best Practices for Managing Connections](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW8) for more info on this.
111
+
112
+ ## Development
113
+
114
+ After checking out the repo, run `rake spec` to run the tests.
115
+
116
+ To install this gem onto your local machine, run `bundle exec rake install`.
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'gravel'
5
+ require 'irb'
6
+
7
+ IRB.start
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gravel/constants'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'gravel'
8
+ spec.version = Gravel::VERSION
9
+ spec.authors = ['Nialto Services']
10
+ spec.email = ['support@nialtoservices.co.uk']
11
+
12
+ spec.summary = %q{Unified Push Notifications}
13
+ spec.homepage = 'https://github.com/nialtoservices/gravel'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.metadata['yard.run'] = 'yri'
21
+
22
+ spec.add_dependency 'jwt', '~> 1.5'
23
+ spec.add_dependency 'net-http2', '~> 0.14.1'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.11'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.0'
28
+ spec.add_development_dependency 'yard', '~> 0.9.8'
29
+ end
@@ -0,0 +1,16 @@
1
+ require 'json'
2
+ require 'jwt'
3
+ require 'net-http2'
4
+ require 'openssl'
5
+ require 'securerandom'
6
+ require 'uri'
7
+
8
+ require 'gravel/apns'
9
+ require 'gravel/apns/auto_token'
10
+ require 'gravel/apns/notification'
11
+ require 'gravel/apns/notification/localization'
12
+
13
+ # Gravel Unified Push Notifications.
14
+ #
15
+ module Gravel
16
+ end
@@ -0,0 +1,224 @@
1
+ module Gravel
2
+ # Manages the connection to APNS.
3
+ # You should keep this instance somewhere instead of recreating it every
4
+ # time you need to send notifications.
5
+ #
6
+ class APNS
7
+ # The production APNS server URL.
8
+ #
9
+ PRODUCTION_URL = 'api.push.apple.com'
10
+
11
+ # The development APNS server URL.
12
+ #
13
+ DEVELOPMENT_URL = 'api.development.push.apple.com'
14
+
15
+ # The default APNS port.
16
+ #
17
+ DEFAULT_PORT = 443
18
+
19
+ # The alternative APNS port.
20
+ #
21
+ ALTERNATIVE_PORT = 2197
22
+
23
+ class << self
24
+ # Attempt to load a key from the specified path.
25
+ #
26
+ # @param path [String] The path to the key file.
27
+ # @return [OpenSSL::PKey::EC] The key.
28
+ #
29
+ def key_from_file(path)
30
+ unless File.file?(path)
31
+ raise "A key could not be loaded from: #{path}"
32
+ end
33
+
34
+ OpenSSL::PKey::EC.new(File.read(path))
35
+ end
36
+ end
37
+
38
+ # The topic to pass to APNS.
39
+ # This is usually the bundle identifier of the application you're sending
40
+ # push notifications to.
41
+ #
42
+ # @return [String] The APNS topic.
43
+ #
44
+ attr_reader :topic
45
+
46
+ # Create a new APNS instance.
47
+ #
48
+ # @param alternative_port [Boolean] (optional) Should we use the default (443) or the alternative (2197) port?
49
+ # @param concurrency [Integer] (optional) How many connections to APNS should we open?
50
+ # @param environment [Symbol] (optional) :production or :development
51
+ # @param key [OpenSSL::PKey::EC] An elliptic curve key (APNS authentication).
52
+ # @param key_id [String] The key's identifier.
53
+ # @param team_id [String] The team's identifier.
54
+ # @param topic [String] The topic to pass to APNS.
55
+ # @return [Gravel::APNS] An APNS instance.
56
+ #
57
+ def initialize(options = {})
58
+ options = {
59
+ alternative_port: false,
60
+ concurrency: 1,
61
+ environment: :development
62
+ }.merge(options)
63
+
64
+ unless options[:topic].is_a?(String)
65
+ raise 'The APNS topic is required.'
66
+ end
67
+
68
+ unless [true, false].include?(options[:alternative_port])
69
+ raise 'The alternative port should be a boolean value.'
70
+ end
71
+
72
+ unless options[:concurrency].is_a?(Integer) && options[:concurrency] > 0
73
+ raise 'Concurrency should be specified as an Integer greater than zero.'
74
+ end
75
+
76
+ unless [:development, :production].include?(options[:environment])
77
+ raise 'The environment should be either :production or :development.'
78
+ end
79
+
80
+ host = case options[:environment]
81
+ when :development
82
+ DEVELOPMENT_URL
83
+ when :production
84
+ PRODUCTION_URL
85
+ end
86
+
87
+ port = options[:alternative_port] ? ALTERNATIVE_PORT : DEFAULT_PORT
88
+
89
+ @auto_token = Gravel::APNS::AutoToken.new(options[:team_id], options[:key_id], options[:key])
90
+ @queue = Queue.new
91
+ @topic = options[:topic]
92
+ @url = "https://#{host}:#{port}"
93
+
94
+ @workers = options[:concurrency].times.map do
95
+ client = NetHttp2::Client.new(@url)
96
+
97
+ thread = Thread.new(client) do |client|
98
+ Thread.current.abort_on_exception = true
99
+
100
+ loop do
101
+ Thread.current[:processing] = false
102
+
103
+ notification, block = @queue.pop
104
+
105
+ Thread.current[:processing] = true
106
+
107
+ process_notification(notification, client, &block)
108
+ end
109
+ end
110
+
111
+ { client: client, thread: thread }
112
+ end
113
+ end
114
+
115
+ # The identifier for the developer's team.
116
+ #
117
+ # @return [String] The team's identifier.
118
+ #
119
+ def team_id
120
+ @auto_token.team_id
121
+ end
122
+
123
+ # The identifier for the APNS key.
124
+ #
125
+ # @return [String] The key's identifier.
126
+ #
127
+ def key_id
128
+ @auto_token.key_id
129
+ end
130
+
131
+ # Push a notification onto the send queue.
132
+ #
133
+ # @param notification [Gravel::APNS::Notification] The notification to send.
134
+ # @param block [Proc] The block to call when the request has completed.
135
+ # @return [Boolean] Whether or not the notification was sent.
136
+ #
137
+ def send(notification, &block)
138
+ if @workers.nil? || @workers.empty?
139
+ raise "There aren't any workers to process this notification!"
140
+ end
141
+
142
+ @queue.push([notification, block])
143
+
144
+ nil
145
+ end
146
+
147
+ # Wait for all threads to finish processing.
148
+ #
149
+ def wait
150
+ threads = @workers.map { |w| w[:thread] }
151
+
152
+ until @queue.empty? && threads.all? { |t| t[:processing] == false }
153
+ # Waiting for the threads to finish ...
154
+ end
155
+ end
156
+
157
+ # Close all clients and terminate all workers.
158
+ #
159
+ def close
160
+ @queue.clear
161
+
162
+ if @workers.is_a?(Array)
163
+ @workers.each do |payload|
164
+ payload[:thread].kill
165
+ payload[:client].close
166
+ end
167
+
168
+ @workers = nil
169
+ end
170
+
171
+ nil
172
+ end
173
+
174
+ private
175
+ # Process a notification (in a worker).
176
+ #
177
+ # @param notification [Gravel::APNS::Notification] The notification to process.
178
+ # @param client [NetHttp2::Client] The client to use.
179
+ # @param block [Proc] The block to call on completion.
180
+ #
181
+ def process_notification(notification, client, &block)
182
+ unless notification.is_a?(Gravel::APNS::Notification)
183
+ raise 'The notification must be an instance of Gravel::APNS::Notification.'
184
+ end
185
+
186
+ unless client.is_a?(NetHttp2::Client)
187
+ raise 'The client must be an instance of NetHttp2::Client.'
188
+ end
189
+
190
+ unless notification.device_token
191
+ block.call(false) if block_given?
192
+ return
193
+ end
194
+
195
+ path = "/3/device/#{notification.device_token}"
196
+
197
+ headers = Hash.new
198
+ headers['authorization'] = @auto_token.bearer_token
199
+ headers['apns-topic'] = @topic
200
+
201
+ if notification.uuid
202
+ headers['apns-id'] = notification.uuid
203
+ end
204
+
205
+ if notification.collapse_id
206
+ headers['apns-collapse-id'] = notification.collapse_id
207
+ end
208
+
209
+ if notification.priority
210
+ headers['apns-priority'] = notification.priority.to_s
211
+ end
212
+
213
+ if notification.expiration && notification.expiration.is_a?(Time)
214
+ headers['apns-expiration'] = notification.expiration.utc.to_i.to_s
215
+ end
216
+
217
+ body = notification.payload.to_json
218
+
219
+ response = client.call(:post, path, headers: headers, body: body)
220
+
221
+ block.call(response.ok?, response) if block_given?
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,107 @@
1
+ module Gravel
2
+ class APNS
3
+ # Used internally to generate JWT tokens for APNS.
4
+ #
5
+ class AutoToken
6
+ # The identifier for the developer's team.
7
+ #
8
+ # @return [String] The team identifier.
9
+ #
10
+ attr_reader :team_id
11
+
12
+ # The identifier for the APNS key.
13
+ #
14
+ # @return [String] The key identifier.
15
+ #
16
+ attr_reader :key_id
17
+
18
+ # Create a new AutoToken instance.
19
+ #
20
+ # @param team_id [String] The team identifier.
21
+ # @param key_id [String] The key identifier.
22
+ # @param key [OpenSSL::PKey::EC] The private key.
23
+ # @return [Gravel::APNS::AutoToken] An AutoToken instance.
24
+ #
25
+ def initialize(team_id, key_id, key)
26
+ unless team_id.is_a?(String)
27
+ raise 'The team identifier must be a string.'
28
+ end
29
+
30
+ unless key_id.is_a?(String)
31
+ raise 'The key identifier must be a string.'
32
+ end
33
+
34
+ unless key_id.length == 10
35
+ raise 'The key identifier does not appear to be valid.'
36
+ end
37
+
38
+ unless key.is_a?(OpenSSL::PKey::EC)
39
+ raise 'The key must be an elliptic curve key.'
40
+ end
41
+
42
+ unless key.private?
43
+ raise 'The key must contain a private key.'
44
+ end
45
+
46
+ @key = key
47
+ @key_id = key_id
48
+ @team_id = team_id
49
+ @token_generation_mutex = Mutex.new
50
+ end
51
+
52
+ # Get the next token to use.
53
+ #
54
+ # @return [String] The next token.
55
+ #
56
+ def token
57
+ if require_token_generation?
58
+ @token_generation_mutex.synchronize do
59
+ # Double check if we need to regenerate the token.
60
+ # This could happen if two threads try to concurrently access
61
+ # the token after it has expired (or before initial generation).
62
+ if require_token_generation?
63
+ @last_generated = time
64
+ @token = generate_token
65
+ end
66
+ end
67
+ end
68
+
69
+ @token
70
+ end
71
+
72
+ # Generate a bearer token.
73
+ #
74
+ # @return [String] A bearer token.
75
+ #
76
+ def bearer_token
77
+ 'Bearer ' + token
78
+ end
79
+
80
+ private
81
+ # Check if we need to generate a new token.
82
+ #
83
+ def require_token_generation?
84
+ @token == nil || @last_generated == nil || @last_generated < (time - 3540)
85
+ end
86
+
87
+ # Generate a new token.
88
+ #
89
+ # @return [String] The token.
90
+ #
91
+ def generate_token
92
+ headers = { 'kid' => @key_id }
93
+ claims = { 'iat' => Time.now.utc.to_i, 'iss' => @team_id }
94
+
95
+ JWT.encode(claims, @key, 'ES256', headers)
96
+ end
97
+
98
+ # Get the current time in seconds (Epoch).
99
+ #
100
+ # @return [Integer] The current time in seconds.
101
+ #
102
+ def time
103
+ Time.now.utc.to_i
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,203 @@
1
+ module Gravel
2
+ class APNS
3
+ # A notification for an Apple device (using APNS).
4
+ #
5
+ class Notification
6
+ PRIORITY_IMMEDIATE = 10
7
+ PRIORITY_ECO = 5
8
+
9
+ # The title of the notification.
10
+ # You can provide a localization on this value.
11
+ #
12
+ # @return [String|Gravel::APNS::Notification::Localization] The title.
13
+ #
14
+ attr_accessor :title
15
+
16
+ # The subtitle of the notification.
17
+ # You can provide a localization on this value.
18
+ #
19
+ # @return [String|Gravel::APNS::Notification::Localization] The subtitle.
20
+ #
21
+ attr_accessor :subtitle
22
+
23
+ # The body of the notification.
24
+ # You can provide a localization on this value.
25
+ #
26
+ # @return [String|Gravel::APNS::Notification::Localization] The body.
27
+ #
28
+ attr_accessor :body
29
+
30
+ # The name of the sound file to play when notifying the user.
31
+ #
32
+ # @return [String] The sound file name.
33
+ #
34
+ attr_accessor :sound
35
+
36
+ # The badge number to show on the application icon.
37
+ #
38
+ # @return [Integer] The badge number.
39
+ #
40
+ attr_accessor :badge
41
+
42
+ # A localization key to use when populating the content of the 'View' button.
43
+ #
44
+ # @param [String] The action button's localization key.
45
+ #
46
+ attr_accessor :action_key
47
+
48
+ # A category to identify the notification's type.
49
+ # This should match one of the identifier values as defined in your
50
+ # application.
51
+ #
52
+ # @param [String] The notification's category.
53
+ #
54
+ attr_accessor :category
55
+
56
+ # The filename of an image to use when launching the app.
57
+ #
58
+ # @param [String] The launch image filename.
59
+ #
60
+ attr_accessor :launch_image
61
+
62
+ # Set to true to trigger a silent notification in your application.
63
+ # This is useful to trigger a background app refresh.
64
+ #
65
+ # @return [Boolean] Whether or not new content is available.
66
+ #
67
+ attr_accessor :content_available
68
+
69
+ # The mutable content of the notification.
70
+ #
71
+ # @return [Hash] The mutable content.
72
+ #
73
+ attr_accessor :mutable_content
74
+
75
+ # A unique identifier for this notification.
76
+ #
77
+ # @return [String] The unique identifier.
78
+ #
79
+ attr_accessor :uuid
80
+
81
+ # A group identifier for the notification.
82
+ # This allows APNS to identify similar messages and collapse them
83
+ # into a single notification.
84
+ #
85
+ # @return [String] The collapse identifier.
86
+ #
87
+ attr_accessor :collapse_id
88
+
89
+ # The priority of the notification.
90
+ #
91
+ # @return [Integer] The priority.
92
+ #
93
+ attr_accessor :priority
94
+
95
+ # A time when the notification is no longer valid and APNS should stop
96
+ # attempting to deliver the notification.
97
+ #
98
+ # @return [Time] The expiration time.
99
+ #
100
+ attr_accessor :expiration
101
+
102
+ # A token representing the device you want to send the notification to.
103
+ #
104
+ # @return [String] The device token.
105
+ #
106
+ attr_accessor :device_token
107
+
108
+ # Create a new APNS notification.
109
+ #
110
+ # @return [Gravel::APNS::Notification] The notification object.
111
+ #
112
+ def initialize
113
+ self.content_available = false
114
+ self.uuid = SecureRandom.uuid
115
+ end
116
+
117
+ # Quickly create the same notification for multiple device tokens.
118
+ #
119
+ # @param tokens [Splat] An array of device tokens.
120
+ # @return [Array] An array of notifications.
121
+ #
122
+ def for_device_tokens(*tokens)
123
+ tokens.map do |token|
124
+ notification = self.dup
125
+ notification.uuid = SecureRandom.uuid
126
+ notification.device_token = token
127
+ notification
128
+ end
129
+ end
130
+
131
+ # Generate the APNS payload.
132
+ #
133
+ # @return [Hash] The APNS payload.
134
+ #
135
+ def payload
136
+ aps = Hash.new
137
+
138
+ if self.title.is_a?(String)
139
+ aps['alert'] ||= Hash.new
140
+ aps['alert']['title'] = self.title
141
+ elsif self.title.is_a?(Gravel::APNS::Notification::Localization)
142
+ aps['alert'] ||= Hash.new
143
+ aps['alert'].merge!(self.title.payload(:title))
144
+ end
145
+
146
+ if self.subtitle.is_a?(String)
147
+ aps['alert'] ||= Hash.new
148
+ aps['alert']['subtitle'] = self.subtitle
149
+ elsif self.subtitle.is_a?(Gravel::APNS::Notification::Localization)
150
+ aps['alert'] ||= Hash.new
151
+ aps['alert'].merge!(self.subtitle.payload(:subtitle))
152
+ end
153
+
154
+ if self.body.is_a?(String)
155
+ aps['alert'] ||= Hash.new
156
+ aps['alert']['body'] = self.body
157
+ elsif self.body.is_a?(Gravel::APNS::Notification::Localization)
158
+ aps['alert'] ||= Hash.new
159
+ aps['alert'].merge!(self.body.payload(:body))
160
+ end
161
+
162
+ if self.sound == :default
163
+ aps['sound'] = 'default'
164
+ elsif self.sound.is_a?(String)
165
+ aps['sound'] = self.sound.to_s
166
+ end
167
+
168
+ if self.badge.is_a?(Integer)
169
+ aps['badge'] = self.badge
170
+ end
171
+
172
+ if self.action_key.is_a?(String)
173
+ aps['alert']['action-loc-key'] = self.action_key
174
+ end
175
+
176
+ if self.category.is_a?(String)
177
+ aps['category'] = self.category
178
+ end
179
+
180
+ if self.launch_image.is_a?(String)
181
+ aps['alert']['launch-image'] = self.launch_image
182
+ end
183
+
184
+ if self.content_available
185
+ aps['content-available'] = '1'
186
+ end
187
+
188
+ payload = Hash.new
189
+
190
+ if self.mutable_content.is_a?(Hash)
191
+ aps['mutable-content'] = '1'
192
+ payload.merge!(self.mutable_content)
193
+ end
194
+
195
+ unless aps.empty?
196
+ payload['aps'] = aps
197
+ end
198
+
199
+ payload
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,88 @@
1
+ module Gravel
2
+ class APNS
3
+ class Notification
4
+ # This class can be used to localize part of a notification.
5
+ # You can set a notification's title, subtitle or body attribute
6
+ # to an instance of this class to localize it.
7
+ #
8
+ class Localization
9
+ # The localization key as defined in your app's localization file.
10
+ #
11
+ # @return [String] The localization key.
12
+ #
13
+ attr_reader :key
14
+
15
+ # Additional arguments to pass into the localizable string.
16
+ #
17
+ # @return [Array] Additional arguments.
18
+ #
19
+ attr_accessor :arguments
20
+
21
+ # Create a new localization.
22
+ #
23
+ # @param key [String] The localization key.
24
+ # @param args [Splat] Arguments to pass into the localizable string.
25
+ # @return [Gravel::APNS::Notification::Localization] The localization object.
26
+ #
27
+ def initialize(key, *args)
28
+ unless key.is_a?(String)
29
+ raise 'The localization key must be a string.'
30
+ end
31
+
32
+ @key = key.to_s
33
+ self.arguments = args
34
+ end
35
+
36
+ # Set additional arguments to pass into the localizable string.
37
+ #
38
+ # @param arguments [Array] Additional arguments.
39
+ # @return [Array] The input value.
40
+ #
41
+ def arguments=(arguments)
42
+ unless arguments.nil?
43
+ unless arguments.is_a?(Array)
44
+ raise 'The localization arguments must be an array.'
45
+ end
46
+
47
+ unless arguments.all? { |a| a.is_a?(Float) || a.is_a?(Integer) || a.is_a?(String) }
48
+ raise 'The localization arguments must all be primitives.'
49
+ end
50
+ end
51
+
52
+ @arguments = arguments
53
+ end
54
+
55
+ # Check if there are any additional arguments.
56
+ #
57
+ # @return [Boolean] Whether or not there are any additional arguments.
58
+ #
59
+ def arguments?
60
+ self.arguments && self.arguments.any?
61
+ end
62
+
63
+ # Convert the localization into APNS payload components.
64
+ #
65
+ # @param type [Symbol] The localization type (title/subtitle/body).
66
+ # @return [Hash] The APNS payload components.
67
+ #
68
+ def payload(type)
69
+ components = Hash.new
70
+
71
+ case type
72
+ when :title
73
+ components['title-loc-key'] = @key
74
+ components['title-loc-args'] = self.arguments if arguments?
75
+ when :subtitle
76
+ components['subtitle-loc-key'] = @key
77
+ components['subtitle-loc-args'] = self.arguments if arguments?
78
+ when :body
79
+ components['loc-key'] = @key
80
+ components['loc-args'] = self.arguments if arguments?
81
+ end
82
+
83
+ components
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module Gravel
2
+ VERSION = '0.1.0.alpha1'
3
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'gravel'
@@ -0,0 +1,5 @@
1
+ describe Gravel do
2
+ it 'should have a version' do
3
+ expect { Gravel::VERSION }.not_to raise_exception
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gravel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha1
5
+ platform: ruby
6
+ authors:
7
+ - Nialto Services
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-http2
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.14.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.14.1
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.11'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.11'
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
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.9.8
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.9.8
97
+ description:
98
+ email:
99
+ - support@nialtoservices.co.uk
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - Gemfile
107
+ - README.md
108
+ - Rakefile
109
+ - bin/console
110
+ - bin/setup
111
+ - gravel.gemspec
112
+ - lib/gravel.rb
113
+ - lib/gravel/apns.rb
114
+ - lib/gravel/apns/auto_token.rb
115
+ - lib/gravel/apns/notification.rb
116
+ - lib/gravel/apns/notification/localization.rb
117
+ - lib/gravel/constants.rb
118
+ - spec/spec_helper.rb
119
+ - spec/unit/gravel/constants_spec.rb
120
+ homepage: https://github.com/nialtoservices/gravel
121
+ licenses:
122
+ - MIT
123
+ metadata:
124
+ yard.run: yri
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.3.1
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.6.11
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Unified Push Notifications
145
+ test_files:
146
+ - spec/spec_helper.rb
147
+ - spec/unit/gravel/constants_spec.rb