exponent-server-sdk-jm 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.
data/.travis.yml ADDED
@@ -0,0 +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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in exponent-server-sdk-jm.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 Jesse Ruder
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
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/push-notifications/sending-notifications/#message-request-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 ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ def load_libs(rake_task)
5
+ rake_task.libs << 'test'
6
+ rake_task.libs << 'lib'
7
+ end
8
+
9
+ Rake::TestTask.new(:test) do |rake_task|
10
+ load_libs rake_task
11
+ rake_task.test_files = FileList['test/**/*-test.rb']
12
+ end
13
+
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
+ end
18
+
19
+ task default: :test
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'exponent-server-sdk-jm'
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
@@ -0,0 +1,26 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'exponent-server-sdk-jm/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'exponent-server-sdk-jm'
7
+ spec.version = Exponent::VERSION
8
+ spec.authors = ['Jesse Ruder', 'Pablo Gomez']
9
+ spec.email = ['jesse@sixfivezero.net', 'pablonahuelgomez@gmail.com']
10
+ spec.summary = %q{Exponent Server SDK - JM fork}
11
+ spec.description = %q{Exponent Server SDK - JM fork}
12
+ spec.homepage = ''
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_dependency 'typhoeus'
21
+
22
+ spec.add_development_dependency 'bundler'
23
+ spec.add_development_dependency 'minitest'
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rubocop'
26
+ end
@@ -0,0 +1,2 @@
1
+ class TooManyMessagesError < StandardError
2
+ end
@@ -0,0 +1,3 @@
1
+ module Exponent
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,317 @@
1
+ require 'exponent-server-sdk-jm/version'
2
+ require 'exponent-server-sdk-jm/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
+ invalid = message.match(/ExponentPushToken\[(...*)\]/)
183
+ unregistered = message.match(/\"(...*)\"/)
184
+ error_class = @error_builder.parse_push_ticket(push_ticket)
185
+
186
+ @invalid_push_tokens.push(invalid[0]) unless invalid.nil?
187
+ @invalid_push_tokens.push(unregistered[1]) unless unregistered.nil?
188
+
189
+ @errors.push(error_class) unless @errors.include?(error_class)
190
+ end
191
+
192
+ def body
193
+ # memoization FTW!
194
+ @body ||= JSON.parse(@response.body)
195
+ rescue SyntaxError
196
+ # Sometimes the server returns an empty string.
197
+ # It must be escaped before we can process it.
198
+ @body = JSON.parse(@response.body.to_json)
199
+ rescue StandardError
200
+ # Prevent nil errors in old version of ruby when using fetch
201
+ @body = {}
202
+ end
203
+
204
+ ##### DEPRECATED METHODS #####
205
+
206
+ # @deprecated
207
+ def build_error_from_failure
208
+ @error_builder.build_from_erroneous(body)
209
+ end
210
+
211
+ # @deprecated
212
+ def extract_data
213
+ data = body.fetch('data')
214
+ if data.is_a? Hash
215
+ validate_status(data.fetch('status'), body)
216
+ data
217
+ elsif data.is_a? Array
218
+ data.map do |receipt|
219
+ validate_status(receipt.fetch('status'), body)
220
+ receipt
221
+ end
222
+ else
223
+ {}
224
+ end
225
+ end
226
+
227
+ # @deprecated
228
+ def validate_status(status, response)
229
+ raise build_error_from_success(response) unless status == 'ok'
230
+ end
231
+
232
+ # @deprecated
233
+ def build_error_from_success(response)
234
+ @error_builder.build_from_successful(response)
235
+ end
236
+ end
237
+
238
+ class ErrorBuilder
239
+ def parse_response(response)
240
+ with_error_handling(response) do
241
+ error = response.fetch('errors')
242
+ error_name = error.fetch('code')
243
+ message = error.fetch('message')
244
+
245
+ get_error_class(error_name).new(message)
246
+ end
247
+ end
248
+
249
+ def parse_push_ticket(push_ticket)
250
+ with_error_handling(push_ticket) do
251
+ message = push_ticket.fetch('message')
252
+ get_error_class(push_ticket.fetch('details').fetch('error')).new(message)
253
+ end
254
+ end
255
+
256
+ %i[erroneous successful].each do |selector|
257
+ define_method(:"build_from_#{selector}") do |response|
258
+ with_error_handling(response) do
259
+ send "from_#{selector}_response", response
260
+ end
261
+ end
262
+ end
263
+
264
+ private
265
+
266
+ def with_error_handling(response)
267
+ yield(response)
268
+ rescue KeyError, NoMethodError
269
+ unknown_error_format(response)
270
+ end
271
+
272
+ def validate_error_name(condition)
273
+ condition ? yield : Exponent::Push::UnknownError
274
+ end
275
+
276
+ def get_error_class(error_name)
277
+ validate_error_name(Exponent::Push.error_names.include?(error_name)) do
278
+ Exponent::Push.const_get("#{error_name}Error")
279
+ end
280
+ end
281
+
282
+ def unknown_error_format(response)
283
+ Exponent::Push::UnknownError.new("Unknown error format: #{response.respond_to?(:body) ? response.body : response}")
284
+ end
285
+
286
+ ##### DEPRECATED METHODS #####
287
+
288
+ # @deprecated
289
+ def from_erroneous_response(response)
290
+ error = response.fetch('errors').first
291
+ error_name = error.fetch('code')
292
+ message = error.fetch('message')
293
+
294
+ get_error_class(error_name).new(message)
295
+ end
296
+
297
+ # @deprecated
298
+ def from_successful_response(response)
299
+ delivery_result = response.fetch('data').first
300
+ message = delivery_result.fetch('message')
301
+ get_error_class(delivery_result.fetch('details').fetch('error')).new(message)
302
+ end
303
+ end
304
+
305
+ Error = Class.new(StandardError)
306
+
307
+ def self.error_names
308
+ %w[DeviceNotRegistered MessageTooBig
309
+ MessageRateExceeded InvalidCredentials
310
+ Unknown]
311
+ end
312
+
313
+ error_names.each do |error_name|
314
+ const_set "#{error_name}Error", Class.new(Error)
315
+ end
316
+ end
317
+ end