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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +3923 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +19 -0
- data/examples/getting_started.rb +47 -0
- data/exponent-server-sdk-jm.gemspec +26 -0
- data/lib/exponent-server-sdk-jm/too_many_messages_error.rb +2 -0
- data/lib/exponent-server-sdk-jm/version.rb +3 -0
- data/lib/exponent-server-sdk-jm.rb +317 -0
- data/test/exponent-server-sdk-test.rb +680 -0
- metadata +129 -0
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
|
+
[](https://travis-ci.org/expo/expo-server-sdk-ruby)
|
4
|
+
[](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,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
|