gravel 0.1.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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