exponent-server-sdk 0.0.7 → 0.1.0

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.
@@ -1,10 +1,10 @@
1
- language: ruby
2
- cache: bundler
3
- rvm:
4
- - 2.6.0
5
- - 2.5.0
6
- - 2.4.0
7
- - 2.3.0
8
- before_install:
9
- - gem update --system
10
- - gem install bundler
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.6.0
5
+ - 2.5.0
6
+ - 2.4.0
7
+ - 2.3.0
8
+ before_install:
9
+ - gem update --system
10
+ - gem install bundler
data/README.md CHANGED
@@ -1,58 +1,72 @@
1
- # Exponent Server SDK Ruby
2
- [![Build Status](https://travis-ci.org/expo/expo-server-sdk-ruby.svg?branch=master)](https://travis-ci.org/expo/expo-server-sdk-ruby)
3
- [![Gem Version](https://badge.fury.io/rb/exponent-server-sdk.svg)](https://badge.fury.io/rb/exponent-server-sdk)
4
-
5
- Use to send push notifications to Exponent Experiences from a Ruby server.
6
-
7
- ## Installation
8
-
9
- Add this line to your application's Gemfile:
10
-
11
- ```ruby
12
- gem 'exponent-server-sdk'
13
- ```
14
-
15
- And then execute:
16
-
17
- ```shell
18
- $ bundle
19
- ```
20
-
21
- Or install it yourself as:
22
-
23
- ```shell
24
- $ gem install exponent-server-sdk
25
- ```
26
-
27
- ## Usage
28
-
29
- ### Client
30
-
31
- The push client is the preferred way. This hits the latest version of the api.
32
-
33
- Optional arguments: `gzip: true`
34
-
35
-
36
- ```ruby
37
- client = Exponent::Push::Client.new
38
- # client = Exponent::Push::Client.new(gzip: true) # for compressed, faster requests
39
-
40
- messages = [{
41
- to: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
42
- sound: "default",
43
- body: "Hello world!"
44
- }, {
45
- to: "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]",
46
- badge: 1,
47
- body: "You've got mail"
48
- }]
49
-
50
- client.publish messages
51
- ```
52
-
53
- The complete format of the messages can be found [here.](https://docs.expo.io/versions/latest/guides/push-notifications#message-format)
54
-
55
- ## Contributing
56
-
57
- If you have problems with the code in this repository, please file issues & bug reports. We encourage you
58
- to submit a pull request with a solution or a failing test to reproduce your issue. Thanks!
1
+ # Exponent Server SDK Ruby
2
+
3
+ [![Build Status](https://travis-ci.org/expo/expo-server-sdk-ruby.svg?branch=master)](https://travis-ci.org/expo/expo-server-sdk-ruby)
4
+ [![Gem Version](https://badge.fury.io/rb/exponent-server-sdk.svg)](https://badge.fury.io/rb/exponent-server-sdk)
5
+
6
+ Use to send push notifications to Exponent Experiences from a Ruby server.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'exponent-server-sdk'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```shell
19
+ $ bundle
20
+ ```
21
+
22
+ Or install it yourself as:
23
+
24
+ ```shell
25
+ $ gem install exponent-server-sdk
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Client
31
+
32
+ The push client is the preferred way. This hits the latest version of the api.
33
+
34
+ Optional arguments: `gzip: true`
35
+
36
+ ```ruby
37
+ client = Exponent::Push::Client.new
38
+ # client = Exponent::Push::Client.new(gzip: true) # for compressed, faster requests
39
+
40
+ messages = [{
41
+ to: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
42
+ sound: "default",
43
+ body: "Hello world!"
44
+ }, {
45
+ to: "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]",
46
+ badge: 1,
47
+ body: "You've got mail"
48
+ }]
49
+
50
+ # @Deprecated
51
+ # client.publish(messages)
52
+
53
+ # MAX 100 messages at a time
54
+ handler = client.send_messages(messages)
55
+
56
+ # Array of all errors returned from the API
57
+ # puts handler.errors
58
+
59
+ # you probably want to delay calling this because the service might take a few moments to send
60
+ # I would recommend reading the expo documentation regarding delivery delays
61
+ client.verify_deliveries(handler.receipt_ids)
62
+
63
+ ```
64
+
65
+ See the getting started example. If you clone this repo, you can also use it to test locally by entering your ExponentPushToken. Otherwise it serves as a good copy pasta example to get you going.
66
+
67
+ The complete format of the messages can be found [here.](https://docs.expo.io/versions/latest/guides/push-notifications#message-format)
68
+
69
+ ## Contributing
70
+
71
+ If you have problems with the code in this repository, please file issues & bug reports. We encourage you
72
+ to submit a pull request with a solution or a failing test to reproduce your issue. Thanks!
data/Rakefile CHANGED
@@ -1,19 +1,19 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
3
 
4
- def load_libs(t)
5
- t.libs << "test"
6
- t.libs << "lib"
4
+ def load_libs(rake_task)
5
+ rake_task.libs << 'test'
6
+ rake_task.libs << 'lib'
7
7
  end
8
8
 
9
- Rake::TestTask.new(:test) do |t|
10
- load_libs t
11
- t.test_files = FileList['test/**/*-test.rb']
9
+ Rake::TestTask.new(:test) do |rake_task|
10
+ load_libs rake_task
11
+ rake_task.test_files = FileList['test/**/*-test.rb']
12
12
  end
13
13
 
14
- Rake::TestTask.new(:manual_test) do |t|
15
- load_libs t
16
- t.test_files = FileList['manual_test.rb']
14
+ Rake::TestTask.new(:getting_started) do |rake_task|
15
+ load_libs rake_task
16
+ rake_task.test_files = FileList['examples/getting_started.rb']
17
17
  end
18
18
 
19
- task :default => :test
19
+ task default: :test
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'exponent-server-sdk'
4
+
5
+ class Test
6
+ def initialize
7
+ # @client = Exponent::Push::Client.new
8
+
9
+ # OR use GZIP to be AWESOME
10
+ @client = Exponent::Push::Client.new(gzip: true)
11
+ end
12
+
13
+ def too_many_messages
14
+ (0..101).map { create_message }
15
+ end
16
+
17
+ def create_message
18
+ {
19
+ # REPLACE WITH YOUR EXPONENT PUSH TOKEN LIKE:
20
+ # to: 'ExponentPushToken[g5sIEbOm2yFdzn5VdSSy9n]',
21
+ to: "ExponentPushToken[#{(0...22).map { ('a'..'z').to_a[rand(26)] }.join}]",
22
+ sound: 'default',
23
+ title: 'Hello World',
24
+ subtitle: 'This is a Push Notification',
25
+ body: 'Here\'s a little message for you...',
26
+ data: {
27
+ user_id: 1,
28
+ points: 23_434
29
+ },
30
+ ttl: 10,
31
+ expiration: 1_886_207_332,
32
+ priority: 'default',
33
+ badge: 0,
34
+ channelId: 'game'
35
+ }
36
+ end
37
+
38
+ def test
39
+ # messages = too_many_messages
40
+ messages = [create_message]
41
+
42
+ response_handler = @client.send_messages(messages)
43
+ puts response_handler.response.response_body
44
+ end
45
+ end
46
+
47
+ Test.new.test
@@ -1,5 +1,4 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'exponent-server-sdk/version'
5
4
 
@@ -21,6 +20,7 @@ Gem::Specification.new do |spec|
21
20
  spec.add_dependency 'typhoeus'
22
21
 
23
22
  spec.add_development_dependency 'bundler'
24
- spec.add_development_dependency 'rake'
25
23
  spec.add_development_dependency 'minitest'
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rubocop'
26
26
  end
@@ -1,160 +1,315 @@
1
- require 'exponent-server-sdk/version'
2
- require 'typhoeus'
3
- require 'json'
4
-
5
- module Exponent
6
- def self.is_exponent_push_token?(token)
7
- token.start_with?('ExponentPushToken')
8
- end
9
-
10
- module Push
11
-
12
- class Client
13
- def initialize(**args)
14
- @http_client = args[:http_client] || Typhoeus
15
- @response_handler = args[:response_handler] || ResponseHandler.new
16
- @gzip = args[:gzip] == true
17
- end
18
-
19
- def publish(messages)
20
- response_handler.handle(push_notifications(messages))
21
- end
22
-
23
- private
24
-
25
- attr_reader :http_client, :response_handler
26
-
27
- def push_notifications(messages)
28
- http_client.post(
29
- push_url,
30
- body: messages.to_json,
31
- headers: headers
32
- )
33
- end
34
-
35
- def push_url
36
- 'https://exp.host/--/api/v2/push/send'
37
- end
38
-
39
- def headers
40
- headers = {
41
- 'Content-Type' => 'application/json',
42
- 'Accept' => 'application/json'
43
- }
44
- headers['Accept-Encoding'] = 'gzip, deflate' if @gzip
45
- headers
46
- end
47
- end
48
-
49
- class ResponseHandler
50
- def initialize(error_builder = ErrorBuilder.new)
51
- @error_builder = error_builder
52
- end
53
-
54
- def handle(response)
55
- case response.code.to_s
56
- when /(^4|^5)/
57
- raise build_error_from_failure(parse_json(response))
58
- else
59
- handle_success(parse_json(response))
60
- end
61
- end
62
-
63
- private
64
-
65
- attr_reader :error_builder
66
-
67
- def parse_json(response)
68
- JSON.parse(response.body)
69
- end
70
-
71
- def build_error_from_failure(response)
72
- error_builder.build_from_erroneous(response)
73
- end
74
-
75
- def handle_success(response)
76
- extract_data(response)
77
- end
78
-
79
- def extract_data(response)
80
- data = response.fetch('data')
81
- if data.is_a? Hash
82
- validate_status(data.fetch('status'), response)
83
- data
84
- else
85
- data.map do |receipt|
86
- validate_status(receipt.fetch('status'), response)
87
- receipt
88
- end
89
- end
90
- end
91
-
92
- def validate_status(status, response)
93
- raise build_error_from_success(response) unless status == 'ok'
94
- end
95
-
96
- def build_error_from_success(response)
97
- error_builder.build_from_successful(response)
98
- end
99
- end
100
-
101
- class ErrorBuilder
102
- %i[erroneous successful].each do |selector|
103
- define_method(:"build_from_#{selector}") do |response|
104
- with_error_handling(response) do
105
- send "from_#{selector}_response", response
106
- end
107
- end
108
- end
109
-
110
- private
111
-
112
- def with_error_handling(response)
113
- yield(response)
114
- rescue KeyError
115
- unknown_error_format(response)
116
- end
117
-
118
- def from_erroneous_response(response)
119
- error = response.fetch('errors').first
120
- error_name = error.fetch('code')
121
- message = error.fetch('message')
122
-
123
- get_error_class(error_name).new(message)
124
- end
125
-
126
- def from_successful_response(response)
127
- data = response.fetch('data').first
128
- message = data.fetch('message')
129
-
130
- get_error_class(data.fetch('details').fetch('error')).new(message)
131
- end
132
-
133
- def validate_error_name(condition)
134
- condition ? yield : Exponent::Push::UnknownError
135
- end
136
-
137
- def get_error_class(error_name)
138
- validate_error_name(Exponent::Push.error_names.include?(error_name)) do
139
- Exponent::Push.const_get("#{error_name}Error")
140
- end
141
- end
142
-
143
- def unknown_error_format(response)
144
- Exponent::Push::UnknownError.new("Unknown error format: #{response}")
145
- end
146
- end
147
-
148
- Error = Class.new(StandardError)
149
-
150
- def self.error_names
151
- %w[DeviceNotRegistered MessageTooBig
152
- MessageRateExceeded InvalidCredentials
153
- Unknown]
154
- end
155
-
156
- error_names.each do |error_name|
157
- const_set "#{error_name}Error", Class.new(Error)
158
- end
159
- end
160
- end
1
+ require 'exponent-server-sdk/version'
2
+ require 'exponent-server-sdk/too_many_messages_error'
3
+ require 'typhoeus'
4
+ require 'json'
5
+
6
+ # Basic Usage:
7
+ #
8
+ # Create new client
9
+ # client = Exponent::Push::Client.new(**args)
10
+ #
11
+ # Send UPTO ~~100~~ messages per call,
12
+ # https://docs.expo.io/versions/latest/guides/push-notifications/#message-format
13
+ # response_handler = client.send_messages([list of formatted messages])
14
+ #
15
+ # Check the response to see if any errors were re
16
+ # response_handler.errors?
17
+ #
18
+ # To process each error, iterate over the errors array
19
+ # which contains each Error class instance
20
+ # response_handler.errors
21
+ #
22
+ # There is an array of invalid ExponentPushTokens that were found in the initial /send call
23
+ # response_handler.invalid_push_tokens['ExponentPushToken[1212121212121212]']
24
+ #
25
+ # You can use the handler to get receipt_ids
26
+ # response_handler.receipt_ids
27
+ #
28
+ # You can pass an array of receipt_ids to verify_deliveries method and
29
+ # it will populate a new ResponseHandler with any errors
30
+ # receipt_response = client.verify_deliveries(receipt_ids)
31
+
32
+ module Exponent
33
+ def self.is_exponent_push_token?(token)
34
+ token.start_with?('ExponentPushToken')
35
+ end
36
+
37
+ module Push
38
+ class Client
39
+ def initialize(**args)
40
+ @http_client = args[:http_client] || Typhoeus
41
+ @error_builder = ErrorBuilder.new
42
+ # future versions will deprecate this
43
+ @response_handler = args[:response_handler] || ResponseHandler.new
44
+ @gzip = args[:gzip] == true
45
+ end
46
+
47
+ # returns a string response with parsed success json or error
48
+ # @deprecated
49
+ def publish(messages)
50
+ warn '[DEPRECATION] `publish` is deprecated. Please use `send_messages` instead.'
51
+ @response_handler.handle(push_notifications(messages))
52
+ end
53
+
54
+ # returns response handler that provides access to errors? and other response inspection methods
55
+ def send_messages(messages, **args)
56
+ # https://docs.expo.io/versions/latest/guides/push-notifications/#message-format
57
+ raise TooManyMessagesError, 'Only 100 message objects at a time allowed.' if messages.length > 100
58
+
59
+ response = push_notifications(messages)
60
+
61
+ # each call to send_messages will return a new instance of ResponseHandler
62
+ handler = args[:response_handler] || ResponseHandler.new
63
+ handler.process_response(response)
64
+ handler
65
+ end
66
+
67
+ def verify_deliveries(receipt_ids, **args)
68
+ response = get_receipts(receipt_ids)
69
+ handler = args[:response_handler] || ResponseHandler.new
70
+ handler.process_response(response)
71
+ handler
72
+ end
73
+
74
+ private
75
+
76
+ def push_notifications(messages)
77
+ @http_client.post(
78
+ push_url,
79
+ body: messages.to_json,
80
+ headers: headers,
81
+ accept_encoding: @gzip
82
+ )
83
+ end
84
+
85
+ def push_url
86
+ 'https://exp.host/--/api/v2/push/send'
87
+ end
88
+
89
+ def get_receipts(receipt_ids)
90
+ @http_client.post(
91
+ receipts_url,
92
+ body: { ids: receipt_ids }.to_json,
93
+ headers: headers,
94
+ accept_encoding: @gzip
95
+ )
96
+ end
97
+
98
+ def receipts_url
99
+ 'https://exp.host/--/api/v2/push/getReceipts'
100
+ end
101
+
102
+ def headers
103
+ headers = {
104
+ 'Content-Type' => 'application/json',
105
+ 'Accept' => 'application/json'
106
+ }
107
+ headers
108
+ end
109
+ end
110
+
111
+ class ResponseHandler
112
+ attr_reader :response, :invalid_push_tokens, :receipt_ids, :errors
113
+
114
+ def initialize(error_builder = ErrorBuilder.new)
115
+ @error_builder = error_builder
116
+ @response = nil
117
+ @receipt_ids = []
118
+ @invalid_push_tokens = []
119
+ @errors = []
120
+ end
121
+
122
+ def process_response(response)
123
+ @response = response
124
+
125
+ case response.code.to_s
126
+ when /(^4|^5)/
127
+ raise @error_builder.parse_response(response)
128
+ else
129
+ sort_results
130
+ end
131
+ end
132
+
133
+ def errors?
134
+ @errors.any?
135
+ end
136
+
137
+ # @deprecated
138
+ def handle(response)
139
+ warn '[DEPRECATION] `handle` is deprecated. Please use `process_response` instead.'
140
+ @response = response
141
+ case response.code.to_s
142
+ when /(^4|^5)/
143
+ raise build_error_from_failure
144
+ else
145
+ extract_data
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def sort_results
152
+ data = body&.fetch('data', nil) || nil
153
+
154
+ # something is definitely wrong
155
+ return if data.nil?
156
+
157
+ # Array indicates a response from the /send endpoint
158
+ # Hash indicates a response from the /getReceipts endpoint
159
+ if data.is_a? Array
160
+ data.each do |push_ticket|
161
+ receipt_id = push_ticket.fetch('id', nil)
162
+ if push_ticket.fetch('status', nil) == 'ok'
163
+ @receipt_ids.push(receipt_id) unless receipt_id.nil?
164
+ else
165
+ process_error(push_ticket)
166
+ end
167
+ end
168
+ else
169
+ process_receipts(data)
170
+ end
171
+ end
172
+
173
+ def process_receipts(receipts)
174
+ receipts.each do |receipt_id, receipt|
175
+ @receipt_ids.push(receipt_id) unless receipt_id.nil?
176
+ process_error(receipt) unless receipt.fetch('status') == 'ok'
177
+ end
178
+ end
179
+
180
+ def process_error(push_ticket)
181
+ message = push_ticket.fetch('message')
182
+ matches = message.match(/ExponentPushToken\[(...*)\]/)
183
+ error_class = @error_builder.parse_push_ticket(push_ticket)
184
+
185
+ @invalid_push_tokens.push(matches[0]) unless matches.nil?
186
+
187
+ @errors.push(error_class) unless @errors.include?(error_class)
188
+ end
189
+
190
+ def body
191
+ # memoization FTW!
192
+ @body ||= JSON.parse(@response.body)
193
+ rescue SyntaxError
194
+ # Sometimes the server returns an empty string.
195
+ # It must be escaped before we can process it.
196
+ @body = JSON.parse(@response.body.to_json)
197
+ rescue StandardError
198
+ # Prevent nil errors in old version of ruby when using fetch
199
+ @body = {}
200
+ end
201
+
202
+ ##### DEPRECATED METHODS #####
203
+
204
+ # @deprecated
205
+ def build_error_from_failure
206
+ @error_builder.build_from_erroneous(body)
207
+ end
208
+
209
+ # @deprecated
210
+ def extract_data
211
+ data = body.fetch('data')
212
+ if data.is_a? Hash
213
+ validate_status(data.fetch('status'), body)
214
+ data
215
+ elsif data.is_a? Array
216
+ data.map do |receipt|
217
+ validate_status(receipt.fetch('status'), body)
218
+ receipt
219
+ end
220
+ else
221
+ {}
222
+ end
223
+ end
224
+
225
+ # @deprecated
226
+ def validate_status(status, response)
227
+ raise build_error_from_success(response) unless status == 'ok'
228
+ end
229
+
230
+ # @deprecated
231
+ def build_error_from_success(response)
232
+ @error_builder.build_from_successful(response)
233
+ end
234
+ end
235
+
236
+ class ErrorBuilder
237
+ def parse_response(response)
238
+ with_error_handling(response) do
239
+ error = response.fetch('errors')
240
+ error_name = error.fetch('code')
241
+ message = error.fetch('message')
242
+
243
+ get_error_class(error_name).new(message)
244
+ end
245
+ end
246
+
247
+ def parse_push_ticket(push_ticket)
248
+ with_error_handling(push_ticket) do
249
+ message = push_ticket.fetch('message')
250
+ get_error_class(push_ticket.fetch('details').fetch('error')).new(message)
251
+ end
252
+ end
253
+
254
+ %i[erroneous successful].each do |selector|
255
+ define_method(:"build_from_#{selector}") do |response|
256
+ with_error_handling(response) do
257
+ send "from_#{selector}_response", response
258
+ end
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ def with_error_handling(response)
265
+ yield(response)
266
+ rescue KeyError, NoMethodError
267
+ unknown_error_format(response)
268
+ end
269
+
270
+ def validate_error_name(condition)
271
+ condition ? yield : Exponent::Push::UnknownError
272
+ end
273
+
274
+ def get_error_class(error_name)
275
+ validate_error_name(Exponent::Push.error_names.include?(error_name)) do
276
+ Exponent::Push.const_get("#{error_name}Error")
277
+ end
278
+ end
279
+
280
+ def unknown_error_format(response)
281
+ Exponent::Push::UnknownError.new("Unknown error format: #{response}")
282
+ end
283
+
284
+ ##### DEPRECATED METHODS #####
285
+
286
+ # @deprecated
287
+ def from_erroneous_response(response)
288
+ error = response.fetch('errors').first
289
+ error_name = error.fetch('code')
290
+ message = error.fetch('message')
291
+
292
+ get_error_class(error_name).new(message)
293
+ end
294
+
295
+ # @deprecated
296
+ def from_successful_response(response)
297
+ delivery_result = response.fetch('data').first
298
+ message = delivery_result.fetch('message')
299
+ get_error_class(delivery_result.fetch('details').fetch('error')).new(message)
300
+ end
301
+ end
302
+
303
+ Error = Class.new(StandardError)
304
+
305
+ def self.error_names
306
+ %w[DeviceNotRegistered MessageTooBig
307
+ MessageRateExceeded InvalidCredentials
308
+ Unknown]
309
+ end
310
+
311
+ error_names.each do |error_name|
312
+ const_set "#{error_name}Error", Class.new(Error)
313
+ end
314
+ end
315
+ end