expo-push 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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/expo-push.gemspec +26 -0
- data/lib/expo-push/too_many_messages_error.rb +2 -0
- data/lib/expo-push/version.rb +3 -0
- data/lib/expo-push.rb +269 -0
- data/test/expo-push-test.rb +550 -0
- metadata +131 -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
|
+
[![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 'expo-push'
|
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 expo-push
|
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 'expo-push'
|
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
|
data/expo-push.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'expo-push/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'expo-push'
|
7
|
+
spec.version = Exponent::VERSION
|
8
|
+
spec.authors = ['Jesse Ruder', 'Pablo Gomez', 'Mike Taylor']
|
9
|
+
spec.email = ['jesse@sixfivezero.net', 'pablonahuelgomez@gmail.com', 'mike.taylor@growmaple.com']
|
10
|
+
spec.summary = %q{Expo Push}
|
11
|
+
spec.description = %q{A ruby gem to communicate with the Expo Push service.}
|
12
|
+
spec.homepage = 'https://github.com/growmaple/expo-push'
|
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', '~> 1.4'
|
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
|
data/lib/expo-push.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
require 'expo-push/version'
|
2
|
+
require 'expo-push/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 response handler that provides access to errors? and other response inspection methods
|
48
|
+
def send_messages(messages, **args)
|
49
|
+
# https://docs.expo.io/versions/latest/guides/push-notifications/#message-format
|
50
|
+
raise TooManyMessagesError, 'Only 100 message objects at a time allowed.' if messages.length > 100
|
51
|
+
|
52
|
+
response = push_notifications(messages)
|
53
|
+
|
54
|
+
# each call to send_messages will return a new instance of ResponseHandler
|
55
|
+
handler = args[:response_handler] || ResponseHandler.new
|
56
|
+
handler.process_response(response)
|
57
|
+
handler
|
58
|
+
end
|
59
|
+
|
60
|
+
def verify_deliveries(receipt_ids, **args)
|
61
|
+
response = get_receipts(receipt_ids)
|
62
|
+
handler = args[:response_handler] || ResponseHandler.new
|
63
|
+
handler.process_response(response)
|
64
|
+
handler
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def push_notifications(messages)
|
70
|
+
@http_client.post(
|
71
|
+
push_url,
|
72
|
+
body: messages.to_json,
|
73
|
+
headers: headers,
|
74
|
+
accept_encoding: @gzip
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def push_url
|
79
|
+
'https://exp.host/--/api/v2/push/send'
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_receipts(receipt_ids)
|
83
|
+
@http_client.post(
|
84
|
+
receipts_url,
|
85
|
+
body: { ids: receipt_ids }.to_json,
|
86
|
+
headers: headers,
|
87
|
+
accept_encoding: @gzip
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def receipts_url
|
92
|
+
'https://exp.host/--/api/v2/push/getReceipts'
|
93
|
+
end
|
94
|
+
|
95
|
+
def headers
|
96
|
+
headers = {
|
97
|
+
'Content-Type' => 'application/json',
|
98
|
+
'Accept' => 'application/json'
|
99
|
+
}
|
100
|
+
headers
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class ResponseHandler
|
105
|
+
attr_reader :response, :invalid_push_tokens, :receipt_ids, :errors, :raw_errors
|
106
|
+
|
107
|
+
def initialize(error_builder = ErrorBuilder.new)
|
108
|
+
@error_builder = error_builder
|
109
|
+
@response = nil
|
110
|
+
@receipt_ids = []
|
111
|
+
@invalid_push_tokens = []
|
112
|
+
@errors = []
|
113
|
+
@raw_errors = []
|
114
|
+
end
|
115
|
+
|
116
|
+
def process_response(response)
|
117
|
+
@response = response
|
118
|
+
|
119
|
+
case response.code.to_s
|
120
|
+
when '504'
|
121
|
+
# NOTE: Raise specific error so app knows to retry the request.
|
122
|
+
raise Exponent::Push::GatewayTimeoutError.new(
|
123
|
+
"Request timed out: #{response.code} #{response.response_body}"
|
124
|
+
)
|
125
|
+
when '502'
|
126
|
+
# NOTE: Raise specific error so app knows to retry the request.
|
127
|
+
raise Exponent::Push::BadGatewayError.new(
|
128
|
+
"Request timed out: #{response.code} #{response.response_body}"
|
129
|
+
)
|
130
|
+
when '400'
|
131
|
+
# NOTE: The app will want to handle expo server error response differently.
|
132
|
+
# Responding with an object that exposes what the expo server errored with.
|
133
|
+
@raw_errors = body.fetch('errors', [])
|
134
|
+
when /(^4|^5)/
|
135
|
+
# NOTE: Catch-all in case we do not get a 400 or 504 error. This will raise unknown
|
136
|
+
# error with information on what the issue was.
|
137
|
+
raise @error_builder.parse_response(response)
|
138
|
+
else
|
139
|
+
sort_results
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def errors?
|
144
|
+
@errors.any?
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def sort_results
|
150
|
+
data = body&.fetch('data', nil) || nil
|
151
|
+
|
152
|
+
# something is definitely wrong
|
153
|
+
return if data.nil?
|
154
|
+
|
155
|
+
# Array indicates a response from the /send endpoint
|
156
|
+
# Hash indicates a response from the /getReceipts endpoint
|
157
|
+
if data.is_a? Array
|
158
|
+
data.each do |push_ticket|
|
159
|
+
receipt_id = push_ticket.fetch('id', nil)
|
160
|
+
if push_ticket.fetch('status', nil) == 'ok'
|
161
|
+
@receipt_ids.push(receipt_id) unless receipt_id.nil?
|
162
|
+
else
|
163
|
+
process_error(push_ticket)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
else
|
167
|
+
process_receipts(data)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def process_receipts(receipts)
|
172
|
+
receipts.each do |receipt_id, receipt|
|
173
|
+
@receipt_ids.push(receipt_id) unless receipt_id.nil?
|
174
|
+
process_error(receipt) unless receipt.fetch('status') == 'ok'
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def process_error(push_ticket)
|
179
|
+
message = push_ticket.fetch('message')
|
180
|
+
invalid = message.match(/ExponentPushToken\[(...*)\]/)
|
181
|
+
unregistered = message.match(/\"(...*)\"/)
|
182
|
+
error_class = @error_builder.parse_push_ticket(push_ticket)
|
183
|
+
|
184
|
+
@invalid_push_tokens.push(invalid[0]) unless invalid.nil?
|
185
|
+
@invalid_push_tokens.push(unregistered[1]) unless unregistered.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
|
+
end
|
202
|
+
|
203
|
+
class ErrorBuilder
|
204
|
+
def parse_response(response)
|
205
|
+
with_error_handling(response) do
|
206
|
+
error = response.fetch('errors')
|
207
|
+
error_name = error.fetch('code')
|
208
|
+
message = error.fetch('message')
|
209
|
+
|
210
|
+
get_error_class(error_name).new(message)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def parse_push_ticket(push_ticket)
|
215
|
+
with_error_handling(push_ticket) do
|
216
|
+
message = push_ticket.fetch('message')
|
217
|
+
get_error_class(push_ticket.fetch('details').fetch('error')).new(message)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
%i[erroneous successful].each do |selector|
|
222
|
+
define_method(:"build_from_#{selector}") do |response|
|
223
|
+
with_error_handling(response) do
|
224
|
+
send "from_#{selector}_response", response
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def with_error_handling(response)
|
232
|
+
yield(response)
|
233
|
+
rescue KeyError, NoMethodError => e
|
234
|
+
unknown_error_format(response, e)
|
235
|
+
end
|
236
|
+
|
237
|
+
def validate_error_name(condition)
|
238
|
+
condition ? yield : Exponent::Push::UnknownError
|
239
|
+
end
|
240
|
+
|
241
|
+
def get_error_class(error_name)
|
242
|
+
validate_error_name(Exponent::Push.error_names.include?(error_name)) do
|
243
|
+
Exponent::Push.const_get("#{error_name}Error")
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def unknown_error_format(response, e)
|
248
|
+
error_message = e.message
|
249
|
+
error_response_code = response.response_code
|
250
|
+
error_response_body = response.response_body
|
251
|
+
Exponent::Push::UnknownError.new(
|
252
|
+
"Unknown error format: #{e.message} #{error_response_code} #{error_response_body}"
|
253
|
+
)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
Error = Class.new(StandardError)
|
258
|
+
|
259
|
+
def self.error_names
|
260
|
+
%w[DeviceNotRegistered MessageTooBig
|
261
|
+
MessageRateExceeded InvalidCredentials GatewayTimeout
|
262
|
+
BadGateway Unknown]
|
263
|
+
end
|
264
|
+
|
265
|
+
error_names.each do |error_name|
|
266
|
+
const_set "#{error_name}Error", Class.new(Error)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|