expo-push 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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.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 '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
@@ -0,0 +1,2 @@
1
+ class TooManyMessagesError < StandardError
2
+ end
@@ -0,0 +1,3 @@
1
+ module Exponent
2
+ VERSION = '1.0.2'.freeze
3
+ 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